Skip to content

philips: add hue native control#11655

Merged
Koenkk merged 34 commits into
Koenkk:masterfrom
Mihonarium:patch-1
Apr 23, 2026
Merged

philips: add hue native control#11655
Koenkk merged 34 commits into
Koenkk:masterfrom
Mihonarium:patch-1

Conversation

@Mihonarium
Copy link
Copy Markdown
Contributor

@Mihonarium Mihonarium commented Mar 4, 2026

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_control option for native control of Philips lights.

Rewrites the Philips manuSpecificPhilips2 encoder/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.)

  • Y-coordinate byte packingdecodeGradientColors assembled the Y value from the wrong nibbles, corrupting every gradient color's Y component
  • Off-by-two returndecodeGradientColors returned position + nColors * 3 instead of position + size + 1, causing the parser to land at the wrong offset for subsequent fields
  • Wrong Y scaling constantGRADIENT_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 reportedphilipsTz.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.
  • transitionfadeSpeed — 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 #11655 (comment)

LaurentvdBos and others added 16 commits January 3, 2026 13:49
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.
@Mihonarium
Copy link
Copy Markdown
Contributor Author

Okay, I'm satisfied/done!

Feel free to make changes/merge.

Comment thread src/lib/philips.ts Outdated
Comment thread src/lib/philips.ts Outdated
Comment thread src/lib/philips.ts
Comment thread src/lib/philips.ts
@Koenkk
Copy link
Copy Markdown
Owner

Koenkk commented Mar 7, 2026

@chrivers could you also take a look?

@Mihonarium
Copy link
Copy Markdown
Contributor Author

Mihonarium commented Apr 22, 2026

Okay, done! Please test that the special features work correctly if Philips2 native control hasn't been set to True

@Mihonarium
Copy link
Copy Markdown
Contributor Author

Mihonarium commented Apr 22, 2026

@Koenkk feel free to edit/change anything!

also cc @LaurentvdBos @chrivers

@Mihonarium Mihonarium requested a review from Koenkk April 22, 2026 20:16
Comment thread src/lib/philips.ts Outdated
@Koenkk
Copy link
Copy Markdown
Owner

Koenkk commented Apr 23, 2026

Thanks!

@Koenkk Koenkk merged commit e079ac9 into Koenkk:master Apr 23, 2026
3 checks passed
@Mihonarium
Copy link
Copy Markdown
Contributor Author

Mihonarium commented Apr 23, 2026

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):

image

@Mihonarium
Copy link
Copy Markdown
Contributor Author

@Mihonarium Mihonarium deleted the patch-1 branch April 23, 2026 19:08
@mundschenk-at
Copy link
Copy Markdown
Contributor

Congratulations for getting this merged, I'm really looking forward to testing it!

Koenkk added a commit that referenced this pull request Apr 23, 2026
@Mihonarium Mihonarium restored the patch-1 branch April 23, 2026 19:14
@Koenkk
Copy link
Copy Markdown
Owner

Koenkk commented Apr 23, 2026

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. Should publish messages to zigbee devices fails. @Mihonarium could you check why it fails? (clone z2m, use pnpm link ../zigbee-herdsman-converters && pnpm i and then execute z2m tests with pnpm test)

Koenkk added a commit that referenced this pull request Apr 23, 2026
@Mihonarium
Copy link
Copy Markdown
Contributor Author

(Continued in #12008)

@Mihonarium
Copy link
Copy Markdown
Contributor Author

(Fixed, needs updates to the tests in Koenkk/zigbee2mqtt#31785.)

@Mihonarium Mihonarium changed the title philips: rewrite manuSpecificPhilips2 encode/decode, fix effects and state reporting philips: add hue native control May 1, 2026
@Grandma-Betty
Copy link
Copy Markdown

This is huge! Thanks a bunch @Mihonarium

@lundyfpv
Copy link
Copy Markdown

lundyfpv commented May 4, 2026

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

@Mihonarium
Copy link
Copy Markdown
Contributor Author

Mihonarium commented May 4, 2026

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

@Nixon506E
Copy link
Copy Markdown
Contributor

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.

@lundyfpv
Copy link
Copy Markdown

lundyfpv commented May 5, 2026

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

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?

@lundyfpv
Copy link
Copy Markdown

lundyfpv commented May 8, 2026

Is there anything I can do to rectify 3 of the same hue bulb not accepting effects in a group?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants