philips: rewrite manuSpecificPhilips2 encode/decode, fix effects and state reporting#11655
Open
Mihonarium wants to merge 29 commits intoKoenkk:masterfrom
Open
philips: rewrite manuSpecificPhilips2 encode/decode, fix effects and state reporting#11655Mihonarium wants to merge 29 commits intoKoenkk:masterfrom
Mihonarium wants to merge 29 commits intoKoenkk:masterfrom
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](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.
…t logs for production, not duplicating lists and their reverses)
…ter's HS colors handling
Author
|
Okay, I'm satisfied/done! Feel free to make changes/merge. |
Koenkk
reviewed
Mar 7, 2026
Koenkk
reviewed
Mar 7, 2026
Koenkk
reviewed
Mar 7, 2026
Koenkk
reviewed
Mar 7, 2026
Owner
|
@chrivers could you also take a look? |
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
…k in case a bulb doesn't support manuSpecificPhilips2
Koenkk
reviewed
Mar 13, 2026
| // 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")) { |
Owner
There was a problem hiding this comment.
I was checking the current tz converter, but this also has some special features like:
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
manuSpecificPhilips2encoder/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
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.