philips: add hue native control#11655
Conversation
Per the Bifrost spec, gradient light strips support three rendering styles: `linear` (smooth color blend), `scattered` (one color per segment), and `mirrored` (symmetric from center). Previously the style was hardcoded to Linear everywhere. Now:
- Exposed as an enum (`gradient_style`) with values `linear`, `scattered`, `mirrored`
- Settable alongside gradient colors: `{"gradient": [...], "gradient_style": "mirrored"}`
- Settable standalone (re-sends current gradient colors with the new style)
- Published by the fromZigbee converter when reported by the device
- `encodeGradientColors` parameterized instead of hardcoding `0x00`
As per @chrivers suggestions: The Fz converter now publishes `philips2_raw` containing the unaltered hex-encoded state blob from the device. This enables clients like Bifrost to perform their own decoding without depending on z2m's interpretation layer, as [specifically requested](#8697) by the spec author. The Fz converter also now publishes `gradient_xy` containing an array of `{x, y}` coordinate pairs alongside the existing `gradient` RGB hex array. The RGB conversion is lossy — it round-trips through a device-dependent color space with undefined gamut, causing colors like saturated red to appear washed out (issue #8697 point 1). The XY representation preserves the exact device-independent coordinates from the wire format.
the old test expectations were computed with the wrong Y scaling constant (0.8413 vs 0.8264)
Add tests for DecodeManuSpecificPhilips2 and EncodeManuSpecificPhilips2 functions. - Bifrost spec examples: Decodes all 7 hex examples from the Bifrost spec doc and verifies every field — on/off, brightness, colorXY, fadeSpeed, effectType, effectSpeed, gradientColors (with style), gradientParams. Also verifies that examples 3 and 7 share the colors they should (they were captured from the same session). - Round-trip tests: Encode(Decode(hex)) ≈ hex for all 7 Bifrost examples, plus Decode(Encode(data)) ≈ data for every individual field type and for all fields simultaneously. Tolerances match quantization precision — exact for integers, ±0.01 for 8-bit floats, ±0.001 for 12-bit gradient XY. - Gradient byte packing: The Bifrost spec's x=0x123, y=0x456 → [0x23, 0x61, 0x45] example plus all four corner cases (0x000/0xFFF combinations). - Wire order: Verifies the critical non-monotonic ordering where GRADIENT_COLORS (flag bit 8) appears before EFFECT_SPEED (bit 7) and GRADIENT_PARAMS (bit 6) in the actual byte stream. - Individual field encoding/decoding: ON_OFF, brightness, colorMirek, fadeSpeed, effectType (all 13 known effects), gradientParams at fractional resolution, gradient style byte positions.
…t logs for production, not duplicating lists and their reverses)
…ter's HS colors handling
|
Okay, I'm satisfied/done! Feel free to make changes/merge. |
|
@chrivers could you also take a look? |
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
|
Okay, done! Please test that the special features work correctly if Philips2 native control hasn't been set to True |
|
@Koenkk feel free to edit/change anything! also cc @LaurentvdBos @chrivers |
|
Thanks! |
|
Yay! By the way (I hope this is allowed? it is awesome and I think everyone should use it, but let me know if not, I'll remove it, it's obviously somewhat offtopic): If you have more than three lights, I have a card that allows arranging them spatially on a 2D canvas to control arbitrary sets of lights in as few taps and as little attention as possible: https://github.com/Mihonarium/HASS-Spatial-Lights-Card/ It was inspired by the Hue app, where I could drag lots of lights around the color wheel. I wanted something at least as good; I think I made something a lot better. This is my living room, with lights corresponding to where they physically are (and I can select one of the Hue bulbs and pick any of the effects added to presets for them in two taps):
|
|
Also: I guess #8697, Koenkk/zigbee2mqtt#18406, Koenkk/zigbee2mqtt#15891, Koenkk/zigbee2mqtt#24438, Koenkk/zigbee2mqtt#31218 can be closed |
|
Congratulations for getting this merged, I'm really looking forward to testing it! |
|
Had to revert this PR because it seems to break standard bulb control: https://github.com/Koenkk/zigbee2mqtt/actions/runs/24853454219/job/72759920745?pr=31783, e.g. |
|
(Continued in #12008) |
|
(Fixed, needs updates to the tests in Koenkk/zigbee2mqtt#31785.) |
|
This is huge! Thanks a bunch @Mihonarium |
|
I'm noticing a few bulbs having issues when in a zigbee group. I can apply effects to them individually and they will work. If there is a group with multiple models some of them will apply while others do not. All of them accept the effect individually. [5/3/2026, 9:11:01 PM] z2m: Publish 'set' 'effect' to 'group_bulbs_island' failed: 'Error: Status 'UNSUPPORTED_CLUSTER' manuSpecificPhilips2 |
|
Opened a draft PR to disable native control for mixed groups. idk if there's a way to send Hue-only effects to mixed groups; that would certainly be preferable if possible |
|
I am seeing transitions in automations sometimes being in seconds and sometimes in tenths of a second. I know the hue hub expected the latter but I am just not sure of the inconsistency. |
To be clear I'm seeing this issue with groups that contain only hue bulbs but they may be different feature sets. One group has 9290011998B (white ambiance) mixed with a 9290023351 (warm white). Individually they can be set but in a group it doesn't seem to work. On a seperate note I have 3 9290024784 in a group together and they don't seem to accept effects as a group but do work individually. They also fail with the same error. [5/4/2026, 8:08:56 PM] z2m: Publish 'set' 'effect' to 'group_bulbs_island' failed: 'Error: Status 'UNSUPPORTED_CLUSTER' manuSpecificPhilips2' One thing I notice is in the Z2M gui you have the option to select the effect on an individual device but not on a group under the expose tab. Would this have any effect on being able to use effects on groups? |
|
Is there anything I can do to rectify 3 of the same hue bulb not accepting effects in a group? |

philips: add hue native control
add/reweite manuSpecificPhilips2 encode/decode, fix effects and state reporting
Addresses #8697, Koenkk/zigbee2mqtt#18406, Koenkk/zigbee2mqtt#15891, Koenkk/zigbee2mqtt#24438.
Summary
Adds a
hue_native_controloption for native control of Philips lights.Rewrites the Philips
manuSpecificPhilips2encoder/decoder drafted by @LaurentvdBos based on the Bifrost spec, fixing several data corruption bugs and adding missing functionality. Also improves effect UX: effects now report state, support color+speed in a single command, and the color wheel/effect interaction is configurable.Opt-in
By default, standard ZCL converters handle on/off, brightness, color, and color temperature. The behavior is unchanged from before this PR. The manuSpecificPhilips2 cluster can send all of these in a single atomic command, which is required for full effect support: changing color while an effect is running, stopping effects by changing color, and setting an effect with a specific color in one command. However, native control also changes some user-facing behavior (e.g., no simulated fade-to-off transition), so it is opt-in.
Users can enable it per-device in Z2M Settings → Settings (Specific), or globally via
device_options: { hue_native_control: true }in configuration.yaml.Bug fixes
(Compared to the manySpecificPhilips2 draft.)
decodeGradientColorsassembled the Y value from the wrong nibbles, corrupting every gradient color's Y componentdecodeGradientColorsreturnedposition + nColors * 3instead ofposition + size + 1, causing the parser to land at the wrong offset for subsequent fieldsGRADIENT_COLORS_MAX_Ywas0.8413(CIE D65 white point), should be0.8264(Wide Color Gamut upper bound per Bifrost spec)knownEffectskey shift — Effect hex keys were shifted by one byte, causing wrong effects to be triggeredphilipsTz.effect.convertSetdidn't return{state}, so Z2M's effect state was alwaysnull"none".bind("manuSpecificPhilips2", ...)call was inside theif (args.gradient)block. Moved to cover allhueEffectdevices.New features
effect+colorin one command —{"effect": "candle", "color": "#FF4400"}activates the effect at that color atomically. Also supportseffect_speedin the same payload.effect_color(new expose) — Sets the base color of the active effect without stopping it. Accepts hex or XY.effect_color_mode(new per-device option) — Controls what happens when the regular color wheel is used while an effect is active:"stop"(default): color change stops the effect (matches Hue app)"update": color change re-sends the effect with the new coloreffect_coloror "update" mode) with an explicit brightness, the brightness is sent as a separate command after the effect, since effects reset brightness on activation.effectandeffect_speedaccess changed toSTATE_SET— Current values are now visible in the Z2M frontend and HA.effect_speed— Settable via both the new path (philipsLightTz) and the effect converter.gradient_scale/gradient_offset— Settable. Fixed-point 5.3 format exposed as float.gradient_style— Fully settable (linear,scattered,mirrored). Was decoded but not writable.gradient_xy— Lossless XY coordinates published alongside RGB hexgradient.philips2_raw— Raw hex blob published for advanced clients (e.g. Bifrost) to decode independently.transition→fadeSpeed— Mapped to the Bifrost spec's per-message fade speed field.(Feel free to remove or ask me to remove the effect_color, effect_color_mode, and related stuff if it goes against the overall philosophy.)
Encoder/decoder rewrite
Replaced the ad-hoc hex string parsing with structured
Encode/DecodeManuSpecificPhilips2functions usingDataViewfor proper binary field handling. The wire order follows the Bifrost spec (gradient colors before effect speed before gradient params — reversed relative to flag bit order). Each field type is defined as aHueTypeDetailsstruct withencode/decode/flag/maxLength, iterated in wire order.Tests
test/philips.test.ts— corrected expected values for the MAX_Y fixtest/generateDefinition.test.ts— addedmanuSpecificPhilips2Fzto expected fromZigbee and new toZigbee keys/exposes for the auto-generated definition testtest/modernExtend.test.ts— replacedphilips.fz.gradientwithphilips.manuSpecificPhilips2Fzin expected fromZigbee, added new toZigbee keys/exposes for the philipsLight testtest/philips2.test.ts(61 tests):Encode→DecodeandDecode→Encode→Decode) for every field typeBackward compatibility
All changes are backward-compatible. Existing MQTT payloads, automations, and gradient scenes continue to work. New exposes (
effect_color,effect_speed,gradient_style,gradient_xy,philips2_raw) are additive. Theeffect_color_modeoption defaults to"stop"(Hue app behavior). Devices may need a reconfigure after updating to establish themanuSpecificPhilips2binding for state reports.philipsFz.gradientis superseded bymanuSpecificPhilips2Fzand is no longer registered for any device. It could be possible that external converter authors import it, but it's not documented anywhere and people are more likely to usephilipsLight(). Probably worth considering deprecated/removing in the future? I've not removed it myself though.Limitations
After activating an effect, we sync the device state via a 1-second setTimeout, as some Hue effects change light color/brightness. If the device goes offline in the window, the error is silently caught. There could be more robust approaches.
Other
I made a very cool hass dashboard card (inspired by the Hue app, but better than it)! See #11655 (comment)