UNPKG

zigbee-herdsman-converters

Version:

Collection of device converters to be used with zigbee-herdsman

1,779 lines 89.3 kB
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.HueGradientStyle = exports.HueEffectType = exports.fz = exports.hueEffects = exports.gradientScenes = exports.tz = exports.m = exports.manuSpecificPhilips2Fz = exports.knownEffects = void 0;
exports.DecodeManuSpecificPhilips2 = DecodeManuSpecificPhilips2;
exports.EncodeManuSpecificPhilips2 = EncodeManuSpecificPhilips2;
exports.decodeGradientColors = decodeGradientColors;
exports.encodeGradientColors = encodeGradientColors;
const zigbee_herdsman_1 = require("zigbee-herdsman");
const fz = __importStar(require("../converters/fromZigbee"));
const tz = __importStar(require("../converters/toZigbee"));
const reporting = __importStar(require("../lib/reporting"));
const libColor = __importStar(require("./color"));
const color_1 = require("./color");
const exposes = __importStar(require("./exposes"));
const light = __importStar(require("./light"));
const logger_1 = require("./logger");
const modernExtend = __importStar(require("./modernExtend"));
const globalStore = __importStar(require("./store"));
const utils = __importStar(require("./utils"));
const utils_1 = require("./utils");
const NS = "zhc:philips";
const ea = exposes.access;
const e = exposes.presets;
const eNumeric = exposes.Numeric;
// Gradient color XY scaling constants per Bifrost spec.
// MAX_X = 0.7347: maximum X inside the visible light spectrum / Wide Gamut red X.
// MAX_Y = 0.8264: outer bound of the Wide Gamut Y axis.
// NOTE: many older implementations incorrectly use 0.8431 for MAX_Y.
const GRADIENT_COLORS_MAX_X = 0.7347;
const GRADIENT_COLORS_MAX_Y = 0.8264;
const encodeRGBToScaledGradient = (hex) => {
    const xy = color_1.ColorRGB.fromHex(hex).toXY();
    const x = (xy.x * 4095) / GRADIENT_COLORS_MAX_X;
    const y = (xy.y * 4095) / GRADIENT_COLORS_MAX_Y;
    const xx = Math.round(x).toString(16).padStart(3, "0");
    const yy = Math.round(y).toString(16).padStart(3, "0");
    return [xx[1], xx[2], yy[2], xx[0], yy[0], yy[1]].join("");
};
const decodeScaledGradientToRGB = (p) => {
    const x = p[3] + p[0] + p[1];
    const y = p[4] + p[5] + p[2];
    const xx = Number(((Number.parseInt(x, 16) * GRADIENT_COLORS_MAX_X) / 4095).toFixed(4));
    const yy = Number(((Number.parseInt(y, 16) * GRADIENT_COLORS_MAX_Y) / 4095).toFixed(4));
    return new color_1.ColorXY(xx, yy).toRGB().toHEX();
};
const COLOR_MODE_GRADIENT = "4b01";
const COLOR_MODE_COLOR_XY = "0b00";
const COLOR_MODE_COLOR_TEMP = "0f00";
const COLOR_MODE_EFFECT = "ab00";
const COLOR_MODE_BRIGHTNESS = "0300";
exports.knownEffects = {
    "0180": "candle",
    "0280": "fireplace",
    "0380": "colorloop",
    "0980": "sunrise",
    "0a80": "sparkle",
    "0b80": "opal",
    "0c80": "glisten",
    "0d80": "sunset",
    "0e80": "underwater",
    "0f80": "cosmos",
    "1080": "sunbeam",
    "1180": "enchant",
};
const HUE_TAP_LOOKUP = {
    34: "press_1",
    16: "press_2",
    17: "press_3",
    18: "press_4",
    // Actions below are never generated by a Hue Tap but by a PMT 215Z
    // https://github.com/Koenkk/zigbee2mqtt/issues/18088
    98: "press_3_and_4",
    99: "release_3_and_4",
    100: "press_1_and_2",
    101: "release_1_and_2",
};
exports.manuSpecificPhilips2Fz = {
    cluster: "manuSpecificPhilips2",
    type: ["attributeReport", "readResponse"],
    convert: (model, msg, publish, options, meta) => {
        const retval = {};
        if (msg.data.state !== undefined) {
            // Publish the raw, unaltered state blob so advanced clients (e.g. Bifrost)
            // can perform their own decoding without depending on z2m's interpretation.
            retval["philips_raw"] = msg.data.state.toString("hex");
            const decoded = DecodeManuSpecificPhilips2(msg.data.state);
            logger_1.logger.debug(`Decoded manuSpecificPhilips2 state: ${JSON.stringify(decoded)}`, NS);
            if (decoded.onOff !== undefined) {
                retval["state"] = decoded.onOff ? "ON" : "OFF";
            }
            if (decoded.brightness !== undefined) {
                retval["brightness"] = decoded.brightness;
            }
            if (decoded.colorMirek !== undefined) {
                retval["color_temp"] = decoded.colorMirek;
                retval["color_mode"] = "color_temp";
            }
            if (decoded.colorXY !== undefined) {
                retval["color"] = decoded.colorXY.toObject();
                retval["color_mode"] = "xy";
            }
            if (decoded.fadeSpeed !== undefined) {
                retval["transition"] = decoded.fadeSpeed / 10;
            }
            if (decoded.effectType !== undefined) {
                retval["effect"] = effectNames[decoded.effectType] ?? `unknown_0x${decoded.effectType.toString(16)}`;
            }
            if (decoded.effectSpeed !== undefined) {
                retval["effect_speed"] = decoded.effectSpeed;
            }
            if (decoded.gradientColors !== undefined) {
                // RGB hex for backward compat with z2m frontend and existing automations
                retval["gradient"] = decoded.gradientColors.colors.map((c) => c.toRGB().toHEX());
                // Lossless XY coordinates for clients that need device-independent color
                retval["gradient_xy"] = decoded.gradientColors.colors.map((c) => c.toObject());
                retval["gradient_style"] = gradientStyleNames[decoded.gradientColors.style] ?? "unknown";
            }
            if (decoded.gradientParams !== undefined) {
                retval["gradient_scale"] = decoded.gradientParams.scale;
                retval["gradient_offset"] = decoded.gradientParams.offset;
            }
        }
        return retval;
    },
};
// Keys for Philips2-specific features not handled by standard light converters.
const philips2Keys = ["effect_speed", "gradient_scale", "gradient_offset", "gradient_style", "effect_color"];
const philipsModernExtend = {
    addPhilipsGenBasicCluster: () => modernExtend.deviceAddCustomCluster("genBasic", {
        name: "genBasic",
        ID: zigbee_herdsman_1.Zcl.Clusters.genBasic.ID,
        attributes: {
            ledIndication: {
                name: "ledIndication",
                ID: 0x0033, // 51
                type: zigbee_herdsman_1.Zcl.DataType.BOOLEAN,
                manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V,
                write: true,
            },
            deviceMode: {
                name: "deviceMode",
                ID: 0x0034, // 52
                type: zigbee_herdsman_1.Zcl.DataType.ENUM8,
                manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V,
                write: true,
            },
        },
        commands: {},
        commandsResponse: {},
    }),
    addPhilipsMsOccupancySensingCluster: () => modernExtend.deviceAddCustomCluster("msOccupancySensing", {
        name: "msOccupancySensing",
        ID: zigbee_herdsman_1.Zcl.Clusters.msOccupancySensing.ID,
        attributes: {
            motionSensitivity: {
                name: "motionSensitivity",
                ID: 0x0030,
                type: zigbee_herdsman_1.Zcl.DataType.UINT8,
                manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V,
                write: true,
            },
        },
        commands: {},
        commandsResponse: {},
    }),
    addCustomClusterManuSpecificPhilipsContact: () => modernExtend.deviceAddCustomCluster("manuSpecificPhilipsContact", {
        name: "manuSpecificPhilipsContact",
        ID: 0xfc06,
        manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V,
        attributes: {
            contact: { name: "contact", ID: 0x0100, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, write: true, max: 0xff },
            contactLastChange: { name: "contactLastChange", ID: 0x0101, type: zigbee_herdsman_1.Zcl.DataType.UINT32, write: true, max: 0xffffffff },
            tamper: { name: "tamper", ID: 0x0102, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, write: true, max: 0xff },
            tamperLastChange: { name: "tamperLastChange", ID: 0x0103, type: zigbee_herdsman_1.Zcl.DataType.UINT32, write: true, max: 0xffffffff },
        },
        commands: {},
        commandsResponse: {},
    }),
    addManuSpecificPhilipsCluster: () => modernExtend.deviceAddCustomCluster("manuSpecificPhilips", {
        name: "manuSpecificPhilips",
        ID: 0xfc00,
        manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V,
        attributes: {
            config: { name: "config", ID: 0x0031, type: zigbee_herdsman_1.Zcl.DataType.BITMAP16, write: true },
        },
        commands: {},
        commandsResponse: {
            hueNotification: {
                name: "hueNotification",
                ID: 0x00,
                parameters: [
                    { name: "button", type: zigbee_herdsman_1.Zcl.DataType.UINT8, max: 0xff },
                    { name: "unknown1", type: zigbee_herdsman_1.Zcl.DataType.UINT24, max: 0xffffff },
                    { name: "type", type: zigbee_herdsman_1.Zcl.DataType.UINT8, max: 0xff },
                    { name: "unknown2", type: zigbee_herdsman_1.Zcl.DataType.UINT8, max: 0xff },
                    { name: "time", type: zigbee_herdsman_1.Zcl.DataType.UINT8, max: 0xff },
                    { name: "unknown3", type: zigbee_herdsman_1.Zcl.DataType.UINT8, max: 0xff },
                ],
            },
        },
    }),
    addManuSpecificPhilips2Cluster: () => modernExtend.deviceAddCustomCluster("manuSpecificPhilips2", {
        name: "manuSpecificPhilips2",
        ID: 0xfc03,
        manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V,
        attributes: {
            state: { name: "state", ID: 0x0002, type: zigbee_herdsman_1.Zcl.DataType.OCTET_STR, write: true },
        },
        commands: {
            multiColor: { name: "multiColor", ID: 0x00, parameters: [{ name: "data", type: zigbee_herdsman_1.Zcl.BuffaloZclDataType.BUFFER }] },
        },
        commandsResponse: {},
    }),
    addManuSpecificPhilips3Cluster: () => modernExtend.deviceAddCustomCluster("manuSpecificPhilips3", {
        name: "manuSpecificPhilips3",
        ID: 0xfc01,
        manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V,
        attributes: {},
        commands: {
            command1: { name: "command1", ID: 1, parameters: [{ name: "data", type: zigbee_herdsman_1.Zcl.BuffaloZclDataType.BUFFER }] },
            command2: { name: "command2", ID: 2, parameters: [{ name: "data", type: zigbee_herdsman_1.Zcl.BuffaloZclDataType.BUFFER }] },
            command3: { name: "command3", ID: 3, parameters: [{ name: "data", type: zigbee_herdsman_1.Zcl.BuffaloZclDataType.BUFFER }] },
            command4: { name: "command4", ID: 4, parameters: [{ name: "data", type: zigbee_herdsman_1.Zcl.BuffaloZclDataType.BUFFER }] },
            command7: { name: "command7", ID: 7, parameters: [{ name: "data", type: zigbee_herdsman_1.Zcl.BuffaloZclDataType.BUFFER }] },
        },
        commandsResponse: {},
    }),
    light: (args) => {
        args = { hueEffect: true, turnsOffAtBrightness1: true, ota: true, ...args };
        if (args.hueEffect || args.gradient)
            args.effect = false;
        if (args.color)
            args.color = { modes: ["xy", "hs"], enhancedHue: true, ...((0, utils_1.isObject)(args.color) ? args.color : {}) };
        const result = modernExtend.light(args);
        const toZigbee = result.toZigbee;
        result.toZigbee = [];
        // Keys we intercept for Hue native control: core light attributes
        // that can be sent atomically via the manuSpecificPhilips2 cluster.
        // Command-only keys (brightness_move, hue_step, etc.) have no Philips2
        // equivalent and are left to their standard converters in the array.
        const nativeKeys = ["state", "brightness", "brightness_percent", "color", "color_temp", "color_temp_percent", "transition"];
        const keys = [...nativeKeys, ...philips2Keys];
        const philipsLightTz = {
            key: keys,
            convertSet: async (entity, key, value, meta) => {
                // Resolve control mode: explicit option wins; otherwise default to standard converters.
                const nativeControl = meta.options.hue_native_control === true;
                const { options } = meta;
                // Check if device supports the manuSpecificPhilips2 cluster.
                // Wrapped in try-catch because supportsInputCluster may throw for
                // custom clusters not in the cluster registry (e.g. in test mocks).
                let hasPhilips2Cluster = false;
                if (utils.isEndpoint(entity)) {
                    try {
                        hasPhilips2Cluster = entity.supportsInputCluster("manuSpecificPhilips2");
                    }
                    catch {
                        hasPhilips2Cluster = false;
                    }
                }
                // Delegate to standard converters if:
                //   - Device doesn't support manuSpecificPhilips2 cluster (old bulbs), OR
                //   - User hasn't opted into native Philips2 control (default)
                // This mimics Z2M's own per-key dispatch so a single message routes
                // each key to the appropriate converter.
                const mustDelegate = (utils.isEndpoint(entity) && !hasPhilips2Cluster) || !nativeControl;
                if (mustDelegate) {
                    const used = new Set();
                    let mergedState = {};
                    // Replicate Z2M's key ordering (publish.ts): when turning off,
                    // state/brightness come first; otherwise they come last. This ensures
                    // standard converters are called in the same sequence as without our
                    // intercepting converter, so e.g. the light turns on before color is
                    // set, and color is set before the light turns off.
                    const messageEntries = Object.entries(meta.message);
                    const stateValue = typeof meta.message.state === "string" ? meta.message.state.toLowerCase() : undefined;
                    const sorter = stateValue === "off" ? 1 : -1;
                    messageEntries.sort((a) => (["state", "brightness", "brightness_percent"].includes(a[0]) ? sorter : sorter * -1));
                    for (const [msgKey, msgValue] of messageEntries) {
                        // Only delegate keys we claim. Command-only keys (brightness_move,
                        // hue_step, etc.) are NOT in our key list — Z2M routes them to
                        // their standard converters directly.
                        if (!keys.includes(msgKey))
                            continue;
                        // philips2Keys have no standard equivalent and are handled below.
                        if (philips2Keys.includes(msgKey))
                            continue;
                        for (const tz of toZigbee) {
                            if (!used.has(tz) && tz.key.includes(msgKey) && tz.convertSet) {
                                used.add(tz);
                                const result = await tz.convertSet(entity, msgKey, msgValue, meta);
                                if (result && "state" in result && result.state) {
                                    mergedState = { ...mergedState, ...result.state };
                                }
                                break;
                            }
                        }
                    }
                    // In delegated mode, we still need to handle Philips2-specific keys (effect_color,
                    // effect_speed, gradient_scale, etc.) below. But if the current call is for a
                    // standard key and no Philips2-specific keys are in the message, we're done.
                    const hasPhilips2Keys = Object.keys(meta.message).some((k) => philips2Keys.includes(k));
                    if (!hasPhilips2Keys) {
                        return Object.keys(mergedState).length > 0 ? { state: mergedState } : undefined;
                    }
                    // Fall through: continue below to handle Philips2-specific fields only.
                }
                // Native control mode (or handling Philips2-specific keys in delegate mode):
                // build a Philips2 payload from the message. In delegate mode, we filter out
                // standard keys since the standard converters already sent them.
                const message = nativeControl
                    ? meta.message
                    : Object.fromEntries(Object.entries(meta.message).filter(([k]) => philips2Keys.includes(k)));
                const newState = {};
                const data = {};
                if (message.state !== undefined && typeof message.state === "string") {
                    const msgState = message.state.toLowerCase();
                    if (["on", "off", "toggle"].includes(msgState) === true) {
                        const targetState = msgState === "toggle" ? (meta.state.state === "ON" ? "off" : "on") : msgState;
                        data.onOff = targetState === "on";
                        newState.state = data.onOff ? "ON" : "OFF";
                    }
                }
                if (message.brightness != null) {
                    // Bifrost spec: brightness values 0 and 255 are INVALID, valid range 1..254
                    data.brightness = clamp(Number(message.brightness), 1, 254);
                }
                else if (message.brightness_percent != null) {
                    data.brightness = clamp(utils.mapNumberRange(Number(message.brightness_percent), 0, 100, 0, 255), 1, 254);
                }
                if (data.brightness !== undefined) {
                    newState.brightness = data.brightness;
                }
                if (message.color != null) {
                    const newColor = libColor.Color.fromConverterArg(message.color);
                    if (newColor.isHSV()) {
                        // Convert HSV → RGB → XY instead of silently dropping
                        const xy = newColor.hsv.toRGB().gammaCorrected().toXY().rounded(4);
                        data.colorXY = xy;
                    }
                    else {
                        const xy = newColor.isRGB() ? newColor.rgb.gammaCorrected().toXY().rounded(4) : newColor.xy;
                        data.colorXY = xy;
                    }
                    newState.color_mode = "xy";
                    newState.color = data.colorXY.toObject();
                }
                if (message.color_temp != null || message.color_temp_percent != null) {
                    const [colorTempMin, colorTempMax] = light.findColorTempRange(entity);
                    const preset = { warmest: colorTempMax, warm: 454, neutral: 370, cool: 250, coolest: colorTempMin };
                    let colorTemp = message.color_temp;
                    if (message.color_temp_percent != null) {
                        colorTemp = utils
                            .mapNumberRange(Number(message.color_temp_percent), 0, 100, colorTempMin != null ? colorTempMin : 154, colorTempMax != null ? colorTempMax : 500)
                            .toString();
                    }
                    if (utils.isString(colorTemp) && colorTemp in preset) {
                        data.colorMirek = utils.getFromLookup(colorTemp, preset);
                    }
                    else {
                        data.colorMirek = Number(colorTemp);
                    }
                    newState.color_mode = "color_temp";
                    newState.color_temp = data.colorMirek;
                }
                // Map transition time to Philips2 fadeSpeed
                // Bifrost spec: 0 = instant, practical range ~2..8, >0x100 = very slow
                if (message.transition != null) {
                    data.fadeSpeed = Math.round(Number(message.transition) * 10);
                }
                else if (options.transition != null && options.transition !== "") {
                    const transition = (0, utils_1.toNumber)(options.transition, "transition");
                    data.fadeSpeed = Math.round(Number(transition) * 10);
                }
                // Effect speed: 0.0 = slowest, 1.0 = fastest (maps to 0..255 byte)
                if (message.effect_speed != null) {
                    data.effectSpeed = clamp(Number(message.effect_speed), 0, 1);
                }
                // Gradient scale/offset: fixed-point 5.3 format, exposed as float
                if (message.gradient_scale != null) {
                    if (data.gradientParams === undefined) {
                        data.gradientParams = { scale: Number(message.gradient_scale), offset: 0 };
                    }
                    else {
                        data.gradientParams.scale = Number(message.gradient_scale);
                    }
                }
                if (message.gradient_offset != null) {
                    if (data.gradientParams === undefined) {
                        data.gradientParams = { scale: 1.0, offset: Number(message.gradient_offset) };
                    }
                    else {
                        data.gradientParams.offset = Number(message.gradient_offset);
                    }
                }
                // Gradient style: stored in state for use with gradient color commands.
                // When gradient colors are also being sent through this path, the style
                // is applied directly to data.gradientColors.style.
                if (message.gradient_style != null) {
                    const styleLookup = {
                        linear: HueGradientStyle.Linear,
                        scattered: HueGradientStyle.Scattered,
                        mirrored: HueGradientStyle.Mirrored,
                    };
                    const style = styleLookup[String(message.gradient_style).toLowerCase()];
                    if (style !== undefined) {
                        if (data.gradientColors !== undefined) {
                            data.gradientColors.style = style;
                        }
                        newState.gradient_style = message.gradient_style;
                    }
                }
                // When color/color_temp changes without an explicit effect command,
                // behavior depends on the effect_color_mode option:
                // - "stop" (default, matches Hue app): color change stops the effect
                // - "update": color change re-sends the active effect with the new color
                if ((data.colorXY !== undefined || data.colorMirek !== undefined) && message.effect === undefined) {
                    const effectColorMode = meta.options.effect_color_mode ?? "stop";
                    const activeEffect = meta.state?.effect;
                    if (effectColorMode === "update" && activeEffect && activeEffect !== "none" && activeEffect in effectLookupAll) {
                        // Re-send the active effect with the new color
                        data.effectType = effectLookupAll[activeEffect];
                        newState.effect = activeEffect;
                    }
                    else {
                        // Hue app behavior: color change stops effect, clear stale state
                        newState.effect = "none";
                    }
                }
                // Handle effect_color: explicitly set the active effect's base color
                // without stopping it. If no effect is active, just sets the color.
                if (message.effect_color != null) {
                    const newColor = libColor.Color.fromConverterArg(message.effect_color);
                    if (newColor.isHSV()) {
                        data.colorXY = newColor.hsv.toRGB().gammaCorrected().toXY().rounded(4);
                    }
                    else {
                        data.colorXY = newColor.isRGB() ? newColor.rgb.gammaCorrected().toXY().rounded(4) : newColor.xy;
                    }
                    newState.color_mode = "xy";
                    newState.color = data.colorXY.toObject();
                    // Re-send the active effect with the new color
                    const activeEffect = meta.state?.effect;
                    if (activeEffect && activeEffect !== "none" && activeEffect in effectLookupAll) {
                        data.effectType = effectLookupAll[activeEffect];
                        newState.effect = activeEffect;
                    }
                }
                // When re-sending an effect (via effect_color or "update" mode),
                // the effect resets brightness on activation. To preserve the user's
                // brightness, we send it as a separate command AFTER the effect.
                // Extract brightness now and send it after the main payload.
                let deferredBrightness;
                if (data.effectType !== undefined && message.effect === undefined) {
                    if (data.brightness !== undefined) {
                        // User explicitly sent brightness alongside color — defer it
                        deferredBrightness = data.brightness;
                        delete data.brightness;
                        // Keep newState.brightness so state updates correctly
                    }
                }
                const encodedPayload = Buffer.from(EncodeManuSpecificPhilips2(data));
                // An empty Philips2Data encodes as just 2 zero bytes (the flags header).
                // Check length rather than all-zeros, since a valid payload could
                // legitimately contain zero-valued fields.
                if (encodedPayload.length <= 2) {
                    logger_1.logger.debug("No Philips2 fields to send, falling back to standard converters", NS);
                    // Delegate to the standard converter that handles this key.
                    // Z2M calls convertSet once per key, so exactly one converter matches.
                    for (const tz of toZigbee) {
                        if (tz.key.includes(key)) {
                            return await tz.convertSet(entity, key, value, meta);
                        }
                    }
                }
                else {
                    logger_1.logger.debug(`Sending manuSpecificPhilips2 payload: ${encodedPayload.toString("hex")}`, NS);
                    const payload = { data: encodedPayload };
                    await entity.command("manuSpecificPhilips2", "multiColor", payload);
                    // Send brightness as a separate command after effect activation,
                    // since the effect resets brightness on start.
                    if (deferredBrightness !== undefined) {
                        const brightnessData = { brightness: deferredBrightness };
                        const brightnessPayload = Buffer.from(EncodeManuSpecificPhilips2(brightnessData));
                        await entity.command("manuSpecificPhilips2", "multiColor", {
                            data: brightnessPayload,
                        });
                        newState.brightness = deferredBrightness;
                    }
                    // When an effect is active or being set, read state after a delay
                    // to sync brightness (effects modulate it internally).
                    if (data.effectType !== undefined) {
                        setTimeout(async () => {
                            try {
                                await entity.read("manuSpecificPhilips2", ["state"]);
                            }
                            catch (_e) {
                                // Best-effort sync
                            }
                        }, 1000).unref();
                    }
                    // Merge syncColorState results into newState. syncColorState
                    // returns only color-related keys (color, color_mode, color_temp),
                    // so we spread it on top of newState to keep state, brightness,
                    // effect, etc. intact.
                    const colorState = libColor.syncColorState(newState, meta.state, entity, meta.options);
                    return { state: { ...newState, ...colorState } };
                }
            },
            convertGet: async (entity, key, meta) => {
                let hasPhilips2Cluster = false;
                if (utils.isEndpoint(entity)) {
                    try {
                        hasPhilips2Cluster = entity.supportsInputCluster("manuSpecificPhilips2");
                    }
                    catch {
                        hasPhilips2Cluster = false;
                    }
                }
                if (utils.isEndpoint(entity) && !hasPhilips2Cluster) {
                    for (const tz of toZigbee) {
                        if (tz.key.includes(key) && tz.convertGet) {
                            return await tz.convertGet(entity, key, meta);
                        }
                    }
                    return;
                }
                try {
                    await entity.read("manuSpecificPhilips2", ["state"]);
                }
                catch (e) {
                    logger_1.logger.debug(`Reading manuSpecificPhilips2 state failed: ${e}`, NS);
                }
            },
            options: [
                new exposes.Binary("hue_native_control", ea.SET, true, false).withDescription("Control this light using a Philips-specific protocol instead of standard Zigbee commands. " +
                    "When enabled, on/off, brightness, color, and color temperature are combined into single atomic commands. " +
                    "This is required to use the Effect color update mode. " +
                    "When disabled (default), standard Zigbee commands are used, which preserves the usual behavior, " +
                    "including simulating on/off transitions."),
                new exposes.Enum("effect_color_mode", ea.SET, ["stop", "update"]).withDescription("Controls what happens when color is changed while an effect is active (requires Hue native control). " +
                    "'stop' (default): color change stops the effect (Hue app behavior). " +
                    "'update': color change re-sends the effect with the new color."),
            ],
        };
        // philipsLightTz claims all standard light keys. Inside convertSet, it delegates
        // back to the original standard converters by default (opt-out), or sends via
        // manuSpecificPhilips2 when the user enables the hue_native_control option.
        // The original standard converters are captured in the `toZigbee` closure above.
        // Standard converters for keys we DON'T claim. For converters that handle
        // both claimed and unclaimed keys (e.g. light_onoff_brightness handles "state"
        // which we claim AND "on_time" which we don't), we create wrappers with only
        // the unclaimed keys so Z2M doesn't double-dispatch.
        const unclaimed = [];
        for (const tz of toZigbee) {
            const unclaimedKeys = tz.key.filter((k) => !keys.includes(k));
            if (unclaimedKeys.length === 0)
                continue; // We claim all keys of this converter
            if (unclaimedKeys.length === tz.key.length) {
                // No overlap — include as-is
                unclaimed.push(tz);
            }
            else {
                // Partial overlap — wrap with only unclaimed keys
                unclaimed.push({ ...tz, key: unclaimedKeys });
            }
        }
        result.toZigbee = [philipsLightTz, philipsTz.hue_power_on_behavior, philipsTz.hue_power_on_error, ...unclaimed];
        if (args.hueEffect || args.gradient) {
            result.toZigbee.push(philipsTz.effect);
            const effects = ["blink", "breathe", "okay", "channel_change", "candle"];
            if (args.color)
                effects.push("fireplace", "colorloop");
            if (args.gradient) {
                result.toZigbee.push(philipsTz.gradient_scene, philipsTz.gradient({ reverse: true }));
                if (args.gradient !== true) {
                    effects.push(...args.gradient.extraEffects);
                }
                result.exposes.push(
                // gradient_scene is deprecated, use gradient instead
                ...(0, utils_1.exposeEndpoints)(e.enum("gradient_scene", ea.SET, Object.keys(exports.gradientScenes)), args.endpointNames), ...(0, utils_1.exposeEndpoints)(e
                    .list("gradient", ea.ALL, e.text("hex", ea.ALL).withDescription("Color in RGB HEX format (eg #663399)"))
                    .withLengthMin(1)
                    .withLengthMax(9)
                    .withDescription("List of RGB HEX colors"), args.endpointNames));
            }
            // Register the Fz converter for all devices (if user enables state reports later)
            result.fromZigbee.push(exports.manuSpecificPhilips2Fz);
            // Don't bind or configure reporting automatically - causes unwanted effects
            // (reports in-between states, conflicting with optimistic states)
            // https://github.com/Koenkk/zigbee2mqtt/issues/32050#issuecomment-4496461658
            // All Hue-specific effects per Bifrost spec
            effects.push("sunset", "sunrise", "sparkle", "opal", "glisten", "underwater", "cosmos", "sunbeam", "enchant");
            effects.push("none", "finish_effect", "stop_effect", "stop_hue_effect");
            result.exposes.push(...(0, utils_1.exposeEndpoints)(e.enum("effect", ea.STATE_SET, effects), args.endpointNames));
            // Expose effect_speed as a numeric 0..1 (0=slowest, 1=fastest)
            result.exposes.push(...(0, utils_1.exposeEndpoints)(new eNumeric("effect_speed", ea.STATE_SET)
                .withValueMin(0)
                .withValueMax(1)
                .withValueStep(0.01)
                .withDescription("Animation speed for the active effect (0=slowest, 1=fastest)"), args.endpointNames));
            // Expose effect_color: sets the base color of the active effect
            // without stopping it. Accepts same formats as color (hex, xy, hs).
            result.exposes.push(...(0, utils_1.exposeEndpoints)(e
                .text("effect_color", ea.SET)
                .withDescription('Set the base color of the active effect without stopping it (hex e.g. #FF4400, or JSON {"x":0.6,"y":0.3})'), args.endpointNames));
            if (args.gradient) {
                // Expose gradient style as an enum (per Bifrost spec: Linear, Scattered, Mirrored)
                result.exposes.push(...(0, utils_1.exposeEndpoints)(e
                    .enum("gradient_style", ea.ALL, ["linear", "scattered", "mirrored"])
                    .withDescription("Gradient rendering style: linear (smooth blend), scattered (color per segment), mirrored (symmetric from center)"), args.endpointNames));
                // Expose gradient scale and offset as numerics (fixed-point 5.3 format)
                result.exposes.push(...(0, utils_1.exposeEndpoints)(new eNumeric("gradient_scale", ea.SET)
                    .withValueMin(0)
                    .withValueMax(31)
                    .withValueStep(0.125)
                    .withDescription("Gradient scale (0=auto fit, 1.0+=number of colors visible)"), args.endpointNames), ...(0, utils_1.exposeEndpoints)(new eNumeric("gradient_offset", ea.SET)
                    .withValueMin(0)
                    .withValueMax(31)
                    .withValueStep(0.125)
                    .withDescription("Gradient color offset (0=start from first color)"), args.endpointNames));
            }
        }
        const customCluster2 = philipsModernExtend.addManuSpecificPhilips2Cluster();
        const customCluster3 = philipsModernExtend.addManuSpecificPhilips3Cluster();
        result.onEvent = [...customCluster2.onEvent, ...customCluster3.onEvent, ...(result.onEvent ?? [])];
        result.configure = [...customCluster2.configure, ...customCluster3.configure, ...(result.configure ?? [])];
        return result;
    },
    onOff: (args) => {
        args = { ota: true, ...args };
        const result = modernExtend.onOff(args);
        const customCluster2 = philipsModernExtend.addManuSpecificPhilips2Cluster();
        result.onEvent = [...customCluster2.onEvent, ...(result.onEvent ?? [])];
        result.configure = [...customCluster2.configure, ...(result.configure ?? [])];
        return result;
    },
    twilightOnOff: () => {
        const fromZigbee = [fz.ignore_command_on, fz.ignore_command_off, philipsFz.hue_twilight];
        const exposes = [
            e.action([
                "dot_press",
                "dot_hold",
                "dot_press_release",
                "dot_hold_release",
                "hue_press",
                "hue_hold",
                "hue_press_release",
                "hue_hold_release",
            ]),
        ];
        const toZigbee = [];
        const configure = [
            async (device, coordinatorEndpoint) => {
                const endpoint = device.getEndpoint(1);
                await reporting.bind(endpoint, coordinatorEndpoint, ["genOnOff", "manuSpecificPhilips"]);
            },
        ];
        const result = { exposes, fromZigbee, toZigbee, configure, isModernExtend: true };
        return result;
    },
    contact: () => {
        const exposes = [
            e.contact().withAccess(ea.STATE_GET),
            e.tamper().withAccess(ea.STATE_GET),
            new eNumeric("contact_last_changed", ea.STATE_GET)
                .withUnit("s")
                .withDescription("Time (in seconds) since when contact was last changed."),
            new eNumeric("tamper_last_changed", ea.STATE_GET).withUnit("s").withDescription("Time (in seconds) since when tamper was last changed."),
        ];
        const fromZigbee = [
            {
                cluster: "manuSpecificPhilipsContact",
                type: ["attributeReport", "readResponse"],
                convert: (model, msg, publish, options, meta) => {
                    const payload = {};
                    if (msg.data.contact !== undefined) {
                        // NOTE: 0 = closed, 1 = open
                        payload.contact = msg.data.contact === 0;
                    }
                    if (msg.data.tamper !== undefined) {
                        // NOTE: 0 = OK, 1 = tampered
                        payload.tamper = msg.data.tamper > 0;
                    }
                    if (msg.data.contactLastChange !== undefined) {
                        // NOTE: seems to be 1/10 of a second
                        payload.contact_last_changed = Math.round(msg.data.contactLastChange / 10);
                    }
                    if (msg.data.tamperLastChange !== undefined) {
                        // NOTE: seems to be 1/10 of a second
                        payload.tamper_last_changed = Math.round(msg.data.tamperLastChange / 10);
                    }
                    return payload;
                },
            },
            // NOTE: kept for compatibility as there is no auto-reconfigure for modernExtend
            //       this should not fire once reconfigured.
            {
                cluster: "genOnOff",
                type: ["commandOff", "commandOn"],
                convert: (model, msg, publish, options, meta) => {
                    if (msg.type === "commandOff" || msg.type === "commandOn") {
                        return { contact: msg.type === "commandOff" };
                    }
                },
            },
        ];
        const toZigbee = [
            {
                key: ["contact", "tamper", "contact_last_changed", "tamper_last_changed"],
                convertGet: async (entity, key, meta) => {
                    let attrib = key;
                    switch (key) {
                        case "contact_last_changed":
                            attrib = "contactLastChange";
                            break;
                        case "tamper_last_changed":
                            attrib = "tamperLastChange";
                            break;
                    }
                    const ep = (0, utils_1.determineEndpoint)(entity, meta, "manuSpecificPhilipsContact");
                    try {
                        await ep.read("manuSpecificPhilipsContact", [attrib]);
                    }
                    catch (e) {
                        logger_1.logger.debug(`Reading ${attrib} failed: ${e}, device probably doesn't support it`, "zhc:setupattribute");
                    }
                },
            },
        ];
        const configure = [
            // NOTE: trigger report after 4 hours incase the network was offline when a contact was triggered
            //       contactLastChange and tamperLastChange seem come with every report of contact, so we do
            //       not configure reporting
            modernExtend.setupConfigureForReporting("manuSpecificPhilipsContact", "contact", {
                config: { min: 0, max: "4_HOURS", change: 1 },
                access: ea.STATE_GET,
                singleEndpoint: true,
            }),
            modernExtend.setupConfigureForReporting("manuSpecificPhilipsContact", "tamper", {
                config: { min: 0, max: "4_HOURS", change: 1 },
                access: ea.STATE_GET,
                singleEndpoint: true,
            }),
            async (device, coordinatorEndpoint) => {
                // NOTE: new fromZigbee does not use genOnoff's commandOn/commandOff
                //       so we can unbind genOnOff so the legacy fromZigbee does not
                //       cause double triggers.
                const endpoint = device.getEndpoint(2);
                await endpoint.unbind("genOnOff", coordinatorEndpoint);
            },
        ];
        const result = { exposes, fromZigbee, toZigbee, configure, isModernExtend: true };
        return result;
    },
};
exports.m = philipsModernExtend;
const philipsTz = {
    gradient_scene: {
        key: ["gradient_scene"],
        convertSet: async (entity, key, value, meta) => {
            const scene = utils.getFromLookup(value, exports.gradientScenes);
            if (!scene)
                throw new Error(`Gradient scene '${value}' is unknown`);
            const payload = { data: Buffer.from(scene, "hex") };
            await entity.command("manuSpecificPhilips2", "multiColor", payload);
        },
    },
    gradient: (opts = { reverse: false }) => {
        return {
            key: ["gradient", "gradient_style"],
            convertSet: async (entity, key, value, meta) => {
                // Merge gradient_style from the message into opts if present
                const mergedOpts = { ...opts };
                const { message } = meta;
                if (message.gradient_style != null) {
                    const styleLookup = {
                        linear: HueGradientStyle.Linear,
                        scattered: HueGradientStyle.Scattered,
                        mirrored: HueGradientStyle.Mirrored,
                    };
                    const style = styleLookup[String(message.gradient_style).toLowerCase()];
                    if (style !== undefined) {
                        mergedOpts.style = style;
                    }
                }
                // If only gradient_style was sent (no gradient colors), re-send current
                // gradient from state with the new style
                let colors = key === "gradient" ? value : message.gradient;
                if (colors == null && meta.state?.gradient != null) {
                    colors = meta.state.gradient;
                }
                if (colors == null || (Array.isArray(colors) && colors.length === 0)) {
                    return; // Nothing to send
                }
                // @ts-expect-error ignore
                const scene = encodeGradientColors(colors, mergedOpts);
                const payload = { data: Buffer.from(scene, "hex") };
                await entity.command("manuSpecificPhilips2", "multiColor", payload);
                return { state: { gradient_style: message.gradient_style } };
            },
            convertGet: async (entity, key, meta) => {
                try {
                    await entity.read("manuSpecificPhilips2", ["state"]);
                }
                catch (e) {
                    logger_1.logger.debug(`Reading manuSpecificPhilips2 state for gradient failed: ${e}`, NS);
                }
            },
        };
    },
    effect: {
        key: ["effect"],
        convertSet: async (entity, key, value, meta) => {
            utils.assertString(value, "effect");
            const lower = value.toLowerCase();
            // Stop commands — handle before the generic hueEffects branch,
            // since stop_hue_effect is in the hueEffects map but not in effectLookupAll.
            // All three stop variants also send the Hue stop command so they work
            // regardless of whether the active effect is a ZCL or Hue effect.
            if (lower === "none" || lower === "stop_hue_effect" || lower === "finish_effect" || lower === "stop_effect") {
                // Stop Hue-specific effects via manuSpecificPhilips2
                await entity.command("manuSpecificPhilips2", "multiColor", {
                    data: Buffer.from(exports.hueEffects.stop_hue_effect, "hex"),
                });
                // Also send the ZCL effect stop for standard effects (blink, breathe, etc.)
                if (lower === "finish_effect" || lower === "stop_effect") {
                    try {
                        await tz.effect.convertSet(entity, key, value, meta);
                    }
                    catch (_e) {
                        // Ignore — device may not support ZCL identify cluster
                    }
                }
                return { state: { effect: "none" } };
            }
            if (lower in effectLookupAll) {
                // Build payload dynamically so we can include optional color
                const data = {
                    onOff: true,
                    effectType: effectLookupAll[lower],
                };
                // If color is provided alongside effect, include it in the payload
                const msg = meta.message;
                if (msg.color !== undefined) {
                    const newColor = libColor.Color.fromConverterArg(msg.color);
                    if (newColor.isHSV()) {
                        data.colorXY = newColor.hsv.toRGB().gammaCorrected().toXY().rounded(4);
                    }
                    else {
                        data.colorXY = newColor.isRGB() ? newColor.rgb.gammaCorrected().toXY().rounded(4) : newColor.xy;
                    }
                }
                // Include effect_speed if provided alongside effect
                if (msg.effect_speed !== undefined) {
                    data.effectSpeed = clamp(Number(msg.effect_speed), 0, 1);
                }
                const payload = { data: Buffer.from(EncodeManuSpecificPhilips2(data)) };
                await entity.command("manuSpecificPhilips2", "multiColor", payload);
                const state = { effect: lower };
                if (data.effectSpeed !== undefined)
                    state.effect_speed = data.effectSpeed;
                // Effects modulate brightness internally (e.g. candle dims to 30-60%).
                // Read state after a short delay so the Fz converter picks up the
                // actual brightness the device settled on.
                setTimeout(async () => {
                    try {
                        await entity.read("manuSpecificPhilips2", ["state"]);
                    }
                    catch (_e) {
                        // Ignore read failures — best-effort sync
                    }
                }, 1000).unref();
                return { state };
            }
            // Standard ZCL effects (blink, breathe, okay, channel_change)
            return await tz.effect.convertSet(entity, key, value, meta);
        },
    },
    hue_power_on_behavior: {
        key: ["hue_power_on_behavior"],
        convertSet: async (entity, key, value, meta) => {
            if (value === "default") {
                value = "on";
            }
            let supports = { colorTemperature: false, colorXY: false };
            if (utils.isEndpoint(entity) && entity.supportsInputCluster("lightingColorCtrl")) {
                const readResult = await entity.read("lightingColorCtrl", ["colorCapabilities"]);
                supports = {
                    colorTemperature: (readResult.colorCapabilities & (1 << 4)) > 0,
                    colorXY: (readResult.colorCapabilities & (1 << 3)) > 0,
                };
            }
            else if (entity.constructor.name === "Group") {
                supports = { colorTemperature: true, colorXY: true };
            }
            if (value === "off") {
                await entity.write("genOnOff", { 16387: { value: 0x00, type: 0x30 } });
            }
            else if (value === "recover") {
                await entity.write("genOnOff", { 16387: { value: 0xff, type: 0x30 } });
                await entity.write("genLevelCtrl", { 16384: { value: 0xff, type: 0x20 } });
                if (supports.colorTemperature) {
                    await entity.write("lightingColorCtrl", { 16400: { value: 0xffff, type: 0x21 } });
                }
                if (supports.colorXY) {
                    await entity.write("lightingColorCtrl", { 3: { value: 0xffff, type: 0x21 } }, manufacturerOptions);
                    await entity.write("lightingColorCtrl", { 4: { value: 0xffff, type: 0x21 } }, manufacturerOptions);
                }
            }
            else if (value === "on") {
                await entity.write("genOnOff", { 16387: { value: 0x01, type: 0x30 } });
                let brightness = meta.message.hue_power_on_brightness != null ? meta.message.hue_power_on_brightness : 0xfe;
                if (brightness === 255) {
                    // 255 (0xFF) is the value for recover, therefore set it to 254 (0xFE)
                    brightness = 254;
                }
                await entity.write("genLevelCtrl", { 16384: { value: brightness, type: 0x20 } });
                utils.assertEndpoint(entity);
                if (entity.supportsInputCluster("lightingColorCtrl")) {
                    if (meta.message.hue_power_on_color_temperature != null && meta.message.hue_power_on_color != null) {
                        logger_1.logger.error("Provide either color temperature or color, not both", NS);
                    }
                    else if (meta.message.hue_power_on_color_temperature != null) {
                        const colortemp = meta.message.hue_power_on_color_temperature;
                        await entity.write("lightingColorCtrl", { 16400: { value: colortemp, type: 0x21 } });
                        // Set color to default
                        if (supports.colorXY) {
                            await entity.write("lightingColorCtrl", { 3: { value: 0xffff, type: 0x21 } }, manufacturerOptions);
                            await entity.write("lightingColorCtrl", { 4: { value: 0xffff, type: 0x21 } }, manufacturerOptions);
                        }
                    }
                    else if (meta.message.hue_power_on_color != null) {
                        // @ts-expect-error ignore
                        const colorXY = libColor.ColorRGB.fromHex(meta.message.hue_power_on_color).toXY();
                        const xy = { x: utils.mapNumberRange(colorXY.x, 0, 1, 0, 65535), y: utils.mapNumberRange(colorXY.y, 0, 1, 0, 65535) };
                        value = xy;
                        // Set colortemp to default
                        if (supports.colorTemperature) {
                            await entity.write("lightingColorCtrl", { 16400: { value: 366, type: 0x21 } });
                        }
                        await entity.write("lightingColorCtrl", { 3: { value: xy.x, type: 0x21 } }, manufacturerOptions);
                        await entity.write("lightingColorCtrl", { 4: { value: xy.y, type: 0x21 } }, manufacturerOptions);
                    }
                    else {
                        // Set defaults for colortemp and color
                        if (supports.colorTemperature) {
                            await entity.write("lightingColorCtrl", { 16400: { value: 366, type: 0x21 } });
                        }
                        if (supports.colorXY) {
                            await entity.write("lightingColorCtrl", { 3: { value: 0xffff, type: 0x21 } }, manufacturerOptions);
                            await entity.write("lightingColorCtrl", { 4: { value: 0xffff, type: 0x21 } }, manufacturerOptions);
                        }
                    }
                }
            }
            return { state: { hue_power_on_behavior: value } };
        },
    },
    hue_power_on_error: {
        key: ["hue_power_on_brightness", "hue_power_on_color_temperature", "hue_power_on_color"],
        convertSet: (entity, key, value, meta) => {
            if (meta.message.hue_power_on_behavior === undefined) {
                throw new Error(`Provide a value for 'hue_power_on_behavior'`);
            }
        },
    },
    hue_motion_sensitivity: {
        // motion detect sensitivity, philips specific
        key: ["motion_sensitivity"],
        convertSet: async (entity, key, value, meta) => {
            // make sure you write to second endpoint!
            const lookup = { low: 0, medium: 1, high: 2, very_high: 3, max: 4 };
            const payload = { 48: { value: utils.getFromLookup(value, lookup), type: 32 } };
            await entity.write("msOccupancySensing", payload, manufacturerOptions);
            return { state: { motion_sensitivity: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read("msOccupancySensing", ["motionSensitivity"], manufacturerOptions);
        },
    },
    hue_motion_led_indication: {
        key: ["led_indication"],
        convertSet: async (entity, key, value, meta) => {
            const payload = { 51: { value, type: 0x10 } };
            await entity.write("genBasic", payload, manufacturerOptions);
            return { state: { led_indication: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read("genBasic", ["ledIndication"], manufacturerOptions);
        },
    },
    hue_wall_switch_device_mode: {
        key: ["device_mode"],
        convertSet: async (entity, key, value, meta) => {
            utils.assertString(value);
            const values = ["single_rocker", "single_push_button", "dual_rocker", "dual_push_button"];
            utils.validateValue(value, values);
            await entity.write("genBasic", { 52: { value: values.indexOf(value), type: 48 } }, manufacturerOptions);
        },
        convertGet: async (entity, key, meta) => {
            await entity.read("genBasic", ["deviceMode"], manufacturerOptions);
        },
    },
};
exports.tz = philipsTz;
const manufacturerOptions = { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V };
exports.gradientScenes = {
    blossom: "50010400135000000039d553d2955ba5287a9f697e25fb802800",
    crocus: "50010400135000000050389322f97f2b597343764cc664282800",
    precious: "5001040013500000007fa8838bb9789a786d7577499a773f2800",
    narcissa: "500104001350000000b0498a5c0a888fea89eb0b7ee15c742800",
    beginnings: "500104001350000000b3474def153e2ad42e98232c7483292800",
    first_light: "500104001350000000b28b7900e959d3f648a614389723362800",
    horizon: "500104001350000000488b7d6cbb750c6642f1133cc4033c2800",
    valley_dawn: "500104001350000000c1aa7de03a7a8ce861c7c4410d94412800",
    sunflare: "500104001350000000d0aa7d787a7daf197590154d6c14472800",
    emerald_flutter: "5001040013500000006a933977e34bb0d35e916468f246792800",
    memento: "500104001350000000f87318a3e31962331ec3532cceea892800",
    resplendent: "500104001350000000278b6d257a58efe84204273a35f5252800",
    scarlet_dream: "500104001350000000b02c654e4c5b45ab51fb0950d6c84d2800",
    lovebirds: "50010400135000000053ab84ea1a7e35fb7c098c73994c772800",
    smitten: "500104001350000000fe7b70a74b6aa42b65811b60550a592800",
    glitz_and_glam: "500104001350000000cc193cb9b845bad9521d1c77bf6c712800",
    promise: "500104001350000000258b606eca6b28d6382db445df26812800",
    ruby_romance: "5001040013500000000edb63cbcb6bac0c670b2d58204e572800",
    city_of_love: "50010400135000000055830e5cf31b6aa339d2ec70908b802800",
    honolulu: "500104001350000000dbfd59866c6378ec6c45cc765c0a822800",
    savanna_sunset: "50010400135000000005ae65c38c6c6b4b7573ca820fc9832800",
    golden_pond: "5001040013500000007e4a88cc4a8605db8728ec7b666c792800",
    runy_glow: "50010400135000000095bb53ac2a56eb99591e095c54985e2800",
    tropical_twilight: "500104001350000000408523a0b636e777524c0a71a76c6e2800",
    miami: "50010400135000000022ec61e6d94902d83766c3305a43182800",
    cancun: "500104001350000000a7eb54673d55944e6265fd6e26bb842800",
    rio: "500104001350000000a26526088c51a74b58ea6b7137ba892800",
    chinatown: "500104001350000000b33e5b408e59d90d5b4c6c6360ac792800",
    ibiza: "500104001350000000014d6d708c73827b7b6c7a8887f98a2800",
    osaka: "500104001350000000d649510b5c4deb7c5d8b6d6d2b9b802800",
    tokyo: "500104001350000000d1c311665331d3451fd59c4e394c7b2800",
    motown: "50010400135000000055730e5db3156623306c533d7a235c2800",
    fairfax: "50010400135000000072d34a3664477d7a61581d5fc08e5b2800",
    galaxy: "500104001350000000a6cb638b2a4f8cfa549bb9549ff73a2800",
    starlight: "5001040013500000008d897134a9653ec854d2963ed1d4282800",
    "blood moon": "500104001350000000202a6987c8599ee647ec632779c3142800",
    artic_aurora: "50010400135000000082548922057511046571c32d5b93192800",
    moonlight: "50010400135000000055730e5e9320c1832e96243ebec7652800",
    nebula: "50010400135000000026c852e106460d653ee745342964142800",
    sundown: "500104001350000000f37c68157c6d8efa755ac5512e24332800",
    blue_lagoon: "50010400135000000088c3623975699ea672a0c8831ada6d2800",
    palm_beach: "5001040013500000005ec4679ba56077f85a80ea64639c6a2800",
    lake_placid: "5001040013500000002eab69239a692d996552c54c39743a2800",
    mountain_breeze: "500104001350000000df843d2355419195465a98674ca97b2800",
    lake_mist: "500104001350000000e3286f39b96859f86266e54ded943f2800",
    ocean_dawn: "5001040013500000005cf9779da97105b96b07485e32564a2800",
    frosty_dawn: "5001040013500000006d6883bca87e3029758ec9722d6a722800",
    sunday_morning: "5001040013500000002c586dc6f87345997c63f983f777892800",
    emerald_isle: "500104001350000000e535628dc57ed2667d8b687d1e2a812800",
    spring_blossom: "500104001350000000a8b75fd0c75826b851a7094d305b652800",
    midsummer_sun: "500104001350000000002984799984dd29848eba836c0b7f2800",
    autumn_gold: "500104001350000000435a7817aa7ba3f979a8a981f3c9852800",
    spring_lake: "5001040013500000004a976d3347736e677561b77a4b07812800",
    winter_mountain: "5001040013500000002c555c68c55d7c555ef165606136622800",
    midwinter: "500104001350000000bda5532c554dbd254cd5a4428d94392800",
    amber_bloom: "500104001350000000739d67f2bc7372ec78a0ab78be8a6f2800",
    lily: "5001040013500000009cfc76c5ab793d4a6a1a9b586b9c522800",
    painted_sky: "500104001350000000d1c424c3d63783384c3f7a6a83bd6d2800",
    winter_beauty: "500104001350000000e2335ea7b4942467952db986a7ab7b2800",
    orange_fields: "500104001350000000409c69694c79eafa88498a8fb867aa2800",
    forest_adventure: "50010400135000000023999bbd76b363d4b674d3415fb3222800",
    blue_planet: "50010400135000000037a7a3a403b489737b2b746e6873362800",
    soho: "500104001350000000c52c4e220b6eed8a53d404192b04782800",
    vapor_wave: "500104001350000000e1c32401251acb183ac31b8051ea842800",
    magneto: "50010400135000000077b3286d9340b9e3662d99943c9b852800",
    tyrell: "500104001350000000ef4419a898370ea84698353574434e2800",
    disturbia: "50010400135000000084f371a4845e6998388c3b4f57ce582800",
    hal: "50010400135000000075f351a6244cf6dc5d480c658cda862800",
    golden_star: "5001040013500000007a4a8702eb8372ac7892cd61d51e5c2800",
    under_the_tree: "5001040013500000001de498b9a3cc0c9b8563bb6cc1ae5d2800",
    silent_night: "5001040013500000009e296a245a6f660a75086b70953b6e2800",
    rosy_sparkle: "500104001350000000810967c63a6cb2aa5ea7094eddd73c2800",
    festive_fun: "5001040013500000005a9318de53123e9414fdcc67839d612800",
    colour_burst: "500104001350000000f2731ff0c6266a6c64246e57d4f98f2800",
    crystalline: "5001040013500000006ea96a92a85e58074e18543d9cf3332800",
};
exports.hueEffects = {
    candle: "21000101",
    fireplace: "21000102",
    colorloop: "21000103",
    sunrise: "21000109",
    sparkle: "2100010a",
    opal: "2100010b",
    glisten: "2100010c",
    sunset: "2100010d",
    underwater: "2100010e",
    cosmos: "2100010f",
    sunbeam: "21000110",
    enchant: "21000111",
    stop_hue_effect: "200000",
};
const philipsFz = {
    hue_tap_dial: {
        cluster: "manuSpecificPhilips",
        type: "commandHueNotification",
        options: [exposes.options.simulated_brightness()],
        convert: (model, msg, publish, options, meta) => {
            if (utils.hasAlreadyProcessedMessage(msg, model))
                return;
            const buttonLookup = { 1: "button_1", 2: "button_2", 3: "button_3", 4: "button_4", 20: "dial" };
            const button = buttonLookup[msg.data.button];
            const direction = msg.data.unknown3 < 127 ? "right" : "left";
            const time = msg.data.time;
            const payload = {};
            if (button === "dial") {
                const adjustedTime = direction === "right" ? time : 256 - time;
                const dialType = "rotate";
                const speed = adjustedTime <= 25 ? "step" : adjustedTime <= 75 ? "slow" : "fast";
                payload.action = `${button}_${dialType}_${direction}_${speed}`;
                // extra raw info about dial turning
                const typeLookup = { 1: "step", 2: "rotate" };
                const type = typeLookup[msg.data.type];
                payload.action_time = adjustedTime;
                payload.action_direction = direction;
                payload.action_type = type;
                // simulated brightness
                if (options.simulated_brightness) {
                    const opts = options.simulated_brightness;
                    // @ts-expect-error ignore
                    const deltaOpts = typeof opts === "object" && opts.delta != null ? opts.delta : 35;
                    const delta = direction === "right" ? deltaOpts : deltaOpts * -1;
                    const brightness = globalStore.getValue(msg.endpoint, "brightness", 255) + delta;
                    payload.brightness = utils.numberWithinRange(brightness, 0, 255);
                    globalStore.putValue(msg.endpoint, "brightness", payload.brightness);
                }
            }
            else {
                const typeLookup = { 0: "press", 1: "hold", 2: "press_release", 3: "hold_release" };
                const type = typeLookup[msg.data.type];
                payload.action = `${button}_${type}`;
                // duration
                if (type === "press")
                    globalStore.putValue(msg.endpoint, "press_start", Date.now());
                else if (type === "hold" || type === "hold_release") {
                    payload.action_duration = (Date.now() - globalStore.getValue(msg.endpoint, "press_start")) / 1000;
                }
            }
            return payload;
        },
    },
    gradient: {
        cluster: "manuSpecificPhilips2",
        type: ["attributeReport", "readResponse"],
        convert: (model, msg, publish, options, meta) => {
            if (msg.data.state !== undefined) {
                const input = msg.data.state.toString("hex");
                const decoded = decodeGradientColors(input, { reverse: true });
                if (decoded.color_mode === "gradient") {
                    return { gradient: decoded.colors };
                }
            }
            return {};
        },
    },
    hue_smart_button_event: {
        cluster: "manuSpecificPhilips",
        type: "commandHueNotification",
        convert: (model, msg, publish, options, meta) => {
            // Philips HUE Smart Button "ROM001": these events are always from "button 1"
            const lookup = { 0: "press", 1: "hold", 2: "release", 3: "release" };
            return { action: lookup[msg.data.type] };
        },
    },
    hue_wall_switch: {
        cluster: "manuSpecificPhilips",
        type: "commandHueNotification",
        convert: (model, msg, publish, options, meta) => {
            if ((0, utils_1.hasAlreadyProcessedMessage)(msg, model))
                return;
            const buttonLookup = { 1: "left", 2: "right" };
            const button = buttonLookup[msg.data.button];
            const typeLookup = { 0: "press", 1: "hold", 2: "press_release", 3: "hold_release" };
            const type = typeLookup[msg.data.type];
            return { action: `${button}_${type}` };
        },
    },
    hue_dimmer_switch: {
        cluster: "manuSpecificPhilips",
        type: "commandHueNotification",
        options: [exposes.options.simulated_brightness()],
        convert: (model, msg, publish, options, meta) => {
            if ((0, utils_1.hasAlreadyProcessedMessage)(msg, model))
                return;
            const buttonLookup = { 1: "on", 2: "up", 3: "down", 4: "off" };
            const button = buttonLookup[msg.data.button];
            const typeLookup = { 0: "press", 1: "hold", 2: "press_release", 3: "hold_release" };
            const type = typeLookup[msg.data.type];
            const payload = { action: `${button}_${type}` };
            // duration
            if (type === "press")
                globalStore.putValue(msg.endpoint, "press_start", Date.now());
            else if (type === "hold" || type === "release") {
                payload.action_duration = (Date.now() - globalStore.getValue(msg.endpoint, "press_start")) / 1000;
            }
            // simulated brightness
            if (options.simulated_brightness && (button === "down" || button === "up") && type !== "release") {
                const opts = options.simulated_brightness;
                const deltaOpts = typeof opts === "object" && opts.delta != null ? opts.delta : 35;
                const delta = button === "up" ? deltaOpts : deltaOpts * -1;
                const brightness = globalStore.getValue(msg.endpoint, "brightness", 255) + delta;
                payload.brightness = (0, utils_1.numberWithinRange)(brightness, 0, 255);
                payload.action_brightness_delta = delta;
                globalStore.putValue(msg.endpoint, "brightness", payload.brightness);
            }
            return payload;
        },
    },
    hue_twilight: {
        cluster: "manuSpecificPhilips",
        type: "commandHueNotification",
        convert: (model, msg, publish, options, meta) => {
            const buttonLookup = { 1: "dot", 2: "hue" };
            const button = buttonLookup[msg.data.button];
            const typeLookup = { 0: "press", 1: "hold", 2: "press_release", 3: "hold_release" };
            const type = typeLookup[msg.data.type];
            const payload = { action: `${button}_${type}` };
            // duration
            if (type === "press")
                globalStore.putValue(msg.endpoint, "press_start", Date.now());
            else if (type === "hold" || type === "release") {
                payload.action_duration = (Date.now() - globalStore.getValue(msg.endpoint, "press_start")) / 1000;
            }
            return payload;
        },
    },
    hue_motion_led_indication: {
        cluster: "genBasic",
        type: ["attributeReport", "readResponse"],
        convert: (model, msg, publish, options, meta) => {
            if (msg.data.ledIndication !== undefined) {
                return { led_indication: msg.data.ledIndication === true };
            }
        },
    },
    hue_motion_sensitivity: {
        cluster: "msOccupancySensing",
        type: ["attributeReport", "readResponse"],
        convert: (model, msg, publish, options, meta) => {
            if (msg.data.motionSensitivity !== undefined) {
                const lookup = ["low", "medium", "high", "very_high", "max"];
                return { motion_sensitivity: lookup[msg.data.motionSensitivity] };
            }
        },
    },
    hue_wall_switch_device_mode: {
        cluster: "genBasic",
        type: ["attributeReport", "readResponse"],
        convert: (model, msg, publish, options, meta) => {
            if (msg.data.deviceMode !== undefined) {
                const values = ["single_rocker", "single_push_button", "dual_rocker", "dual_push_button"];
                return { device_mode: values[msg.data.deviceMode] };
            }
        },
    },
    hue_tap: {
        cluster: "greenPower",
        type: ["commandNotification", "commandCommissioningNotification"],
        convert: (model, msg, publish, options, meta) => {
            const commandID = msg.data.commandID;
            if ((0, utils_1.hasAlreadyProcessedMessage)(msg, model, msg.data.frameCounter, `${msg.device.ieeeAddr}_${commandID}`))
                return;
            if (commandID >= 0xe0)
                return; // Skip op commands
            if (HUE_TAP_LOOKUP[commandID] === undefined) {
                logger_1.logger.error(`Hue Tap: missing command '${commandID}'`, NS);
            }
            else {
                return { action: HUE_TAP_LOOKUP[commandID] };
            }
        },
    },
};
exports.fz = philipsFz;
const clamp = (x, low, high) => Math.min(Math.max(low, x), high);
const scaleFloatToIntPow2 = (x, bits) => clamp(x, 0.0, 1.0) * (2 ** bits - 1);
const scaleIntPow2ToFloat = (x, bits) => clamp(x, 0, 2 ** bits - 1) / (2 ** bits - 1);
var HueEffectType;
(function (HueEffectType) {
    HueEffectType[HueEffectType["NoEffect"] = 0] = "NoEffect";
    HueEffectType[HueEffectType["Candle"] = 1] = "Candle";
    HueEffectType[HueEffectType["Fireplace"] = 2] = "Fireplace";
    HueEffectType[HueEffectType["Prism"] = 3] = "Prism";
    HueEffectType[HueEffectType["Sunrise"] = 9] = "Sunrise";
    HueEffectType[HueEffectType["Sparkle"] = 10] = "Sparkle";
    HueEffectType[HueEffectType["Opal"] = 11] = "Opal";
    HueEffectType[HueEffectType["Glisten"] = 12] = "Glisten";
    HueEffectType[HueEffectType["Sunset"] = 13] = "Sunset";
    HueEffectType[HueEffectType["Underwater"] = 14] = "Underwater";
    HueEffectType[HueEffectType["Cosmos"] = 15] = "Cosmos";
    HueEffectType[HueEffectType["Sunbeam"] = 16] = "Sunbeam";
    HueEffectType[HueEffectType["Enchant"] = 17] = "Enchant";
})(HueEffectType || (exports.HueEffectType = HueEffectType = {}));
var HueGradientStyle;
(function (HueGradientStyle) {
    HueGradientStyle[HueGradientStyle["Linear"] = 0] = "Linear";
    HueGradientStyle[HueGradientStyle["Scattered"] = 2] = "Scattered";
    HueGradientStyle[HueGradientStyle["Mirrored"] = 4] = "Mirrored";
})(HueGradientStyle || (exports.HueGradientStyle = HueGradientStyle = {}));
const gradientStyleNames = {
    [HueGradientStyle.Linear]: "linear",
    [HueGradientStyle.Scattered]: "scattered",
    [HueGradientStyle.Mirrored]: "mirrored",
};
// Mapping from effect names to HueEffectType enum values.
// Used by both the effect Tz converter and the philipsLightTz convertSet.
const effectLookupAll = {
    candle: HueEffectType.Candle,
    fireplace: HueEffectType.Fireplace,
    colorloop: HueEffectType.Prism,
    sunrise: HueEffectType.Sunrise,
    sparkle: HueEffectType.Sparkle,
    opal: HueEffectType.Opal,
    glisten: HueEffectType.Glisten,
    sunset: HueEffectType.Sunset,
    underwater: HueEffectType.Underwater,
    cosmos: HueEffectType.Cosmos,
    sunbeam: HueEffectType.Sunbeam,
    enchant: HueEffectType.Enchant,
};
// Inverse mapping: HueEffectType → name. Derived from effectLookupAll to stay in sync.
const effectNames = {
    [HueEffectType.NoEffect]: "none",
    ...Object.fromEntries(Object.entries(effectLookupAll).map(([name, type]) => [type, name])),
};
var HueFlags;
(function (HueFlags) {
    HueFlags[HueFlags["OnOff"] = 1] = "OnOff";
    HueFlags[HueFlags["Brightness"] = 2] = "Brightness";
    HueFlags[HueFlags["ColorMirek"] = 4] = "ColorMirek";
    HueFlags[HueFlags["ColorXY"] = 8] = "ColorXY";
    HueFlags[HueFlags["FadeSpeed"] = 16] = "FadeSpeed";
    HueFlags[HueFlags["EffectType"] = 32] = "EffectType";
    HueFlags[HueFlags["GradientParams"] = 64] = "GradientParams";
    HueFlags[HueFlags["EffectSpeed"] = 128] = "EffectSpeed";
    HueFlags[HueFlags["GradientColors"] = 256] = "GradientColors";
    // Reserved for future use per Bifrost spec
    HueFlags[HueFlags["Reserved9"] = 512] = "Reserved9";
    HueFlags[HueFlags["ReservedA"] = 1024] = "ReservedA";
    HueFlags[HueFlags["ReservedB"] = 2048] = "ReservedB";
    HueFlags[HueFlags["ReservedC"] = 4096] = "ReservedC";
    HueFlags[HueFlags["ReservedD"] = 8192] = "ReservedD";
    HueFlags[HueFlags["ReservedE"] = 16384] = "ReservedE";
    HueFlags[HueFlags["ReservedF"] = 32768] = "ReservedF";
})(HueFlags || (HueFlags = {}));
const ON_OFF_DETAILS = {
    name: "onOff",
    flag: HueFlags.OnOff,
    maxLength: 1,
    encode: (v, p, d) => {
        v.setUint8(p, d.onOff ? 1 : 0);
        return p + 1;
    },
    decode: (v, p, d) => {
        d.onOff = !!v.getUint8(p);
        return p + 1;
    },
};
const BRIGHTNESS_DETAILS = {
    name: "brightness",
    flag: HueFlags.Brightness,
    maxLength: 1,
    encode: (v, p, d) => {
        v.setUint8(p, clamp(d.brightness, 1, 254));
        return p + 1;
    },
    decode: (v, p, d) => {
        d.brightness = v.getUint8(p);
        return p + 1;
    },
};
const COLOR_MIREK_DETAILS = {
    name: "colorMirek",
    flag: HueFlags.ColorMirek,
    maxLength: 2,
    encode: (v, p, d) => {
        v.setUint16(p, d.colorMirek, true);
        return p + 2;
    },
    decode: (v, p, d) => {
        d.colorMirek = v.getUint16(p, true);
        return p + 2;
    },
};
const COLOR_XY_DETAILS = {
    name: "colorXY",
    flag: HueFlags.ColorXY,
    maxLength: 4,
    encode: (v, p, d) => {
        v.setUint16(p, scaleFloatToIntPow2(d.colorXY.x, 16), true);
        v.setUint16(p + 2, scaleFloatToIntPow2(d.colorXY.y, 16), true);
        return p + 4;
    },
    decode: (v, p, d) => {
        d.colorXY = color_1.ColorXY.fromObject({
            x: scaleIntPow2ToFloat(v.getUint16(p, true), 16),
            y: scaleIntPow2ToFloat(v.getUint16(p + 2, true), 16),
        });
        return p + 4;
    },
};
const FADE_SPEED_DETAILS = {
    name: "fadeSpeed",
    flag: HueFlags.FadeSpeed,
    maxLength: 2,
    encode: (v, p, d) => {
        v.setUint16(p, d.fadeSpeed, true);
        return p + 2;
    },
    decode: (v, p, d) => {
        d.fadeSpeed = v.getUint16(p, true);
        return p + 2;
    },
};
const EFFECT_TYPE_DETAILS = {
    name: "effectType",
    flag: HueFlags.EffectType,
    maxLength: 1,
    encode: (v, p, d) => {
        v.setUint8(p, d.effectType);
        return p + 1;
    },
    decode: (v, p, d) => {
        d.effectType = v.getUint8(p);
        return p + 1;
    },
};
const GRADIENT_PARAMS_DETAILS = {
    name: "gradientParams",
    flag: HueFlags.GradientParams,
    maxLength: 2,
    encode: (v, p, d) => {
        v.setUint8(p, Math.round(clamp(d.gradientParams.scale * 8, 0, 255)));
        v.setUint8(p + 1, Math.round(clamp(d.gradientParams.offset * 8, 0, 255)));
        return p + 2;
    },
    decode: (v, p, d) => {
        d.gradientParams = {
            scale: v.getUint8(p) / 8.0,
            offset: v.getUint8(p + 1) / 8.0,
        };
        return p + 2;
    },
};
const EFFECT_SPEED_DETAILS = {
    name: "effectSpeed",
    flag: HueFlags.EffectSpeed,
    maxLength: 1,
    encode: (v, p, d) => {
        v.setUint8(p, scaleFloatToIntPow2(d.effectSpeed, 8));
        return p + 1;
    },
    decode: (v, p, d) => {
        d.effectSpeed = scaleIntPow2ToFloat(v.getUint8(p), 8);
        return p + 1;
    },
};
// GRADIENT_COLORS_MAX_X and GRADIENT_COLORS_MAX_Y defined at top of file
const GRADIENT_COLORS_DETAILS = {
    name: "gradientColors",
    flag: HueFlags.GradientColors,
    // Max: 1 byte size + 4 bytes header + 9 colors * 3 bytes = 32 bytes
    maxLength: 5 + 9 * 3,
    encode: (v, p, d) => {
        // Bifrost spec: max 9 colors; 10+ causes device to reject entire message
        const colorCount = clamp(d.gradientColors.colors.length, 0, 9);
        // Size byte: 4 bytes header (count + style + 2 reserved) + 3 bytes per color
        v.setUint8(p, 4 + colorCount * 3);
        v.setUint8(p + 1, colorCount << 4);
        v.setUint8(p + 2, d.gradientColors.style);
        v.setUint16(p + 3, 0, true);
        d.gradientColors.colors.slice(0, colorCount).forEach(({ x, y }, i) => {
            const scaledX = scaleFloatToIntPow2(x / GRADIENT_COLORS_MAX_X, 12);
            const scaledY = scaleFloatToIntPow2(y / GRADIENT_COLORS_MAX_Y, 12);
            // Byte packing per Bifrost spec:
            //   byte 0: x[7:0]
            //   byte 1: y[3:0] << 4 | x[11:8]
            //   byte 2: y[11:4]
            v.setUint8(p + 5 + i * 3, scaledX & 0xff);
            v.setUint8(p + 6 + i * 3, ((scaledY & 0xf) << 4) | ((scaledX & 0xf00) >> 8));
            v.setUint8(p + 7 + i * 3, (scaledY >> 4) & 0xff);
        });
        // Header: 1 byte size + 4 bytes (count, style, 2 reserved) = 5 bytes + 3*N colors
        return p + 5 + 3 * colorCount;
    },
    decode: (v, p, d) => {
        const size = v.getUint8(p);
        const colorCount = v.getUint8(p + 1) >> 4;
        const rawStyle = v.getUint8(p + 2);
        // Validate gradient style per Bifrost spec: Linear=0x00, Scattered=0x02, Mirrored=0x04
        const validStyles = [HueGradientStyle.Linear, HueGradientStyle.Scattered, HueGradientStyle.Mirrored];
        const style = validStyles.includes(rawStyle) ? rawStyle : HueGradientStyle.Linear;
        const colors = Array.from({ length: colorCount }, (_, i) => {
            const a = v.getUint8(p + 5 + i * 3);
            const b = v.getUint8(p + 6 + i * 3);
            const c = v.getUint8(p + 7 + i * 3);
            return color_1.ColorXY.fromObject({
                x: scaleIntPow2ToFloat(((b & 0xf) << 8) | a, 12) * GRADIENT_COLORS_MAX_X,
                y: scaleIntPow2ToFloat((c << 4) | ((b & 0xf0) >> 4), 12) * GRADIENT_COLORS_MAX_Y,
            });
        });
        d.gradientColors = { style, colors };
        return p + size + 1;
    },
};
// Ordered as they appear in the wire format per Bifrost spec.
// IMPORTANT: the wire order does NOT match the flag bit order.
// Specifically, GRADIENT_COLORS (flag 0x0100) appears BEFORE
// EFFECT_SPEED (flag 0x0080) and GRADIENT_PARAMS (flag 0x0040)
// in the actual byte stream. Both encode and decode MUST iterate
// in this order, not in flag-bit order.
// Reference: https://github.com/chrivers/bifrost/blob/master/doc/hue-zigbee-format.md
const HUE_DATA_TYPES = [
    ON_OFF_DETAILS,
    BRIGHTNESS_DETAILS,
    COLOR_MIREK_DETAILS,
    COLOR_XY_DETAILS,
    FADE_SPEED_DETAILS,
    EFFECT_TYPE_DETAILS,
    GRADIENT_COLORS_DETAILS,
    EFFECT_SPEED_DETAILS,
    GRADIENT_PARAMS_DETAILS,
];
function DecodeManuSpecificPhilips2(data) {
    const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
    const flags = view.getUint16(0, true);
    const decoded = {};
    let position = 2;
    HUE_DATA_TYPES.forEach((htd) => {
        if (flags & htd.flag) {
            position = htd.decode(view, position, decoded);
        }
    });
    return decoded;
}
function EncodeManuSpecificPhilips2(data) {
    const buffer = new ArrayBuffer(2 + HUE_DATA_TYPES.map((htd) => htd.maxLength).reduce((pv, cv) => pv + cv, 0));
    const view = new DataView(buffer);
    let position = 2;
    let flags = 0;
    HUE_DATA_TYPES.forEach((htd) => {
        if (data[htd.name] !== undefined) {
            flags |= htd.flag;
            position = htd.encode(view, position, data);
        }
    });
    view.setUint16(0, flags, true);
    return buffer.slice(0, position);
}
// decoder for manuSpecificPhilips2.state
function decodeGradientColors(input, opts) {
    // Gradient mode (4b01)
    // Example: 4b010164fb74346b1350000000f3297fda7d55da7d55f3297fda7d552800
    // 4b01 - mode? (4) (0b00 single color?, 4b01 gradient?)
    //     01 - on/off (2)
    //       64 - brightness (2)
    //         fb74346b - unknown (8) - Might be XY Color?
    //                 13 - length (2)
    //                   50 - ncolors (2)
    //                     000000 - unknown (6)
    //                           f3297fda7d55da7d55f3297fda7d55 - colors (6 * ncolors)
    //                                                         28 - segments (2)
    //                                                           00 - offset (2)
    //
    // Temperature mode (0f00)
    // Example: 0f0000044d01ab6f7067
    // 0f00 - mode (4)
    //     01 - on/off (2)
    //       1a - brightness (2)
    //          4d01 - color temperature (4)
    //              ab6f7067 - unknown (8)
    //
    // XY Color mode (0b00)
    // Example: 0b00010460b09c4e
    // 0b00 - mode (4) == 0b00 single color mode
    //     01 - on/off (2)
    //       04 - brightness (2)
    //         60b09c4e - color (8) (xLow, xHigh, yLow, yHigh)
    //
    // Effect mode (ab00)
    // Example: ab000153df7e446a0180
    // ab00 - mode (4)
    //     01 - on/off (2)
    //       53 - brightness (2)
    //         df7e446a - XY Color (8)
    //                 0180 - effect (4)
    //
    // On/off/brightness mode (0003) – For devices that only support on/off and brightness
    // Example: 030001b2
    // 0300 - mode (4)
    //     01 - on/off (2)
    //       b2 - brightness (2)
    // Device color mode
    const mode = input.slice(0, 4);
    input = input.slice(4);
    // On/off (2 bytes)
    const on = Number.parseInt(input.slice(0, 2), 16) === 1;
    input = input.slice(2);
    // Brightness (2 bytes)
    const brightness = Number.parseInt(input.slice(0, 2), 16);
    input = input.slice(2);
    // Gradient mode
    if (mode === COLOR_MODE_GRADIENT) {
        // Unknown (8 bytes)
        input = input.slice(8);
        // Length (2 bytes)
        input = input.slice(2);
        // Number of colors (2 bytes)
        const nColors = Number.parseInt(input.slice(0, 2), 16) >> 4;
        input = input.slice(2);
        // Unknown (6 bytes)
        input = input.slice(6);
        // Colors (6 * nColors bytes)
        const colorsPayload = input.slice(0, 6 * nColors);
        input = input.slice(6 * nColors);
        const colors = colorsPayload.match(/.{6}/g).map(decodeScaledGradientToRGB);
        // Segments (2 bytes)
        const segments = Number.parseInt(input.slice(0, 2), 16) >> 3;
        input = input.slice(2);
        // Offset (2 bytes)
        const offset = Number.parseInt(input.slice(0, 2), 16) >> 3;
        if (opts?.reverse) {
            colors.reverse();
        }
        return {
            color_mode: "gradient",
            colors,
            segments,
            offset,
            brightness,
            on,
        };
    }
    if (mode === COLOR_MODE_COLOR_XY || mode === COLOR_MODE_EFFECT) {
        // XY Color mode
        const xLow = Number.parseInt(input.slice(0, 2), 16);
        input = input.slice(2);
        const xHigh = Number.parseInt(input.slice(0, 2), 16) << 8;
        input = input.slice(2);
        const yHigh = Number.parseInt(input.slice(0, 2), 16);
        input = input.slice(2);
        const yLow = Number.parseInt(input.slice(0, 2), 16) << 8;
        input = input.slice(2);
        const x = Math.round(((xHigh | xLow) / 65535) * 10000) / 10000;
        const y = Math.round(((yHigh | yLow) / 65535) * 10000) / 10000;
        if (mode === COLOR_MODE_COLOR_XY) {
            return {
                color_mode: "xy",
                x,
                y,
                brightness,
                on,
            };
        }
        // Effect mode
        const effect = input.slice(0, 4);
        // @ts-expect-error ignore
        const name = exports.knownEffects[effect] || `unknown_${effect}`;
        return {
            color_mode: "xy",
            x,
            y,
            brightness,
            on,
            name,
        };
    }
    if (mode === COLOR_MODE_COLOR_TEMP) {
        // Color temperature mode
        const low = Number.parseInt(input.slice(0, 2), 16);
        input = input.slice(2);
        const high = Number.parseInt(input.slice(0, 2), 16) << 8;
        input = input.slice(2);
        const temp = high | low;
        return {
            color_mode: "color_temp",
            color_temp: temp,
            brightness,
            on,
        };
    }
    if (mode === COLOR_MODE_BRIGHTNESS) {
        return {
            brightness,
            on,
        };
    }
    // Unknown mode
    return {};
}
// Value is a list of RGB HEX colors
function encodeGradientColors(value, opts) {
    if (value.length > 9) {
        throw new Error(`Expected up to 9 colors, got ${value.length}`);
    }
    if (value.length < 1) {
        throw new Error("Expected at least 1 color, got 0");
    }
    // For devices where it makes more sense to specify the colors in reverse
    // For example Hue Signe, where the last color is the top color.
    if (opts.reverse) {
        value.reverse();
    }
    // The number of colors and segments can technically differ. Here they are always the same, but we could
    // support it by extending the API.
    // If number of colors is less than the number of segments, the colors will repeat.
    // It seems like the maximum number of colors is 9, and the maximum number of segments is 31.
    const nColors = (value.length << 4).toString(16).padStart(2, "0");
    let segments = value.length;
    if (opts.segments) {
        segments = opts.segments;
    }
    if (segments < 1 || segments > 31) {
        throw new Error(`Expected segments to be between 1 and 31 (inclusive), got ${segments}`);
    }
    const segmentsPayload = (segments << 3).toString(16).padStart(2, "0");
    // Encode the colors
    const colorsPayload = value.map(encodeRGBToScaledGradient).join("");
    // Offset of the first color. 0 means the first segment uses the first color. (min 0, max 31)
    let offset = 0;
    if (opts.offset) {
        offset = opts.offset;
    }
    const offsetPayload = (offset << 3).toString(16).padStart(2, "0");
    // Payload length
    const length = (1 + 3 * (value.length + 1)).toString(16).padStart(2, "0");
    // Gradient style: Linear=0x00, Scattered=0x02, Mirrored=0x04
    // Per Bifrost spec, only these three values are valid.
    let style = 0x00; // Default: Linear
    if (opts.style != null) {
        style = opts.style;
    }
    const stylePayload = style.toString(16).padStart(2, "0");
    // 5001 - mode? set gradient?
    // 0400 - unknown
    const scene = `50010400${length}${nColors}${stylePayload}0000${colorsPayload}${segmentsPayload}${offsetPayload}`;
    return scene;
}
//# sourceMappingURL=philips.js.map