Skip to content

philips: rewrite manuSpecificPhilips2 encode/decode, fix effects and state reporting#11655

Open
Mihonarium wants to merge 29 commits intoKoenkk:masterfrom
Mihonarium:patch-1
Open

philips: rewrite manuSpecificPhilips2 encode/decode, fix effects and state reporting#11655
Mihonarium wants to merge 29 commits intoKoenkk:masterfrom
Mihonarium:patch-1

Conversation

@Mihonarium
Copy link

@Mihonarium Mihonarium commented Mar 4, 2026

philips: rewrite manuSpecificPhilips2 encode/decode, fix effects and state reporting

Addresses #8697, Koenkk/zigbee2mqtt#18406, Koenkk/zigbee2mqtt#15891, Koenkk/zigbee2mqtt#24438.

Summary

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

Bug fixes

  • 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.

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](Koenkk#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 Koenkk#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
Author

Okay, I'm satisfied/done!

Feel free to make changes/merge.

@Koenkk
Copy link
Owner

Koenkk commented Mar 7, 2026

@chrivers could you also take a look?

// Since Z2M calls us once and marks us as used for all our keys, we must delegate
// ALL message keys to the appropriate standard converters in one shot,
// mimicking Z2M's own per-key dispatch loop.
if (utils.isEndpoint(entity) && !entity.supportsInputCluster("manuSpecificPhilips2")) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was checking the current tz converter, but this also has some special features like:

// Simulate 'Off' with transition via 'MoveToLevelWithOnOff', otherwise just use 'Off'.

I expect that with this converter the behaviour will be slightly different. To prevent issues for users, maybe wen can make this converter opt-in for now through an option? Example

options: [exposes.options.color_sync(), exposes.options.transition()],

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.

3 participants