# philips: add hue native control
add/reweite manuSpecificPhilips2 encode/dec…ode, fix effects and state reporting
Addresses #8697, Koenkk/zigbee2mqtt#18406, Koenkk/zigbee2mqtt#15891, Koenkk/zigbee2mqtt#24438.
## Summary
Adds a `hue_native_control` option for native control of Philips lights.
Rewrites the Philips `manuSpecificPhilips2` encoder/decoder drafted by @LaurentvdBos based on the [Bifrost spec](https://github.com/chrivers/bifrost/blob/master/doc/hue-zigbee-format.md), 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.)
- **Y-coordinate byte packing** — `decodeGradientColors` assembled the Y value from the wrong nibbles, corrupting every gradient color's Y component
- **Off-by-two return** — `decodeGradientColors` returned `position + nColors * 3` instead of `position + size + 1`, causing the parser to land at the wrong offset for subsequent fields
- **Wrong Y scaling constant** — `GRADIENT_COLORS_MAX_Y` was `0.8413` (CIE D65 white point), should be `0.8264` (Wide Color Gamut upper bound per Bifrost spec)
- **`knownEffects` key shift** — Effect hex keys were shifted by one byte, causing wrong effects to be triggered
- **Brightness validation** — Values 0 and 255 are invalid per spec, now clamped to 1–254
- **Effect state never reported** — `philipsTz.effect.convertSet` didn't return `{state}`, so Z2M's effect state was always `null`
- **Stale effect state** — Changing color stops the active effect on the device, but Z2M kept showing the old effect name. Now cleared to `"none"`.
- **Attribute reports only for gradient devices** — The `bind("manuSpecificPhilips2", ...)` call was inside the `if (args.gradient)` block. Moved to cover all `hueEffect` devices.
## New features
- **All Bifrost effects** — Added sunset, sparkle, opal, glisten, underwater, cosmos, sunbeam, enchant (13 total including no_effect, candle, fireplace, prism/colorloop, sunrise)
- **`effect` + `color` in one command** — `{"effect": "candle", "color": "#FF4400"}` activates the effect at that color atomically. Also supports `effect_speed` in 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 color
- **Deferred brightness on effect re-send** — When re-sending an effect (via `effect_color` or "update" mode) with an explicit brightness, the brightness is sent as a separate command after the effect, since effects reset brightness on activation.
- **`effect` and `effect_speed` access changed to `STATE_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 hex `gradient`.
- **`philips2_raw`** — Raw hex blob published for advanced clients (e.g. Bifrost) to decode independently.
- **HSV→XY conversion** — HSV colors are now converted correctly instead of being silently dropped.
- **`transition` → `fadeSpeed`** — Mapped to the Bifrost spec's per-message fade speed field.
- **Delayed state read after effect activation** — Reads device state 1s after activating an effect to sync actual brightness.
(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`/`DecodeManuSpecificPhilips2` functions using `DataView` for 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 a `HueTypeDetails` struct with `encode`/`decode`/`flag`/`maxLength`, iterated in wire order.
## Tests
- Updated `test/philips.test.ts` — corrected expected values for the MAX_Y fix
- Updated `test/generateDefinition.test.ts` — added `manuSpecificPhilips2Fz` to expected fromZigbee and new toZigbee keys/exposes for the auto-generated definition test
- Updated `test/modernExtend.test.ts` — replaced `philips.fz.gradient` with `philips.manuSpecificPhilips2Fz` in expected fromZigbee, added new toZigbee keys/exposes for the philipsLight test
- Added `test/philips2.test.ts` (61 tests):
- Known-payload decode of all 7 Bifrost spec examples
- Round-trip (`Encode→Decode` and `Decode→Encode→Decode`) for every field type
- Gradient byte packing edge cases (0x000/0xFFF, Bifrost spec example x=0x123 y=0x456)
- Wire order verification (non-monotonic flag-to-position mapping)
- All 13 effect types
- 108 philips tests total (47 + 61), all passing
## Backward 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. The `effect_color_mode` option defaults to `"stop"` (Hue app behavior). Devices may need a reconfigure after updating to establish the `manuSpecificPhilips2` binding for state reports.
`philipsFz.gradient` is superseded by `manuSpecificPhilips2Fz` and 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 use `philipsLight()`. 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 https://github.com/Koenkk/zigbee-herdsman-converters/pull/11655#issuecomment-4307065598