UNPKG

zigbee-herdsman-converters

Version:

Collection of device converters to be used with zigbee-herdsman

2,457 lines • 121 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.thermostat_temperature_setpoint_hold_duration = exports.thermostat_temperature_setpoint_hold = exports.thermostat_keypad_lockout = exports.thermostat_temperature_display_mode = exports.thermostat_programming_operation_mode = exports.thermostat_control_sequence_of_operation = exports.thermostat_system_mode = exports.thermostat_weekly_schedule = exports.thermostat_remote_sensing = exports.effect = exports.light_color_colortemp = exports.light_colortemp_startup = exports.light_onoff_brightness = exports.light_hue_saturation_move = exports.light_hue_saturation_step = exports.light_color_and_colortemp_via_color = exports.light_colortemp_move = exports.light_colortemp_step = exports.light_brightness_move = exports.light_brightness_step = exports.ballast_config = exports.level_config = exports.occupancy_timeout = exports.cover_mode = exports.cover_position_tilt = exports.cover_state = exports.squawk = exports.warning_simple = exports.warning = exports.cover_via_brightness = exports.lock_userstatus = exports.pincode_lock = exports.lock_sound_volume = exports.lock_auto_relock_time = exports.lock = exports.light_color_options = exports.light_color_mode = exports.power_on_behavior = exports.battery_voltage = exports.battery_percentage_remaining = exports.arm_mode = exports.zcl_command = exports.identify = exports.factory_reset = exports.command = exports.write = exports.read = exports.light_colortemp = exports.light_color = exports.on_off = void 0;
exports.power_source = exports.TYZB01_on_off = exports.humidity = exports.temperature = exports.dcpower = exports.dcvoltage = exports.dccurrent = exports.accurrent_neutral = exports.accurrent_phase_c = exports.accurrent_phase_b = exports.accurrent = exports.acvoltage_phase_c = exports.acvoltage_phase_b = exports.acvoltage = exports.powerfactor = exports.electrical_measurement_power_reactive = exports.frequency = exports.currentsummreceived = exports.currenttier4summdelivered = exports.currenttier3summdelivered = exports.currenttier2summdelivered = exports.currenttier1summdelivered = exports.currentsummdelivered = exports.metering_extended_status = exports.metering_status = exports.metering_power = exports.electrical_measurement_power_phase_c = exports.electrical_measurement_power_phase_b = exports.electrical_measurement_power = exports.thermostat_ac_louver_position = exports.thermostat_max_cool_setpoint_limit = exports.thermostat_min_cool_setpoint_limit = exports.thermostat_max_heat_setpoint_limit = exports.thermostat_min_heat_setpoint_limit = exports.thermostat_running_mode = exports.thermostat_relay_status_log = exports.thermostat_setpoint_raise_lower = exports.thermostat_unoccupied_cooling_setpoint = exports.thermostat_occupied_cooling_setpoint = exports.thermostat_unoccupied_heating_setpoint = exports.thermostat_occupied_heating_setpoint = exports.thermostat_running_state = exports.thermostat_pi_heating_demand = exports.thermostat_clear_weekly_schedule = exports.thermostat_occupancy = exports.thermostat_local_temperature_calibration = exports.thermostat_outdoor_temperature = exports.thermostat_local_temperature = exports.fan_speed = exports.fan_mode = void 0;
exports.ignore_rate = exports.ignore_transition = exports.scene_rename = exports.scene_remove_all = exports.scene_remove = exports.scene_add = exports.scene_recall = exports.scene_store = void 0;
const zigbee_herdsman_1 = require("zigbee-herdsman");
const libColor = __importStar(require("../lib/color"));
const constants = __importStar(require("../lib/constants"));
const exposes = __importStar(require("../lib/exposes"));
const legacy = __importStar(require("../lib/legacy"));
const light = __importStar(require("../lib/light"));
const logger_1 = require("../lib/logger");
const globalStore = __importStar(require("../lib/store"));
const utils = __importStar(require("../lib/utils"));
const utils_1 = require("../lib/utils");
const NS = "zhc:tz";
exports.on_off = {
    key: ["state", "on_time", "off_wait_time"],
    convertSet: async (entity, key, value, meta) => {
        const state = utils.isString(meta.message.state) ? meta.message.state.toLowerCase() : null;
        utils.validateValue(state, ["toggle", "off", "on"]);
        if (state === "on" && (meta.message.on_time != null || meta.message.off_wait_time != null)) {
            const onTime = meta.message.on_time != null ? meta.message.on_time : 0;
            const offWaitTime = meta.message.off_wait_time != null ? meta.message.off_wait_time : 0;
            if (typeof onTime !== "number") {
                throw new Error("The on_time value must be a number!");
            }
            if (typeof offWaitTime !== "number") {
                throw new Error("The off_wait_time value must be a number!");
            }
            const payload = meta.converterOptions
                ? // TODO: better typing? currently used in a single place??
                    meta.converterOptions
                : { ctrlbits: 0, ontime: Math.round(onTime * 10), offwaittime: Math.round(offWaitTime * 10) };
            await entity.command("genOnOff", "onWithTimedOff", payload, utils.getOptions(meta.mapped, entity));
        }
        else {
            await entity.command("genOnOff", state, {}, utils.getOptions(meta.mapped, entity));
            if (state === "toggle") {
                const currentState = meta.state[`state${meta.endpoint_name ? `_${meta.endpoint_name}` : ""}`];
                return currentState ? { state: { state: currentState === "OFF" ? "ON" : "OFF" } } : {};
            }
            return { state: { state: state.toUpperCase() } };
        }
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("genOnOff", ["onOff"]);
    },
};
exports.light_color = {
    key: ["color"],
    options: [exposes.options.color_sync(), exposes.options.transition()],
    convertSet: async (entity, key, value, meta) => {
        const newColor = libColor.Color.fromConverterArg(value);
        const newState = {};
        const transtime = utils.getTransition(entity, key, meta).time;
        const supportsHueAndSaturation = utils.getMetaValue(entity, meta.mapped, "supportsHueAndSaturation", "allEqual", false);
        const supportsEnhancedHue = utils.getMetaValue(entity, meta.mapped, "supportsEnhancedHue", "allEqual", false);
        const omitOptionalLevelAndColorParams = utils.getMetaValue(entity, meta.mapped, "omitOptionalLevelAndColorParams", "allEqual", false);
        const optionalParams = omitOptionalLevelAndColorParams ? null : { optionsMask: 0, optionsOverride: 0 };
        if (newColor.isHSV() && supportsHueAndSaturation) {
            const hsv = newColor.hsv;
            const hsvCorrected = hsv.colorCorrected(meta);
            newState.color_mode = constants.colorModeLookup[0];
            newState.color = hsv.toObject(false);
            if (hsv.value !== null && utils.isObject(value)) {
                await entity.command("genLevelCtrl", "moveToLevelWithOnOff", { level: utils.mapNumberRange(hsvCorrected.value, 0, 100, 0, 254), transtime, ...optionalParams }, utils.getOptions(meta.mapped, entity));
            }
            if (hsv.hue !== null && hsv.saturation !== null) {
                const saturation = utils.mapNumberRange(hsvCorrected.saturation, 0, 100, 0, 254);
                if (supportsEnhancedHue) {
                    const enhancehue = utils.mapNumberRange(hsvCorrected.hue, 0, 360, 0, 65535);
                    await entity.command("lightingColorCtrl", "enhancedMoveToHueAndSaturation", { transtime, enhancehue, saturation, ...optionalParams }, utils.getOptions(meta.mapped, entity));
                }
                else {
                    const hue = utils.mapNumberRange(hsvCorrected.hue, 0, 360, 0, 254);
                    await entity.command("lightingColorCtrl", "moveToHueAndSaturation", { transtime, hue, saturation, ...optionalParams }, utils.getOptions(meta.mapped, entity));
                }
            }
            else if (hsv.hue !== null) {
                const direction = value.direction || 0;
                if (supportsEnhancedHue) {
                    const enhancehue = utils.mapNumberRange(hsvCorrected.hue, 0, 360, 0, 65535);
                    await entity.command("lightingColorCtrl", "enhancedMoveToHue", { transtime, enhancehue, direction, ...optionalParams }, utils.getOptions(meta.mapped, entity));
                }
                else {
                    const hue = utils.mapNumberRange(hsvCorrected.hue, 0, 360, 0, 254);
                    await entity.command("lightingColorCtrl", "moveToHue", { transtime, hue, direction, ...optionalParams }, utils.getOptions(meta.mapped, entity));
                }
            }
            else if (hsv.saturation !== null) {
                const saturation = utils.mapNumberRange(hsvCorrected.saturation, 0, 100, 0, 254);
                await entity.command("lightingColorCtrl", "moveToSaturation", { transtime, saturation, ...optionalParams }, utils.getOptions(meta.mapped, entity));
            }
        }
        else if (newColor.isRGB() || newColor.isXY() || newColor.isHSV()) {
            // convert RGB/HSV to XY color mode
            // (many devices only support XY, some support also HSV, but RGB is not supported at all)
            const xy = newColor.isRGB()
                ? newColor.rgb.gammaCorrected().toXY().rounded(4)
                : newColor.isHSV()
                    ? newColor.hsv.colorCorrected(meta).toXY().rounded(4)
                    : newColor.xy;
            // Some bulbs e.g. RB 185 C don't turn to red (they don't respond at all) when x: 0.701 and y: 0.299
            // is send. These values are e.g. send by Home Assistant when clicking red in the color wheel.
            // If we slightly modify these values the bulb will respond.
            // https://github.com/home-assistant/home-assistant/issues/31094
            if (utils.getMetaValue(entity, meta.mapped, "applyRedFix", "allEqual", false) && xy.x === 0.701 && xy.y === 0.299) {
                xy.x = 0.7006;
                xy.y = 0.2993;
            }
            newState.color_mode = constants.colorModeLookup[1];
            newState.color = xy.toObject();
            const colorx = utils.mapNumberRange(xy.x, 0, 1, 0, 65535);
            const colory = utils.mapNumberRange(xy.y, 0, 1, 0, 65535);
            await entity.command("lightingColorCtrl", "moveToColor", { transtime, colorx, colory, ...optionalParams }, utils.getOptions(meta.mapped, entity));
        }
        else {
            throw new Error("Invalid color");
        }
        return { state: libColor.syncColorState(newState, meta.state, entity, meta.options) };
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("lightingColorCtrl", light.readColorAttributes(entity, meta));
    },
};
exports.light_colortemp = {
    key: ["color_temp", "color_temp_percent"],
    options: [exposes.options.color_sync(), exposes.options.transition()],
    convertSet: async (entity, key, value, meta) => {
        const [colorTempMin, colorTempMax] = light.findColorTempRange(entity);
        const preset = { warmest: colorTempMax, warm: 454, neutral: 370, cool: 250, coolest: colorTempMin };
        const omitOptionalLevelAndColorParams = utils.getMetaValue(entity, meta.mapped, "omitOptionalLevelAndColorParams", "allEqual", false);
        const optionalParams = omitOptionalLevelAndColorParams ? null : { optionsMask: 0, optionsOverride: 0 };
        if (key === "color_temp_percent") {
            utils.assertNumber(value);
            value = utils
                .mapNumberRange(value, 0, 100, colorTempMin != null ? colorTempMin : 154, colorTempMax != null ? colorTempMax : 500)
                .toString();
        }
        if (utils.isString(value) && value in preset) {
            value = utils.getFromLookup(value, preset);
        }
        value = Number(value);
        // ensure value within range
        utils.assertNumber(value);
        value = light.clampColorTemp(value, colorTempMin, colorTempMax);
        await entity.command("lightingColorCtrl", "moveToColorTemp", { colortemp: value, transtime: utils.getTransition(entity, key, meta).time, ...optionalParams }, utils.getOptions(meta.mapped, entity));
        return {
            state: libColor.syncColorState({ color_mode: constants.colorModeLookup[2], color_temp: value }, meta.state, entity, meta.options),
        };
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("lightingColorCtrl", ["colorMode", "colorTemperature"]);
    },
};
// #region Generic converters
exports.read = {
    key: ["read"],
    convertSet: async (entity, key, value, meta) => {
        utils.assertObject(value, key);
        const result = await entity.read(value.cluster, value.attributes, value.options != null ? value.options : {});
        logger_1.logger.info(`Read result of '${value.cluster}': ${JSON.stringify(result)}`, NS);
        if (value.state_property != null) {
            return { state: { [value.state_property]: result } };
        }
    },
};
exports.write = {
    key: ["write"],
    convertSet: async (entity, key, value, meta) => {
        utils.assertObject(value, key);
        const options = utils.getOptions(meta.mapped, entity);
        if (value.options != null) {
            Object.assign(options, value.options);
        }
        await entity.write(value.cluster, value.payload, options);
        logger_1.logger.info(`Wrote '${JSON.stringify(value.payload)}' to '${value.cluster}'`, NS);
    },
};
exports.command = {
    key: ["command"],
    convertSet: async (entity, key, value, meta) => {
        utils.assertObject(value, key);
        const options = utils.getOptions(meta.mapped, entity);
        await entity.command(value.cluster, value.command, value.payload != null ? value.payload : {}, options);
        logger_1.logger.info(`Invoked '${value.cluster}.${value.command}' with payload '${JSON.stringify(value.payload)}'`, NS);
    },
};
exports.factory_reset = {
    key: ["reset"],
    convertSet: async (entity, key, value, meta) => {
        await entity.command("genBasic", "resetFactDefault", {}, utils.getOptions(meta.mapped, entity));
    },
};
exports.identify = {
    key: ["identify"],
    options: [exposes.options.identify_timeout()],
    convertSet: async (entity, key, value, meta) => {
        // External value takes priority over options for compatibility
        const identifyTimeout = value ?? meta.options.identify_timeout ?? 3;
        await entity.command("genIdentify", "identify", { identifytime: identifyTimeout }, utils.getOptions(meta.mapped, entity));
    },
};
exports.zcl_command = {
    key: ["zclcommand"],
    convertSet: async (entity, key, value, meta) => {
        utils.assertObject(value, key);
        const payload = value.payload != null ? value.payload : {};
        utils.assertEndpoint(entity);
        await entity.zclCommand(value.cluster, value.command, payload, value.options ?? {}, value.log_payload ?? {}, value.check_status ?? false, value.frametype ?? zigbee_herdsman_1.Zcl.FrameType.SPECIFIC);
        if (value.logging ?? false) {
            logger_1.logger.info(`Invoked ZCL command ${value.cluster}.${value.command} with payload '${JSON.stringify(payload)}'`, NS);
        }
    },
};
exports.arm_mode = {
    key: ["arm_mode"],
    convertSet: async (entity, key, value, meta) => {
        utils.assertEndpoint(entity);
        utils.assertObject(value, key);
        if (Array.isArray(meta.mapped))
            throw new Error("Not supported for groups");
        const isNotification = value.transaction != null;
        const modeSrc = isNotification ? constants.armNotification : constants.armMode;
        const mode = utils.getKey(modeSrc, value.mode, undefined, Number);
        if (mode === undefined) {
            throw new Error(`Unsupported mode: '${value.mode}', should be one of: ${Object.values(modeSrc)}`);
        }
        if (isNotification) {
            await entity.commandResponse("ssIasAce", "armRsp", { armnotification: mode }, {}, value.transaction);
            // Do not update PanelStatus after confirming transaction.
            // Instead the server should send an arm_mode command with the necessary state.
            // e.g. exit_delay as a result of arm_all_zones
            return;
        }
        let panelStatus = mode;
        if (meta.mapped.model === "3400-D") {
            panelStatus = mode !== 0 && mode !== 4 ? 0x80 : 0x00;
        }
        let secondsRemain = 0;
        let delayUntil = 0;
        if ((mode === 4 || mode === 5) && value.delay != null) {
            utils.assertNumber(value.delay, "delay");
            if (!utils.isInRange(0, constants.iasMaxSecondsRemain, value.delay)) {
                throw new Error(`Invalid delay value: ${value.delay} (expected ${0} to ${constants.iasMaxSecondsRemain})`);
            }
            secondsRemain = Math.round(value.delay);
            delayUntil = performance.now() + value.delay * 1000;
        }
        let audibleNotif = 0;
        if (value.audiblenotif != null) {
            utils.assertNumber(value.audiblenotif, "audiblenotif");
            if (!utils.isInRange(0, 255, value.audiblenotif)) {
                throw new Error(`Invalid audiblenotif value: ${value.audiblenotif} (expected ${0} to ${255})`);
            }
            audibleNotif = Math.round(value.audiblenotif);
        }
        globalStore.putValue(entity, "panelStatus", panelStatus);
        globalStore.putValue(entity, "delayUntil", delayUntil);
        globalStore.putValue(entity, "audibleNotif", audibleNotif);
        const payload = { panelstatus: panelStatus, secondsremain: secondsRemain, audiblenotif: audibleNotif, alarmstatus: 0 };
        await entity.commandResponse("ssIasAce", "panelStatusChanged", payload);
    },
};
exports.battery_percentage_remaining = {
    key: ["battery"],
    convertGet: async (entity, key, meta) => {
        await entity.read("genPowerCfg", ["batteryPercentageRemaining"]);
    },
};
exports.battery_voltage = {
    key: ["battery", "voltage"],
    convertGet: async (entity, key, meta) => {
        await entity.read("genPowerCfg", ["batteryVoltage"]);
    },
};
exports.power_on_behavior = {
    key: ["power_on_behavior"],
    convertSet: async (entity, key, value, meta) => {
        utils.assertString(value, key);
        value = value.toLowerCase();
        const lookup = { off: 0, on: 1, toggle: 2, previous: 255 };
        try {
            await entity.write("genOnOff", { startUpOnOff: utils.getFromLookup(value, lookup) }, utils.getOptions(meta.mapped, entity));
        }
        catch (error) {
            if (error.message.includes("UNSUPPORTED_ATTRIBUTE")) {
                throw new Error("Got `UNSUPPORTED_ATTRIBUTE` error, device does not support power on behaviour");
            }
            throw error;
        }
        return { state: { power_on_behavior: value } };
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("genOnOff", ["startUpOnOff"]);
    },
};
exports.light_color_mode = {
    key: ["color_mode"],
    convertGet: async (entity, key, meta) => {
        await entity.read("lightingColorCtrl", ["colorMode"]);
    },
};
exports.light_color_options = {
    key: ["color_options"],
    convertSet: async (entity, key, value, meta) => {
        utils.assertObject(value, key);
        const options = value.execute_if_off != null && value.execute_if_off ? 1 : 0;
        await entity.write("lightingColorCtrl", { options }, utils.getOptions(meta.mapped, entity));
        return { state: { color_options: value } };
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("lightingColorCtrl", ["options"]);
    },
};
exports.lock = {
    key: ["state"],
    convertSet: async (entity, key, value, meta) => {
        // If no pin code is provided, value is a only string. Ex: "UNLOCK"
        let state = utils.isString(value) ? value.toUpperCase() : null;
        let pincode = "";
        // If pin code is provided, value is an object including new state and code. Ex: {state: "UNLOCK", code: "1234"}
        if (utils.isObject(value)) {
            if (value.code) {
                pincode = utils.isString(value.code) ? value.code : "";
            }
            if (value.state) {
                state = utils.isString(value.state) ? value.state.toUpperCase() : null;
            }
        }
        utils.validateValue(state, ["LOCK", "UNLOCK", "TOGGLE"]);
        await entity.command("closuresDoorLock", `${state.toLowerCase()}Door`, { pincodevalue: Buffer.from(pincode, "ascii") }, utils.getOptions(meta.mapped, entity));
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("closuresDoorLock", ["lockState"]);
    },
};
exports.lock_auto_relock_time = {
    key: ["auto_relock_time"],
    convertSet: async (entity, key, value, meta) => {
        await entity.write("closuresDoorLock", { autoRelockTime: value }, utils.getOptions(meta.mapped, entity));
        return { state: { auto_relock_time: value } };
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("closuresDoorLock", ["autoRelockTime"]);
    },
};
exports.lock_sound_volume = {
    key: ["sound_volume"],
    convertSet: async (entity, key, value, meta) => {
        utils.assertString(value, key);
        utils.validateValue(value, constants.lockSoundVolume);
        await entity.write("closuresDoorLock", { soundVolume: constants.lockSoundVolume.indexOf(value) }, utils.getOptions(meta.mapped, entity));
        return { state: { sound_volume: value } };
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("closuresDoorLock", ["soundVolume"]);
    },
};
exports.pincode_lock = {
    key: ["pin_code"],
    convertSet: async (entity, key, value, meta) => {
        utils.assertObject(value, key);
        const user = value.user;
        const userType = value.user_type || "unrestricted";
        const userEnabled = value.user_enabled != null ? value.user_enabled : true;
        const pinCode = value.pin_code;
        if (Number.isNaN(user))
            throw new Error("user must be numbers");
        const pinCodeCount = utils.getMetaValue(entity, meta.mapped, "pinCodeCount");
        if (!utils.isInRange(0, pinCodeCount - 1, user))
            throw new Error("user must be in range for device");
        if (pinCode == null) {
            await entity.command("closuresDoorLock", "clearPinCode", { userid: user }, utils.getOptions(meta.mapped, entity));
        }
        else {
            if (Number.isNaN(pinCode))
                throw new Error("pinCode must be a number");
            const typeLookup = { unrestricted: 0, year_day_schedule: 1, week_day_schedule: 2, master: 3, non_access: 4 };
            const payload = {
                userid: user,
                userstatus: userEnabled ? 1 : 3,
                usertype: utils.getFromLookup(userType, typeLookup),
                pincodevalue: pinCode.toString(),
            };
            await entity.command("closuresDoorLock", "setPinCode", payload, utils.getOptions(meta.mapped, entity));
        }
    },
    convertGet: async (entity, key, meta) => {
        // @ts-expect-error ignore
        const user = meta?.message?.pin_code ? meta.message.pin_code.user : undefined;
        if (user === undefined) {
            const max = utils.getMetaValue(entity, meta.mapped, "pinCodeCount");
            // Get all
            const options = utils.getOptions(meta.mapped, entity);
            for (let i = 0; i < max; i++) {
                await entity.command("closuresDoorLock", "getPinCode", { userid: i }, options);
            }
        }
        else {
            if (Number.isNaN(user)) {
                throw new Error("user must be numbers");
            }
            const pinCodeCount = utils.getMetaValue(entity, meta.mapped, "pinCodeCount");
            if (!utils.isInRange(0, pinCodeCount - 1, user)) {
                throw new Error("userId must be in range for device");
            }
            await entity.command("closuresDoorLock", "getPinCode", { userid: user }, utils.getOptions(meta.mapped, entity));
        }
    },
};
exports.lock_userstatus = {
    key: ["user_status"],
    convertSet: async (entity, key, value, meta) => {
        utils.assertObject(value, key);
        const user = value.user;
        if (Number.isNaN(user)) {
            throw new Error("user must be numbers");
        }
        const pinCodeCount = utils.getMetaValue(entity, meta.mapped, "pinCodeCount");
        if (!utils.isInRange(0, pinCodeCount - 1, user)) {
            throw new Error("user must be in range for device");
        }
        const status = utils.getKey(constants.lockUserStatus, value.status, undefined, Number);
        if (status === undefined) {
            throw new Error(`Unsupported status: '${value.status}', should be one of: ${Object.values(constants.lockUserStatus)}`);
        }
        await entity.command("closuresDoorLock", "setUserStatus", {
            userid: user,
            userstatus: status,
        }, utils.getOptions(meta.mapped, entity));
    },
    convertGet: async (entity, key, meta) => {
        // @ts-expect-error ignore
        const user = meta?.message?.user_status ? meta.message.user_status.user : undefined;
        const pinCodeCount = utils.getMetaValue(entity, meta.mapped, "pinCodeCount");
        if (user === undefined) {
            const max = pinCodeCount;
            // Get all
            const options = utils.getOptions(meta.mapped, entity);
            for (let i = 0; i < max; i++) {
                await entity.command("closuresDoorLock", "getUserStatus", { userid: i }, options);
            }
        }
        else {
            if (Number.isNaN(user)) {
                throw new Error("user must be numbers");
            }
            if (!utils.isInRange(0, pinCodeCount - 1, user)) {
                throw new Error("userId must be in range for device");
            }
            await entity.command("closuresDoorLock", "getUserStatus", { userid: user }, utils.getOptions(meta.mapped, entity));
        }
    },
};
exports.cover_via_brightness = {
    key: ["position", "state"],
    options: [exposes.options.invert_cover()],
    convertSet: async (entity, key, value, meta) => {
        if (typeof value !== "number") {
            utils.assertString(value, key);
            value = value.toLowerCase();
            if (value === "stop") {
                await entity.command("genLevelCtrl", "stop", { optionsMask: 0, optionsOverride: 0 }, utils.getOptions(meta.mapped, entity));
                return;
            }
            const lookup = { open: 100, close: 0 };
            value = utils.getFromLookup(value, lookup);
        }
        const invert = utils.getMetaValue(entity, meta.mapped, "coverInverted", "allEqual", false)
            ? !meta.options.invert_cover
            : meta.options.invert_cover;
        utils.assertNumber(value);
        const position = invert ? 100 - value : value;
        await entity.command("genLevelCtrl", "moveToLevelWithOnOff", { level: utils.mapNumberRange(Number(position), 0, 100, 0, 255), transtime: 0, optionsMask: 0, optionsOverride: 0 }, utils.getOptions(meta.mapped, entity));
        return { state: { position: value } };
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("genLevelCtrl", ["currentLevel"]);
    },
};
exports.warning = {
    key: ["warning"],
    convertSet: async (entity, key, value, meta) => {
        const mode = { stop: 0, burglar: 1, fire: 2, emergency: 3, police_panic: 4, fire_panic: 5, emergency_panic: 6 };
        const level = { low: 0, medium: 1, high: 2, very_high: 3 };
        const strobeLevel = { low: 0, medium: 1, high: 2, very_high: 3 };
        const values = {
            // @ts-expect-error ignore
            mode: value.mode || "emergency",
            // @ts-expect-error ignore
            level: value.level || "medium",
            // @ts-expect-error ignore
            strobe: value.strobe != null ? value.strobe : true,
            // @ts-expect-error ignore
            duration: value.duration != null ? value.duration : 10,
            // @ts-expect-error ignore
            strobeDutyCycle: value.strobe_duty_cycle != null ? value.strobe_duty_cycle * 10 : 0,
            // @ts-expect-error ignore
            strobeLevel: value.strobe_level != null ? utils.getFromLookup(value.strobe_level, strobeLevel) : 1,
        };
        // biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress`
        let info;
        // https://github.com/Koenkk/zigbee2mqtt/issues/8310 some devices require the info to be reversed.
        if (Array.isArray(meta.mapped))
            throw new Error("Not supported for groups");
        // SIRZB-110/111 require all-zero info byte to reliably stop the siren.
        if (values.mode === "stop" && ["SIRZB-110", "SIRZB-111"].includes(meta.mapped.model)) {
            // @ts-expect-error ignore
            if (value.level == null)
                values.level = "low";
            // @ts-expect-error ignore
            if (value.strobe == null)
                values.strobe = false;
        }
        if (["SIRZB-110", "SRAC-23B-ZBSR", "AV2010/29A", "AV2010/24A"].includes(meta.mapped.model)) {
            info = utils.getFromLookup(values.mode, mode) + ((values.strobe ? 1 : 0) << 4) + (utils.getFromLookup(values.level, level) << 6);
        }
        else {
            info = (utils.getFromLookup(values.mode, mode) << 4) + ((values.strobe ? 1 : 0) << 2) + utils.getFromLookup(values.level, level);
        }
        await entity.command("ssIasWd", "startWarning", { startwarninginfo: info, warningduration: values.duration, strobedutycycle: values.strobeDutyCycle, strobelevel: values.strobeLevel }, utils.getOptions(meta.mapped, entity));
    },
};
exports.warning_simple = {
    key: ["alarm"],
    convertSet: async (entity, key, value, meta) => {
        const alarmState = value === "alarm" || value === "OFF" ? 0 : 1;
        // biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress`
        let info;
        // For Develco SMSZB-120 and HESZB-120, introduced change in fw 4.0.5, tested backward with 4.0.4
        if (Array.isArray(meta.mapped))
            throw new Error("Not supported for groups");
        if (["SMSZB-120", "HESZB-120"].includes(meta.mapped.model)) {
            info = (alarmState << 7) + (alarmState << 6);
        }
        else if (meta.mapped.model === "SIRZB-110") {
            // ZCL-compliant layout: bits 0-3=mode, bit 4=strobe, bits 6-7=level
            // OFF: info=0 (mode=stop, level=low, strobe=off — device requires level=0 to stop)
            // ON: emergency(3) + strobe(1<<4) + very_high(3<<6) = 211
            info = alarmState === 0 ? 0 : 3 + (1 << 4) + (3 << 6);
        }
        else if (meta.mapped.model === "SIRZB-111") {
            // Generic layout: bits 4-7=mode, bit 2=strobe, bits 0-1=level
            // OFF: info=0, ON: (emergency<<4) + (strobe<<2) + very_high = 55
            info = alarmState === 0 ? 0 : (3 << 4) + (1 << 2) + 3;
        }
        else {
            info = (3 << 6) + (alarmState << 2);
        }
        await entity.command("ssIasWd", "startWarning", { startwarninginfo: info, warningduration: 300, strobedutycycle: 0, strobelevel: 0 }, utils.getOptions(meta.mapped, entity));
    },
};
exports.squawk = {
    key: ["squawk"],
    convertSet: async (entity, key, value, meta) => {
        utils.assertObject(value, key);
        const state = { system_is_armed: 0, system_is_disarmed: 1 };
        const level = { low: 0, medium: 1, high: 2, very_high: 3 };
        const values = {
            state: value.state,
            level: value.level || "very_high",
            strobe: value.strobe != null ? value.strobe : false,
        };
        const info = utils.getFromLookup(values.state, state) + ((values.strobe ? 1 : 0) << 4) + (utils.getFromLookup(values.level, level) << 6);
        await entity.command("ssIasWd", "squawk", { squawkinfo: info }, utils.getOptions(meta.mapped, entity));
    },
};
exports.cover_state = {
    key: ["state"],
    options: [exposes.options.invert_cover()],
    convertSet: async (entity, key, value, meta) => {
        const lookup = {
            open: "upOpen",
            close: "downClose",
            stop: "stop",
            on: "upOpen",
            off: "downClose",
        };
        const invertedLookup = {
            open: "downClose",
            close: "upOpen",
            stop: "stop",
            on: "downClose",
            off: "upOpen",
        };
        utils.assertString(value, key);
        const invert = utils.getMetaValue(entity, meta.mapped, "coverInverted", "allEqual", false)
            ? !meta.options.invert_cover
            : meta.options.invert_cover;
        const commandLookup = invert ? invertedLookup : lookup;
        await entity.command("closuresWindowCovering", utils.getFromLookup(value.toLowerCase(), commandLookup), {}, utils.getOptions(meta.mapped, entity));
    },
};
exports.cover_position_tilt = {
    key: ["position", "tilt"],
    options: [exposes.options.invert_cover(), exposes.options.cover_position_tilt_disable_report()],
    convertSet: async (entity, key, value, meta) => {
        utils.assertNumber(value, key);
        const isPosition = key === "position";
        const invert = !(utils.getMetaValue(entity, meta.mapped, "coverInverted", "allEqual", false)
            ? !meta.options.invert_cover
            : meta.options.invert_cover);
        const disableReport = utils.getMetaValue(entity, meta.mapped, "coverPositionTiltDisableReport", "allEqual", false)
            ? !meta.options.cover_position_tilt_disable_report
            : meta.options.cover_position_tilt_disable_report;
        const position = invert ? 100 - value : value;
        // Zigbee officially expects 'open' to be 0 and 'closed' to be 100 whereas
        // HomeAssistant etc. work the other way round.
        // For zigbee-herdsman-converters: open = 100, close = 0
        await entity.command("closuresWindowCovering", isPosition ? "goToLiftPercentage" : "goToTiltPercentage", isPosition ? { percentageliftvalue: position } : { percentagetiltvalue: position }, utils.getOptions(meta.mapped, entity));
        if (disableReport) {
            return;
        }
        return { state: { [isPosition ? "position" : "tilt"]: value } };
    },
    convertGet: async (entity, key, meta) => {
        const isPosition = key === "position";
        await entity.read("closuresWindowCovering", [isPosition ? "currentPositionLiftPercentage" : "currentPositionTiltPercentage"]);
    },
};
exports.cover_mode = {
    key: ["cover_mode"],
    convertSet: async (entity, key, value, meta) => {
        utils.assertObject(value, key);
        const windowCoveringMode = ((value.reversed ? 1 : 0) << 0) | ((value.calibration ? 1 : 0) << 1) | ((value.maintenance ? 1 : 0) << 2) | ((value.led ? 1 : 0) << 3);
        await entity.write("closuresWindowCovering", { windowCoveringMode }, utils.getOptions(meta.mapped, entity));
        return { state: { cover_mode: value } };
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("closuresWindowCovering", ["windowCoveringMode"]);
    },
};
exports.occupancy_timeout = {
    // Sets delay after motion detector changes from occupied to unoccupied
    key: ["occupancy_timeout"],
    convertSet: async (entity, key, value, meta) => {
        utils.assertNumber(value);
        value *= 1;
        await entity.write("msOccupancySensing", { pirOToUDelay: value }, utils.getOptions(meta.mapped, entity));
        return { state: { occupancy_timeout: value } };
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("msOccupancySensing", ["pirOToUDelay"]);
    },
};
exports.level_config = {
    key: ["level_config"],
    convertSet: async (entity, key, value, meta) => {
        const state = {};
        // parse payload to grab the keys
        if (typeof value === "string") {
            try {
                value = JSON.parse(value);
            }
            catch {
                throw new Error("Payload is not valid JSON");
            }
        }
        utils.assertObject(value, key);
        // onOffTransitionTime - range 0x0000 to 0xffff - optional
        if (value.on_off_transition_time != null) {
            let onOffTransitionTimeValue = Number(value.on_off_transition_time) * 10;
            if (onOffTransitionTimeValue > 65535)
                onOffTransitionTimeValue = 65535;
            if (onOffTransitionTimeValue < 0)
                onOffTransitionTimeValue = 0;
            await entity.write("genLevelCtrl", { onOffTransitionTime: onOffTransitionTimeValue }, utils.getOptions(meta.mapped, entity));
            Object.assign(state, { on_off_transition_time: onOffTransitionTimeValue / 10 });
        }
        // onTransitionTime - range 0x0000 to 0xffff - optional
        //                    0xffff = use onOffTransitionTime
        if (value.on_transition_time != null) {
            let onTransitionTimeValue = value.on_transition_time;
            if (typeof onTransitionTimeValue === "number")
                onTransitionTimeValue *= 10;
            if (typeof onTransitionTimeValue === "string" && onTransitionTimeValue.toLowerCase() === "disabled") {
                onTransitionTimeValue = 65535;
            }
            else {
                onTransitionTimeValue = Number(onTransitionTimeValue);
            }
            if (onTransitionTimeValue > 65535)
                onTransitionTimeValue = 65534;
            if (onTransitionTimeValue < 0)
                onTransitionTimeValue = 0;
            await entity.write("genLevelCtrl", { onTransitionTime: onTransitionTimeValue }, utils.getOptions(meta.mapped, entity));
            // reverse translate number -> preset
            if (onTransitionTimeValue === 65535) {
                onTransitionTimeValue = "disabled";
            }
            Object.assign(state, {
                on_transition_time: typeof onTransitionTimeValue === "number" ? onTransitionTimeValue / 10 : onTransitionTimeValue,
            });
        }
        // offTransitionTime - range 0x0000 to 0xffff - optional
        //                    0xffff = use onOffTransitionTime
        if (value.off_transition_time != null) {
            let offTransitionTimeValue = value.off_transition_time;
            if (typeof offTransitionTimeValue === "number")
                offTransitionTimeValue *= 10;
            if (typeof offTransitionTimeValue === "string" && offTransitionTimeValue.toLowerCase() === "disabled") {
                offTransitionTimeValue = 65535;
            }
            else {
                offTransitionTimeValue = Number(offTransitionTimeValue);
            }
            if (offTransitionTimeValue > 65535)
                offTransitionTimeValue = 65534;
            if (offTransitionTimeValue < 0)
                offTransitionTimeValue = 0;
            await entity.write("genLevelCtrl", { offTransitionTime: offTransitionTimeValue }, utils.getOptions(meta.mapped, entity));
            // reverse translate number -> preset
            if (offTransitionTimeValue === 65535) {
                offTransitionTimeValue = "disabled";
            }
            Object.assign(state, {
                off_transition_time: typeof offTransitionTimeValue === "number" ? offTransitionTimeValue / 10 : offTransitionTimeValue,
            });
        }
        // startUpCurrentLevel - range 0x00 to 0xff - optional
        //                       0x00 = return to minimum supported level
        //                       0xff = return to previous previous
        if (value.current_level_startup != null) {
            let startUpCurrentLevelValue = value.current_level_startup;
            if (typeof startUpCurrentLevelValue === "string" && startUpCurrentLevelValue.toLowerCase() === "previous") {
                startUpCurrentLevelValue = 255;
            }
            else if (typeof startUpCurrentLevelValue === "string" && startUpCurrentLevelValue.toLowerCase() === "minimum") {
                startUpCurrentLevelValue = 0;
            }
            else {
                startUpCurrentLevelValue = Number(startUpCurrentLevelValue);
            }
            if (startUpCurrentLevelValue > 255)
                startUpCurrentLevelValue = 254;
            if (startUpCurrentLevelValue < 0)
                startUpCurrentLevelValue = 1;
            await entity.write("genLevelCtrl", { startUpCurrentLevel: startUpCurrentLevelValue }, utils.getOptions(meta.mapped, entity));
            // reverse translate number -> preset
            if (startUpCurrentLevelValue === 255) {
                startUpCurrentLevelValue = "previous";
            }
            if (startUpCurrentLevelValue === 0) {
                startUpCurrentLevelValue = "minimum";
            }
            Object.assign(state, { current_level_startup: startUpCurrentLevelValue });
        }
        // onLevel - range 0x00 to 0xff - optional
        //           Any value outside of MinLevel to MaxLevel, including 0xff and 0x00, is interpreted as "previous".
        if (value.on_level != null) {
            let onLevel = value.on_level;
            if (typeof onLevel === "string" && onLevel.toLowerCase() === "previous") {
                onLevel = 255;
            }
            else {
                onLevel = Number(onLevel);
            }
            if (onLevel > 255)
                onLevel = 254;
            if (onLevel < 1)
                onLevel = 1;
            await entity.write("genLevelCtrl", { onLevel }, utils.getOptions(meta.mapped, entity));
            Object.assign(state, { on_level: onLevel === 255 ? "previous" : onLevel });
        }
        // options - 8-bit map
        //   bit 0: ExecuteIfOff - when 0, Move commands are ignored if the device is off;
        //          when 1, CurrentLevel can be changed while the device is off.
        //   bit 1: CoupleColorTempToLevel - when 1, changes to level also change color temperature.
        //          (What this means is not defined, but it's most likely to be "dim to warm".)
        if (value.execute_if_off != null) {
            const executeIfOffValue = !!value.execute_if_off;
            await entity.write("genLevelCtrl", { options: executeIfOffValue ? 1 : 0 }, utils.getOptions(meta.mapped, entity));
            Object.assign(state, { execute_if_off: executeIfOffValue });
        }
        if (Object.keys(state).length > 0) {
            return { state: { level_config: state } };
        }
    },
    convertGet: async (entity, key, meta) => {
        for (const attribute of [
            "onOffTransitionTime",
            "onTransitionTime",
            "offTransitionTime",
            "startUpCurrentLevel",
            "onLevel",
            "options",
        ]) {
            try {
                await entity.read("genLevelCtrl", [attribute]);
            }
            catch {
                // continue regardless of error, all these are optional in ZCL
            }
        }
    },
};
exports.ballast_config = {
    key: ["ballast_config", "ballast_minimum_level", "ballast_maximum_level", "ballast_power_on_level"],
    // zcl attribute names are camel case, but we want to use snake case in the outside communication
    convertSet: async (entity, key, value, meta) => {
        if (key === "ballast_config") {
            value = utils.toCamelCase(value);
            for (const [attrName, attrValue] of Object.entries(value)) {
                const attributes = { [attrName]: attrValue };
                await entity.write("lightingBallastCfg", attributes);
            }
        }
        if (key === "ballast_minimum_level") {
            await entity.write("lightingBallastCfg", { minLevel: value });
        }
        if (key === "ballast_maximum_level") {
            await entity.write("lightingBallastCfg", { maxLevel: value });
        }
        if (key === "ballast_power_on_level") {
            await entity.write("lightingBallastCfg", { powerOnLevel: value });
        }
        return { state: { [key]: value } };
    },
    convertGet: async (entity, key, meta) => {
        let result = {};
        for (const attrName of [
            "ballastStatus",
            "minLevel",
            "maxLevel",
            "powerOnLevel",
            "powerOnFadeTime",
            "intrinsicBallastFactor",
            "ballastFactorAdjustment",
            "lampQuantity",
            "lampType",
            "lampManufacturer",
            "lampRatedHours",
            "lampBurnHours",
            "lampAlarmMode",
            "lampBurnHoursTripPoint",
        ]) {
            try {
                result = { ...result, ...(await entity.read("lightingBallastCfg", [attrName])) };
            }
            catch {
                // continue regardless of error
            }
        }
        if (key === "ballast_config") {
            logger_1.logger.debug(`ballast_config attribute results received: ${JSON.stringify(utils.toSnakeCase(result))}`, NS);
        }
    },
};
exports.light_brightness_step = {
    key: ["brightness_step", "brightness_step_onoff"],
    options: [exposes.options.transition()],
    convertSet: async (entity, key, value, meta) => {
        const omitOptionalLevelAndColorParams = utils.getMetaValue(entity, meta.mapped, "omitOptionalLevelAndColorParams", "allEqual", false);
        const optionalParams = omitOptionalLevelAndColorParams ? null : { optionsMask: 0, optionsOverride: 0 };
        const onOff = key.endsWith("_onoff");
        const command = onOff ? "stepWithOnOff" : "step";
        value = Number(value);
        utils.assertNumber(value, key);
        const mode = value > 0 ? 0 : 1;
        const transition = utils.getTransition(entity, key, meta).time;
        await entity.command("genLevelCtrl", command, { stepmode: mode, stepsize: Math.abs(value), transtime: transition, ...optionalParams }, utils.getOptions(meta.mapped, entity));
        if (meta.state.brightness !== undefined) {
            utils.assertNumber(meta.state.brightness);
            let brightness = onOff || meta.state.state === "ON" ? meta.state.brightness + value : meta.state.brightness;
            if (value === 0) {
                const entityToRead = utils.getEntityOrFirstGroupMember(entity);
                if (entityToRead) {
                    brightness = (await entityToRead.read("genLevelCtrl", ["currentLevel"])).currentLevel;
                }
            }
            brightness = Math.min(254, brightness);
            brightness = Math.max(onOff || meta.state.state === "OFF" ? 0 : 1, brightness);
            if (utils.getMetaValue(entity, meta.mapped, "turnsOffAtBrightness1", "allEqual", false)) {
                if (onOff && value < 0 && brightness === 1) {
                    brightness = 0;
                }
                else if (onOff && value > 0 && meta.state.brightness === 0) {
                    brightness++;
                }
            }
            return { state: { brightness, state: brightness === 0 ? "OFF" : "ON" } };
        }
    },
};
exports.light_brightness_move = {
    key: ["brightness_move", "brightness_move_onoff"],
    convertSet: async (entity, key, value, meta) => {
        const omitOptionalLevelAndColorParams = utils.getMetaValue(entity, meta.mapped, "omitOptionalLevelAndColorParams", "allEqual", false);
        const optionalParams = omitOptionalLevelAndColorParams ? null : { optionsMask: 0, optionsOverride: 0 };
        if (value === "stop" || value === 0) {
            await entity.command("genLevelCtrl", "stop", { ...optionalParams }, utils.getOptions(meta.mapped, entity));
            // As we cannot determine the new brightness state, we read it from the device
            await utils.sleep(500);
            const target = utils.getEntityOrFirstGroupMember(entity);
            const onOff = (await target.read("genOnOff", ["onOff"])).onOff;
            const brightness = (await target.read("genLevelCtrl", ["currentLevel"])).currentLevel;
            return { state: { brightness, state: onOff === 1 ? "ON" : "OFF" } };
        }
        value = Number(value);
        utils.assertNumber(value, key);
        const command = key.endsWith("onoff") ? "moveWithOnOff" : "move";
        await entity.command("genLevelCtrl", command, { movemode: value > 0 ? 0 : 1, rate: Math.abs(value), ...optionalParams }, utils.getOptions(meta.mapped, entity));
    },
};
exports.light_colortemp_step = {
    key: ["color_temp_step"],
    options: [exposes.options.transition()],
    convertSet: async (entity, key, value, meta) => {
        const omitOptionalLevelAndColorParams = utils.getMetaValue(entity, meta.mapped, "omitOptionalLevelAndColorParams", "allEqual", false);
        const optionalParams = omitOptionalLevelAndColorParams ? null : { optionsMask: 0, optionsOverride: 0 };
        value = Number(value);
        utils.assertNumber(value, key);
        const mode = value > 0 ? 1 : 3;
        const transition = utils.getTransition(entity, key, meta).time;
        await entity.command("lightingColorCtrl", "stepColorTemp", {
            stepmode: mode,
            stepsize: Math.abs(value),
            transtime: transition,
            minimum: 0,
            maximum: 600,
            ...optionalParams,
        }, utils.getOptions(meta.mapped, entity));
        // We cannot determine the color temperature from the current state so we read it, because
        // - We don't know the max/min values
        // - Color mode could have been switched (x/y or hue/saturation)
        const entityToRead = utils.getEntityOrFirstGroupMember(entity);
        if (entityToRead) {
            await utils.sleep(100 + transition * 100);
            await entityToRead.read("lightingColorCtrl", ["colorTemperature"]);
        }
    },
};
exports.light_colortemp_move = {
    key: ["colortemp_move", "color_temp_move"],
    convertSet: async (entity, key, value, meta) => {
        const omitOptionalLevelAndColorParams = utils.getMetaValue(entity, meta.mapped, "omitOptionalLevelAndColorParams", "allEqual", false);
        const optionalParams = omitOptionalLevelAndColorParams ? null : { optionsMask: 0, optionsOverride: 0 };
        // Initialize payload with default constraints
        let minimum = 0;
        let maximum = 600;
        let rate;
        let movemode;
        // Handle different input formats
        if (utils.isString(value)) {
            // String-based commands
            const stringValue = value.toLowerCase();
            if (stringValue === "stop" || stringValue === "release" || stringValue === "0") {
                rate = 1;
                movemode = 0;
            }
            else if (stringValue === "up" || stringValue === "1") {
                rate = meta.message?.rate != null ? Number(meta.message.rate) : 55;
                movemode = 1; // Move to warmer (higher color temp)
            }
            else if (stringValue === "down") {
                rate = meta.message?.rate != null ? Number(meta.message.rate) : 55;
                movemode = 3; // Move to cooler (lower color temp)
            }
            else {
                throw new Error(`${key}: invalid string value "${value}". Expected "stop", "release", "0", "up", "1", or "down"`);
            }
            // Use legacy constraints for string-based commands
            minimum = 153;
            maximum = 370;
        }
        else if (utils.isNumber(value)) {
            // Simple number input
            const numValue = Number(value);
            if (numValue === 0) {
                // Stop command via 0 rate
                rate = 1;
                movemode = 0;
            }
            else {
                // Normal movement
                rate = Math.abs(numValue);
                movemode = numValue > 0 ? 1 : 3;
            }
        }
        else if (utils.isObject(value)) {
            // Object input with rate and optional constraints
            utils.assertObject(value, key);
            if (value.rate == null) {
                throw new Error(`${key}: object must contain 'rate' property`);
            }
            const rateValue = Number(value.rate);
            utils.assertNumber(rateValue, `${key}.rate`);
            if (rateValue === 0) {
                // Stop command via 0 rate
                rate = 1;
                movemode = 0;
            }
            else {
                // Normal movement
                rate = Math.abs(rateValue);
                movemode = rateValue > 0 ? 1 : 3;
            }
            // Apply custom constraints if provided
            if (value.minimum != null) {
                const minValue = Number(value.minimum);
                utils.assertNumber(minValue, `${key}.minimum`);
                minimum = minValue;
            }
            if (value.maximum != null) {
                const maxValue = Number(value.maximum);
                utils.assertNumber(maxValue, `${key}.maximum`);
                maximum = maxValue;
            }
            // Validate constraints
            if (minimum >= maximum) {
                throw new Error(`${key}: minimum (${minimum}) must be less than maximum (${maximum})`);
            }
        }
        else {
            throw new Error(`${key}: invalid value type. Expected number, string, or object with rate property`);
        }
        // Send command
        await entity.command("lightingColorCtrl", "moveColorTemp", { minimum, maximum, rate, movemode, ...optionalParams }, utils.getOptions(meta.mapped, entity));
        // Read current color temperature if stopping
        if (movemode === 0) {
            const entityToRead = utils.getEntityOrFirstGroupMember(entity);
            if (entityToRead) {
                await utils.sleep(100);
                await entityToRead.read("lightingColorCtrl", ["colorTemperature", "colorMode"]);
            }
        }
    },
};
exports.light_color_and_colortemp_via_color = {
    key: ["color", "color_temp", "color_temp_percent"],
    options: [exposes.options.color_sync(), exposes.options.transition()],
    convertSet: async (entity, key, value, meta) => {
        const omitOptionalLevelAndColorParams = utils.getMetaValue(entity, meta.mapped, "omitOptionalLevelAndColorParams", "allEqual", false);
        const optionalParams = omitOptionalLevelAndColorParams ? null : { optionsMask: 0, optionsOverride: 0 };
        if (key === "color") {
            return await exports.light_color.convertSet(entity, key, value, meta);
        }
        if (key === "color_temp" || key === "color_temp_percent") {
            utils.assertNumber(value);
            const xy = libColor.ColorXY.fromMireds(value);
            await entity.command("lightingColorCtrl", "moveToColor", {
                transtime: utils.getTransition(entity, key, meta).time,
                colorx: utils.mapNumberRange(xy.x, 0, 1, 0, 65535),
                colory: utils.mapNumberRange(xy.y, 0, 1, 0, 65535),
                ...optionalParams,
            }, utils.getOptions(meta.mapped, entity));
            return {
                state: libColor.syncColorState({ color_mode: constants.colorModeLookup[2], color_temp: value }, meta.state, entity, meta.options),
            };
        }
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("lightingColorCtrl", light.readColorAttributes(entity, meta));
    },
};
exports.light_hue_saturation_step = {
    key: ["hue_step", "saturation_step"],
    options: [exposes.options.transition()],
    convertSet: async (entity, key, value, meta) => {
        const omitOptionalLevelAndColorParams = utils.getMetaValue(entity, meta.mapped, "omitOptionalLevelAndColorParams", "allEqual", false);
        const optionalParams = omitOptionalLevelAndColorParams ? null : { optionsMask: 0, optionsOverride: 0 };
        value = Number(value);
        utils.assertNumber(value, key);
        const command = key === "hue_step" ? "stepHue" : "stepSaturation";
        const attribute = key === "hue_step" ? "currentHue" : "currentSaturation";
        const mode = value > 0 ? 1 : 3;
        const transition = utils.getTransition(entity, key, meta).time;
        await entity.command("lightingColorCtrl", command, { stepmode: mode, stepsize: Math.abs(value), transtime: transition, ...optionalParams }, utils.getOptions(meta.mapped, entity));
        // We cannot determine the hue/saturation from the current state so we read it, because
        // - Color mode could have been switched (x/y or colortemp)
        const entityToRead = utils.getEntityOrFirstGroupMember(entity);
        if (entityToRead) {
            await utils.sleep(100 + transition * 100);
            await entityToRead.read("lightingColorCtrl", [attribute, "colorMode"]);
        }
    },
};
exports.light_hue_saturation_move = {
    key: ["hue_move", "saturation_move"],
    convertSet: async (entity, key, value, meta) => {
        const omitOptionalLevelAndColorParams = utils.getMetaValue(entity, meta.mapped, "omitOptionalLevelAndColorParams", "allEqual", false);
        const optionalParams = omitOptionalLevelAndColorParams ? null : { optionsMask: 0, optionsOverride: 0 };
        value = value === "stop" ? value : Number(value);
        const command = key === "hue_move" ? "moveHue" : "moveSaturation";
        const attribute = key === "hue_move" ? "currentHue" : "currentSaturation";
        let rate = 0;
        let movemode = 0;
        if (value === "stop" || value === 0) {
            rate = 1;
            movemode = 0;
        }
        else {
            utils.assertNumber(value, key);
            rate = Math.abs(value);
            movemode = value > 0 ? 1 : 3;
        }
        await entity.command("lightingColorCtrl", command, { rate, movemode, ...optionalParams }, utils.getOptions(meta.mapped, entity));
        // We cannot determine the hue/saturation from the current state so we read it, because
        // - Color mode could have been switched (x/y or colortemp)
        if (value === "stop" || value === 0) {
            const entityToRead = utils.getEntityOrFirstGroupMember(entity);
            if (entityToRead) {
                await utils.sleep(100);
                await entityToRead.read("lightingColorCtrl", [attribute, "colorMode"]);
            }
        }
    },
};
exports.light_onoff_brightness = {
    key: ["state", "brightness", "brightness_percent", "on_time", "off_wait_time"],
    options: [exposes.options.transition()],
    convertSet: async (entity, key, value, meta) => {
        const { message } = meta;
        const transition = utils.getTransition(entity, "brightness", meta);
        const turnsOffAtBrightness1 = utils.getMetaValue(entity, meta.mapped, "turnsOffAtBrightness1", "allEqual", false);
        const moveToLevelWithOnOffDisable = utils.getMetaValue(entity, meta.mapped, "moveToLevelWithOnOffDisable", "allEqual", false);
        const omitOptionalLevelAndColorParams = utils.getMetaValue(entity, meta.mapped, "omitOptionalLevelAndColorParams", "allEqual", false);
        const optionalParams = omitOptionalLevelAndColorParams ? null : { optionsMask: 0, optionsOverride: 0 };
        let state = message.state !== undefined ? (typeof message.state === "string" ? message.state.toLowerCase() : null) : undefined;
        let brightness;
        if (message.brightness != null) {
            brightness = Number(message.brightness);
        }
        else if (message.brightness_percent != null) {
            brightness = utils.mapNumberRange(Number(message.brightness_percent), 0, 100, 0, 255);
        }
        if (brightness === 255) {
            // Allow 255 for backwards compatibility.
            brightness = 254;
        }
        if (brightness !== undefined && (Number.isNaN(brightness) || brightness < 0 || brightness > 254)) {
            throw new Error(`Brightness value of message: '${JSON.stringify(message)}' invalid, must be a number >= 0 and =< 254`);
        }
        if (state !== undefined && state !== null && ["on", "off", "toggle"].includes(state) === false) {
            throw new Error(`State value of message: '${JSON.stringify(message)}' invalid, must be 'ON', 'OFF' or 'TOGGLE'`);
        }
        if ((state === undefined || state === null) && brightness === undefined) {
            throw new Error(`At least one of "brightness" or "state" must have a value: '${JSON.stringify(message)}'`);
        }
        // Infer state from desired brightness if unset. Ideally we'd want to keep it as it is, but this code has always
        // used 'MoveToLevelWithOnOff' so that'd break backwards compatibility. To keep the state, the user
        // has to explicitly set it to null.
        if (state === undefined) {
            if (moveToLevelWithOnOffDisable) {
                // Although in some cases it can be usefull to use explicit on and off and moveToLevel. In these casese
                // we just ignore the brightness value and set the state to the current device state.
                state = meta.state.state.toLowerCase();
            }
            else {
                // Also write to `meta.message.state` in case we delegate to the `on_off` converter.
                state = meta.message.state = brightness === 0 ? "off" : "on";
            }
        }
        let publishBrightness = brightness !== undefined;
        const targetState = state === "toggle" ? (meta.state.state === "ON" ? "off" : "on") : state;
        if (targetState === "off") {
            // Simulate 'Off' with transition via 'MoveToLevelWithOnOff', otherwise just use 'Off'.
            // TODO: if this is a group where some members don't support Level Control, turning them off
            //  with transition may have no effect. (Some devices, such as Envilar ZG302-BOX-RELAY, handle
            //  'MoveToLevelWithOnOff' despite not supporting the cluster; others, like the LEDVANCE SMART+
            //  plug, do not.)
            brightness = transition.specified || brightness === 0 ? 0 : undefined;
            if (brightness !== undefined &&
                meta.state.state === "OFF" &&
                utils.getMetaValue(entity, meta.mapped, "noOffTransitionWhenOff", { atLeastOnce: true }, false)) {
                logger_1.logger.debug("Suppressing OFF transition since entity is OFF and has noOffTransitionWhenOff=true", NS);
                brightness = undefined;
            }
            if (meta.state.brightness !== undefined && meta.state.state === "ON") {
                // The light's current level gets clobbered in two cases:
                //   1. when 'Off' has a transition, in which case it is really 'MoveToLevelWithOnOff'
                //      https://github.com/Koenkk/zigbee-herdsman-converters/issues/1073
                //   2. when 'OnLevel' is set: "If OnLevel is not defined, set the CurrentLevel to the stored level."
                //      https://github.com/Koenkk/zigbee2mqtt/issues/2850#issuecomment-580365633
                // We need to remember current brightness in case the next 'On' does not provide it. `meta` is not reliable
                // here, as it will get clobbered too if reporting is configured.
                globalStore.putValue(entity, "brightness", meta.state.brightness);
                globalStore.putValue(entity, "turnedOffWithTransition", brightness !== undefined);
            }
        }
        else if (targetState === "on" && brightness === undefined) {
            // Simulate 'On' with transition via 'MoveToLevelWithOnOff', or restore the level from before
            // it was clobbered by a previous transition to off; otherwise just use 'On'.
            // TODO: same problem as above.
            // TODO: if transition is not specified, should use device default (OnTransitionTime), not 0.
            if (transition.specified || globalStore.getValue(entity, "turnedOffWithTransition") === true) {
                const levelConfig = utils.getObjectProperty(meta.state, "level_config", {});
                let onLevel = utils.getObjectProperty(levelConfig, "on_level", 0);
                if (onLevel === 0 && entity.meta.onLevelSupported !== false) {
                    try {
                        const attributeRead = await entity.read("genLevelCtrl", ["onLevel"]);
                        if (attributeRead !== undefined) {
                            onLevel = attributeRead.onLevel;
                        }
                    }
                    catch {
                        // OnLevel not supported
                    }
                }
                if (onLevel === 0) {
                    onLevel = "previous";
                    entity.meta.onLevelSupported = false;
                    entity.save();
                }
                if (onLevel === 255 || onLevel === "previous") {
                    const current = utils.getObjectProperty(meta.state, "brightness", 254);
                    brightness = globalStore.getValue(entity, "brightness", current);
                }
                else {
                    brightness = onLevel;
                }
                // Published state might have gotten clobbered by reporting.
                publishBrightness = true;
            }
        }
        if (brightness === undefined) {
            const result = (await exports.on_off.convertSet(entity, "state", state, meta));
            if (result) {
                if (result.state && result.state.state === "ON" && meta.state.brightness === 0) {
                    result.state.brightness = 1;
                }
            }
            return result;
        }
        if (brightness === 0 && (targetState === "on" || state === null)) {
            brightness = 1;
        }
        if (brightness === 1 && turnsOffAtBrightness1) {
            brightness = 2;
        }
        if (targetState !== "off") {
            globalStore.putValue(entity, "brightness", brightness);
            globalStore.clearValue(entity, "turnedOffWithTransition");
        }
        if (moveToLevelWithOnOffDisable) {
            // On some devices "moveToLevelWithOnOff" command seems to be broken, leading to the light
            // randomly switching off for levels lower than some threshold. Is those cases it's better to
            // use "moveToLevel" with explicit On and Off when the state changes.
            if (typeof meta.state.state === "string" && meta.state.state.toLowerCase() !== targetState) {
                if (targetState === "on") {
                    const payload = { level: Number(brightness), transtime: transition.time, ...optionalParams };
                    await entity.command("genLevelCtrl", "moveToLevel", payload, utils.getOptions(meta.mapped, entity));
                }
                await exports.on_off.convertSet(entity, "state", state, meta);
            }
            else {
                const payload = { level: Number(brightness), transtime: transition.time, ...optionalParams };
                await entity.command("genLevelCtrl", "moveToLevel", payload, utils.getOptions(meta.mapped, entity));
            }
        }
        else {
            const payload = { level: Number(brightness), transtime: transition.time, ...optionalParams };
            await entity.command("genLevelCtrl", state === null ? "moveToLevel" : "moveToLevelWithOnOff", payload, utils.getOptions(meta.mapped, entity));
        }
        const result = { state: {} };
        if (publishBrightness) {
            result.state.brightness = Number(brightness);
        }
        if (state !== null && !moveToLevelWithOnOffDisable) {
            result.state.state = brightness === 0 ? "OFF" : "ON";
        }
        return result;
    },
    convertGet: async (entity, key, meta) => {
        if (key === "brightness") {
            await entity.read("genLevelCtrl", ["currentLevel"]);
        }
        else if (key === "state") {
            await exports.on_off.convertGet(entity, key, meta);
        }
    },
};
exports.light_colortemp_startup = {
    key: ["color_temp_startup"],
    convertSet: async (entity, key, value, meta) => {
        const [colorTempMin, colorTempMax] = light.findColorTempRange(entity);
        const preset = { warmest: colorTempMax, warm: 454, neutral: 370, cool: 250, coolest: colorTempMin, previous: 65535 };
        if (utils.isString(value) && value in preset) {
            value = utils.getFromLookup(value, preset);
        }
        value = Number(value);
        utils.assertNumber(value);
        // ensure value within range
        // we do allow one exception for 0xffff, which is to restore the previous value
        if (value !== 65535) {
            value = light.clampColorTemp(value, colorTempMin, colorTempMax);
        }
        await entity.write("lightingColorCtrl", { startUpColorTemperature: value /* type failure? */ }, utils.getOptions(meta.mapped, entity));
        return { state: { color_temp_startup: value } };
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("lightingColorCtrl", ["startUpColorTemperature"]);
    },
};
exports.light_color_colortemp = {
    /**
     * This converter is a combination of light_color and light_colortemp and
     * can be used instead of the two individual converters . When used to set,
     * it actually calls out to light_color or light_colortemp to get the
     * return value. When used to get, it gets both color and colorTemp in
     * one call.
     * The reason for the existence of this somewhat peculiar converter is
     * that some lights don't report their state when changed. To fix this,
     * we query the state after we set it. We want to query color and colorTemp
     * both when setting either, because both change when setting one. This
     * converter is used to do just that.
     */
    key: ["color", "color_temp", "color_temp_percent"],
    options: [exposes.options.color_sync(), exposes.options.transition()],
    convertSet: async (entity, key, value, meta) => {
        if (key === "color") {
            const result = await exports.light_color.convertSet(entity, key, value, meta);
            return result;
        }
        if (key === "color_temp" || key === "color_temp_percent") {
            const result = await exports.light_colortemp.convertSet(entity, key, value, meta);
            return result;
        }
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("lightingColorCtrl", light.readColorAttributes(entity, meta, ["colorTemperature"]));
    },
};
exports.effect = {
    key: ["effect", "alert", "flash"], // alert and flash are deprecated.
    convertSet: async (entity, key, value, meta) => {
        if (key === "effect") {
            utils.assertString(value, key);
            const lookup = { blink: 0, breathe: 1, okay: 2, channel_change: 11, finish_effect: 254, stop_effect: 255 };
            value = value.toLowerCase();
            if (value === "colorloop") {
                const transition = meta.message.transition ?? 15;
                utils.assertNumber(transition, "transition");
                const speed = Math.min(255, Math.max(1, Math.round(255 / transition)));
                await exports.light_hue_saturation_move.convertSet(entity, "hue_move", speed, meta);
            }
            else if (value === "stop_colorloop") {
                await exports.light_hue_saturation_move.convertSet(entity, "hue_move", "stop", meta);
            }
            else {
                const payload = { effectid: utils.getFromLookup(value, lookup), effectvariant: 0 };
                await entity.command("genIdentify", "triggerEffect", payload, utils.getOptions(meta.mapped, entity));
            }
        }
        else if (key === "alert" || key === "flash") {
            // Deprecated
            let effectid = 0;
            const lookup = { select: 0x00, lselect: 0x01, none: 0xff };
            if (key === "flash") {
                if (value === 2) {
                    value = "select";
                }
                else if (value === 10) {
                    value = "lselect";
                }
            }
            effectid = utils.getFromLookup(value, lookup);
            const payload = { effectid, effectvariant: 0 };
            await entity.command("genIdentify", "triggerEffect", payload, utils.getOptions(meta.mapped, entity));
        }
    },
};
exports.thermostat_remote_sensing = {
    key: ["remote_sensing"],
    convertSet: async (entity, key, value, meta) => {
        await entity.write("hvacThermostat", { remoteSensing: value });
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("hvacThermostat", ["remoteSensing"]);
    },
};
exports.thermostat_weekly_schedule = {
    key: ["weekly_schedule"],
    convertSet: async (entity, key, value, meta) => {
        /*
             * We want to support a simple human creatable format to send a schedule:
                 {"weekly_schedule": {
                   "dayofweek": ["monday", "tuesday"],
                   "transitions": [
                     {"heatSetpoint": 16, "transitionTime": "0:00"},
                     {"heatSetpoint": 20, "transitionTime": "18:00"},
                     {"heatSetpoint": 16, "transitionTime": "19:30"}
                   ]}}

             * However exposes is not flexible enough to describe something like this. There is a
             *  much more verbose format we also support so that exposes work.
                 {"weekly_schedule": {
                   "dayofweek": [
                     {"day": "monday"},
                     {"day": "tuesday"}
                   ],
                   "transitions": [
                     {"heatSetpoint": 16, "transitionTime": {"hour": 0,  "minute": 0}},
                     {"heatSetpoint": 20, "transitionTime": {"hour": 18, "minute": 0}},
                     {"heatSetpoint": 16, "transitionTime": {"hour": 19, "minute": 30}}
                   ]}}
             */
        utils.assertObject(value, key);
        let daysofweek = value.dayofweek;
        const transitions = value.transitions;
        const convertedTransitions = [];
        let numoftrans = 0;
        const modes = [];
        if (Array.isArray(transitions)) {
            // calculate numoftrans
            if (typeof value.numoftrans !== "undefined") {
                logger_1.logger.warning(`weekly_schedule: ignoring provided numoftrans value (${JSON.stringify(value.numoftrans)}), this is now calculated automatically`, NS);
            }
            numoftrans = transitions.length;
            // mode is calculated below
            if (typeof value.mode !== "undefined") {
                logger_1.logger.warning(`weekly_schedule: ignoring provided mode value (${JSON.stringify(value.mode)}), this is now calculated automatically`, NS);
            }
            // transform transition payload values if needed
            for (const elem of transitions) {
                const heatSetpoint = elem.heatSetpoint ?? elem.heat_setpoint;
                const coolSetpoint = elem.coolSetpoint ?? elem.cool_setpoint;
                let transitionTime = elem.transitionTime ?? elem.transition_time ?? elem.time;
                const convertedTransition = {};
                // update mode if needed
                if (heatSetpoint != null && !modes.includes("heat")) {
                    modes.push("heat");
                }
                if (coolSetpoint != null && !modes.includes("cool")) {
                    modes.push("cool");
                }
                // transform setpoint values if numeric
                if (typeof heatSetpoint === "number") {
                    convertedTransition.heatSetpoint = Math.round(heatSetpoint * 100);
                }
                else if (heatSetpoint != null) {
                    convertedTransition.heatSetpoint = heatSetpoint;
                }
                if (typeof coolSetpoint === "number") {
                    convertedTransition.coolSetpoint = Math.round(coolSetpoint * 100);
                }
                else if (coolSetpoint != null) {
                    convertedTransition.coolSetpoint = coolSetpoint;
                }
                // accept 24h time notation (e.g. 19:30)
                if (typeof transitionTime === "string") {
                    const time = transitionTime.split(":");
                    const timeHour = Number.parseInt(time[0], 10) * 60;
                    const timeMinute = Number.parseInt(time[1], 10);
                    if (time.length !== 2 || Number.isNaN(timeHour) || Number.isNaN(timeMinute)) {
                        logger_1.logger.warning(`weekly_schedule: expected 24h time notation (e.g. 19:30) but got '${transitionTime}'!`, NS);
                    }
                    else {
                        transitionTime = timeHour + timeMinute;
                    }
                }
                else if (typeof transitionTime === "object") {
                    if (transitionTime.hour == null || transitionTime.minute == null) {
                        throw new Error(`weekly_schedule: expected 24h time object (e.g. {"hour": 19, "minute": 30}), but got '${JSON.stringify(transitionTime)}'!`);
                    }
                    if (Number.isNaN(transitionTime.hour)) {
                        throw new Error(`weekly_schedule: expected time.hour to be a number, but got '${transitionTime.hour}'!`);
                    }
                    if (Number.isNaN(transitionTime.minute)) {
                        throw new Error(`weekly_schedule: expected time.minute to be a number, but got '${transitionTime.minute}'!`);
                    }
                    transitionTime = Number.parseInt(transitionTime.hour, 10) * 60 + Number.parseInt(transitionTime.minute, 10);
                }
                convertedTransition.transitionTime = transitionTime;
                convertedTransitions.push(convertedTransition);
            }
        }
        else {
            logger_1.logger.error("weekly_schedule: transitions is not an array!", NS);
            return;
        }
        // map array of desired modes to bitmask
        let mode = 0;
        for (const m of modes) {
            // lookup mode bit
            mode |= 1 << utils.getKey(constants.thermostatScheduleMode, m, 0, Number);
        }
        // map array of days to desired dayofweek bitmask
        if (typeof daysofweek === "string") {
            daysofweek = [daysofweek];
        }
        let dayofweek = 0;
        if (Array.isArray(daysofweek)) {
            for (let d of daysofweek) {
                if (typeof d === "object") {
                    if (d.day == null) {
                        throw new Error(`weekly_schedule: expected dayofweek to be string or {"day": "str"}, but got '${JSON.stringify(d)}'!`);
                    }
                    d = d.day;
                }
                // lookup dayofweek bit
                d = utils.getKey(constants.thermostatDayOfWeek, d.toLowerCase(), d, Number);
                dayofweek |= 1 << d;
            }
        }
        await entity.command("hvacThermostat", "setWeeklySchedule", { dayofweek, numoftrans, transitions: convertedTransitions, mode }, utils.getOptions(meta.mapped, entity));
    },
    convertGet: async (entity, key, meta) => {
        const payload = {
            daystoreturn: 0xff, // Sun-Sat and vacation
            modetoreturn: 3, // heat + cool
        };
        await entity.command("hvacThermostat", "getWeeklySchedule", payload, utils.getOptions(meta.mapped, entity));
    },
};
exports.thermostat_system_mode = {
    key: ["system_mode"],
    convertSet: async (entity, key, value, meta) => {
        let systemMode = utils.getKey(constants.thermostatSystemModes, value, undefined, Number);
        if (systemMode === undefined) {
            systemMode = utils.getKey(legacy.thermostatSystemModes, value, value, Number);
        }
        await entity.write("hvacThermostat", { systemMode });
        return { state: { system_mode: value } };
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("hvacThermostat", ["systemMode"]);
    },
};
exports.thermostat_control_sequence_of_operation = {
    key: ["control_sequence_of_operation"],
    convertSet: async (entity, key, value, meta) => {
        utils.assertEndpoint(entity);
        let val = utils.getKey(constants.thermostatControlSequenceOfOperations, value, undefined, Number);
        if (val === undefined) {
            val = utils.getKey(constants.thermostatControlSequenceOfOperations, value, value, Number);
        }
        const attributes = { ctrlSeqeOfOper: val };
        await entity.write("hvacThermostat", attributes);
        // NOTE: update the cluster attribute we store as this is used by
        //       SMaBiT AV2010/32's dynamic expose function.
        entity.saveClusterAttributeKeyValue("hvacThermostat", attributes);
        return { state: { control_sequence_of_operation: value } };
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("hvacThermostat", ["ctrlSeqeOfOper"]);
    },
};
exports.thermostat_programming_operation_mode = {
    key: ["programming_operation_mode"],
    convertSet: async (entity, key, value, meta) => {
        const val = utils.getKey(constants.thermostatProgrammingOperationModes, value, undefined, Number);
        if (val === undefined) {
            throw new Error(`Programming operation mode invalid, must be one of: ${Object.values(constants.thermostatProgrammingOperationModes).join(", ")}`);
        }
        await entity.write("hvacThermostat", { programingOperMode: val });
        return { state: { programming_operation_mode: value } };
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("hvacThermostat", ["programingOperMode"]);
    },
};
exports.thermostat_temperature_display_mode = {
    key: ["temperature_display_mode"],
    convertSet: async (entity, key, value, meta) => {
        const tempDisplayMode = utils.getKey(constants.temperatureDisplayMode, value, value, Number);
        await entity.write("hvacUserInterfaceCfg", { tempDisplayMode });
        return { state: { temperature_display_mode: value } };
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("hvacUserInterfaceCfg", ["tempDisplayMode"]);
    },
};
exports.thermostat_keypad_lockout = {
    key: ["keypad_lockout"],
    convertSet: async (entity, key, value, meta) => {
        const keypadLockout = utils.getKey(constants.keypadLockoutMode, value, value, Number);
        await entity.write("hvacUserInterfaceCfg", { keypadLockout });
        return { state: { keypad_lockout: value } };
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("hvacUserInterfaceCfg", ["keypadLockout"]);
    },
};
exports.thermostat_temperature_setpoint_hold = {
    key: ["temperature_setpoint_hold"],
    convertSet: async (entity, key, value, meta) => {
        await entity.write("hvacThermostat", { tempSetpointHold: value });
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("hvacThermostat", ["tempSetpointHold"]);
    },
};
exports.thermostat_temperature_setpoint_hold_duration = {
    key: ["temperature_setpoint_hold_duration"],
    convertSet: async (entity, key, value, meta) => {
        await entity.write("hvacThermostat", { tempSetpointHoldDuration: value });
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("hvacThermostat", ["tempSetpointHoldDuration"]);
    },
};
exports.fan_mode = {
    key: ["fan_mode", "fan_state"],
    convertSet: async (entity, key, value, meta) => {
        utils.assertString(value, key);
        const fanMode = utils.getFromLookup(value, constants.fanMode);
        await entity.write("hvacFanCtrl", { fanMode });
        return { state: { fan_mode: value.toLowerCase(), fan_state: value.toLowerCase() === "off" ? "OFF" : "ON" } };
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("hvacFanCtrl", ["fanMode"]);
    },
};
exports.fan_speed = {
    key: ["speed"],
    convertSet: async (entity, key, value, meta) => {
        utils.assertNumber(value);
        await entity.command("genLevelCtrl", "moveToLevel", { level: value, transtime: 0, optionsMask: 0, optionsOverride: 0 }, utils.getOptions(meta.mapped, entity));
        return { state: { speed: value } };
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("genLevelCtrl", ["currentLevel"]);
    },
};
exports.thermostat_local_temperature = {
    key: ["local_temperature"],
    convertGet: async (entity, key, meta) => {
        await entity.read("hvacThermostat", ["localTemp"]);
    },
};
exports.thermostat_outdoor_temperature = {
    key: ["outdoor_temperature"],
    convertGet: async (entity, key, meta) => {
        await entity.read("hvacThermostat", ["outdoorTemp"]);
    },
};
exports.thermostat_local_temperature_calibration = {
    key: ["local_temperature_calibration"],
    convertSet: async (entity, key, value, meta) => {
        utils.assertNumber(value);
        await entity.write("hvacThermostat", { localTemperatureCalibration: Math.round(value * 10) });
        return { state: { local_temperature_calibration: value } };
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("hvacThermostat", ["localTemperatureCalibration"]);
    },
};
exports.thermostat_occupancy = {
    key: ["occupancy"],
    convertGet: async (entity, key, meta) => {
        await entity.read("hvacThermostat", ["occupancy"]);
    },
};
exports.thermostat_clear_weekly_schedule = {
    key: ["clear_weekly_schedule"],
    convertSet: async (entity, key, value, meta) => {
        await entity.command("hvacThermostat", "clearWeeklySchedule", {}, utils.getOptions(meta.mapped, entity));
    },
};
exports.thermostat_pi_heating_demand = {
    key: ["pi_heating_demand"],
    convertGet: async (entity, key, meta) => {
        await entity.read("hvacThermostat", ["pIHeatingDemand"]);
    },
};
exports.thermostat_running_state = {
    key: ["running_state"],
    convertGet: async (entity, key, meta) => {
        await entity.read("hvacThermostat", ["runningState"]);
    },
};
exports.thermostat_occupied_heating_setpoint = {
    key: ["occupied_heating_setpoint"],
    options: [exposes.options.thermostat_unit()],
    convertSet: async (entity, key, value, meta) => {
        utils.assertNumber(value, key);
        let result;
        if (meta.options.thermostat_unit === "fahrenheit") {
            result = Math.round(utils.normalizeCelsiusVersionOfFahrenheit(value) * 100);
        }
        else {
            result = Number((Math.round(Number((value * 2).toFixed(1))) / 2).toFixed(1)) * 100;
        }
        const occupiedHeatingSetpoint = result;
        await entity.write("hvacThermostat", { occupiedHeatingSetpoint });
        return { state: { occupied_heating_setpoint: value } };
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("hvacThermostat", ["occupiedHeatingSetpoint"]);
    },
};
exports.thermostat_unoccupied_heating_setpoint = {
    key: ["unoccupied_heating_setpoint"],
    options: [exposes.options.thermostat_unit()],
    convertSet: async (entity, key, value, meta) => {
        utils.assertNumber(value, key);
        // biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress`
        let result;
        if (meta.options.thermostat_unit === "fahrenheit") {
            result = Math.round(utils.normalizeCelsiusVersionOfFahrenheit(value) * 100);
        }
        else {
            result = Number((Math.round(Number((value * 2).toFixed(1))) / 2).toFixed(1)) * 100;
        }
        const unoccupiedHeatingSetpoint = result;
        await entity.write("hvacThermostat", { unoccupiedHeatingSetpoint });
        return { state: { unoccupied_heating_setpoint: value } };
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("hvacThermostat", ["unoccupiedHeatingSetpoint"]);
    },
};
exports.thermostat_occupied_cooling_setpoint = {
    key: ["occupied_cooling_setpoint"],
    options: [exposes.options.thermostat_unit()],
    convertSet: async (entity, key, value, meta) => {
        utils.assertNumber(value, key);
        // biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress`
        let result;
        if (meta.options.thermostat_unit === "fahrenheit") {
            result = Math.round(utils.normalizeCelsiusVersionOfFahrenheit(value) * 100);
        }
        else {
            result = Number((Math.round(Number((value * 2).toFixed(1))) / 2).toFixed(1)) * 100;
        }
        const occupiedCoolingSetpoint = result;
        await entity.write("hvacThermostat", { occupiedCoolingSetpoint });
        return { state: { occupied_cooling_setpoint: value } };
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("hvacThermostat", ["occupiedCoolingSetpoint"]);
    },
};
exports.thermostat_unoccupied_cooling_setpoint = {
    key: ["unoccupied_cooling_setpoint"],
    options: [exposes.options.thermostat_unit()],
    convertSet: async (entity, key, value, meta) => {
        utils.assertNumber(value, key);
        // biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress`
        let result;
        if (meta.options.thermostat_unit === "fahrenheit") {
            result = Math.round(utils.normalizeCelsiusVersionOfFahrenheit(value) * 100);
        }
        else {
            result = Number((Math.round(Number((value * 2).toFixed(1))) / 2).toFixed(1)) * 100;
        }
        const unoccupiedCoolingSetpoint = result;
        await entity.write("hvacThermostat", { unoccupiedCoolingSetpoint });
        return { state: { unoccupied_cooling_setpoint: value } };
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("hvacThermostat", ["unoccupiedCoolingSetpoint"]);
    },
};
exports.thermostat_setpoint_raise_lower = {
    key: ["setpoint_raise_lower"],
    convertSet: async (entity, key, value, meta) => {
        utils.assertObject(value, key);
        const payload = { mode: value.mode, amount: Math.round(value.amount) * 100 };
        await entity.command("hvacThermostat", "setpointRaiseLower", payload, utils.getOptions(meta.mapped, entity));
    },
};
exports.thermostat_relay_status_log = {
    key: ["relay_status_log"],
    convertGet: async (entity, key, meta) => {
        await entity.command("hvacThermostat", "getRelayStatusLog", {}, utils.getOptions(meta.mapped, entity));
    },
};
exports.thermostat_running_mode = {
    key: ["running_mode"],
    convertGet: async (entity, key, meta) => {
        await entity.read("hvacThermostat", ["runningMode"]);
    },
};
exports.thermostat_min_heat_setpoint_limit = {
    key: ["min_heat_setpoint_limit"],
    convertSet: async (entity, key, value, meta) => {
        utils.assertNumber(value);
        // biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress`
        let result;
        if (meta.options.thermostat_unit === "fahrenheit") {
            result = Math.round(utils.normalizeCelsiusVersionOfFahrenheit(value) * 100);
        }
        else {
            result = Number((Math.round(Number((value * 2).toFixed(1))) / 2).toFixed(1)) * 100;
        }
        const minHeatSetpointLimit = result;
        await entity.write("hvacThermostat", { minHeatSetpointLimit });
        return { state: { min_heat_setpoint_limit: value } };
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("hvacThermostat", ["minHeatSetpointLimit"]);
    },
};
exports.thermostat_max_heat_setpoint_limit = {
    key: ["max_heat_setpoint_limit"],
    convertSet: async (entity, key, value, meta) => {
        utils.assertNumber(value, key);
        // biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress`
        let result;
        if (meta.options.thermostat_unit === "fahrenheit") {
            result = Math.round(utils.normalizeCelsiusVersionOfFahrenheit(value) * 100);
        }
        else {
            result = Number((Math.round(Number((value * 2).toFixed(1))) / 2).toFixed(1)) * 100;
        }
        const maxHeatSetpointLimit = result;
        await entity.write("hvacThermostat", { maxHeatSetpointLimit });
        return { state: { max_heat_setpoint_limit: value } };
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("hvacThermostat", ["maxHeatSetpointLimit"]);
    },
};
exports.thermostat_min_cool_setpoint_limit = {
    key: ["min_cool_setpoint_limit"],
    convertSet: async (entity, key, value, meta) => {
        utils.assertNumber(value, key);
        // biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress`
        let result;
        if (meta.options.thermostat_unit === "fahrenheit") {
            result = Math.round(utils.normalizeCelsiusVersionOfFahrenheit(value) * 100);
        }
        else {
            result = Number((Math.round(Number((value * 2).toFixed(1))) / 2).toFixed(1)) * 100;
        }
        const minCoolSetpointLimit = result;
        await entity.write("hvacThermostat", { minCoolSetpointLimit });
        return { state: { min_cool_setpoint_limit: value } };
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("hvacThermostat", ["minCoolSetpointLimit"]);
    },
};
exports.thermostat_max_cool_setpoint_limit = {
    key: ["max_cool_setpoint_limit"],
    convertSet: async (entity, key, value, meta) => {
        utils.assertNumber(value, key);
        // biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress`
        let result;
        if (meta.options.thermostat_unit === "fahrenheit") {
            result = Math.round(utils.normalizeCelsiusVersionOfFahrenheit(value) * 100);
        }
        else {
            result = Number((Math.round(Number((value * 2).toFixed(1))) / 2).toFixed(1)) * 100;
        }
        const maxCoolSetpointLimit = result;
        await entity.write("hvacThermostat", { maxCoolSetpointLimit });
        return { state: { max_cool_setpoint_limit: value } };
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("hvacThermostat", ["maxCoolSetpointLimit"]);
    },
};
exports.thermostat_ac_louver_position = {
    key: ["ac_louver_position"],
    convertSet: async (entity, key, value, meta) => {
        let acLouverPosition = utils.getKey(constants.thermostatAcLouverPositions, value, undefined, Number);
        if (acLouverPosition === undefined) {
            acLouverPosition = utils.getKey(constants.thermostatAcLouverPositions, value, value, Number);
        }
        await entity.write("hvacThermostat", { acLouverPosition });
        return { state: { ac_louver_position: value } };
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("hvacThermostat", ["acLouverPosition"]);
    },
};
exports.electrical_measurement_power = {
    key: ["power"],
    convertGet: async (entity, key, meta) => {
        const ep = (0, utils_1.determineEndpoint)(entity, meta, "haElectricalMeasurement");
        await ep.read("haElectricalMeasurement", ["activePower"]);
    },
};
exports.electrical_measurement_power_phase_b = {
    key: ["power_phase_b"],
    convertGet: async (entity, key, meta) => {
        const ep = (0, utils_1.determineEndpoint)(entity, meta, "haElectricalMeasurement");
        await ep.read("haElectricalMeasurement", ["activePowerPhB"]);
    },
};
exports.electrical_measurement_power_phase_c = {
    key: ["power_phase_c"],
    convertGet: async (entity, key, meta) => {
        const ep = (0, utils_1.determineEndpoint)(entity, meta, "haElectricalMeasurement");
        await ep.read("haElectricalMeasurement", ["activePowerPhC"]);
    },
};
exports.metering_power = {
    key: ["power"],
    convertGet: async (entity, key, meta) => {
        utils.assertEndpoint(entity);
        const ep = (0, utils_1.determineEndpoint)(entity, meta, "seMetering");
        await ep.read("seMetering", ["instantaneousDemand"]);
    },
};
exports.metering_status = {
    key: ["status"],
    convertGet: async (entity, key, meta) => {
        utils.assertEndpoint(entity);
        await utils.enforceEndpoint(entity, key, meta).read("seMetering", ["status"]);
    },
};
exports.metering_extended_status = {
    key: ["extended_status"],
    convertGet: async (entity, key, meta) => {
        utils.assertEndpoint(entity);
        await utils.enforceEndpoint(entity, key, meta).read("seMetering", ["extendedStatus"]);
    },
};
exports.currentsummdelivered = {
    key: ["energy"],
    convertGet: async (entity, key, meta) => {
        utils.assertEndpoint(entity);
        const ep = (0, utils_1.determineEndpoint)(entity, meta, "seMetering");
        await ep.read("seMetering", ["currentSummDelivered"]);
    },
};
exports.currenttier1summdelivered = {
    key: ["energy_tier_1"],
    convertGet: async (entity, key, meta) => {
        utils.assertEndpoint(entity);
        const ep = (0, utils_1.determineEndpoint)(entity, meta, "seMetering");
        await ep.read("seMetering", ["currentTier1SummDelivered"]);
    },
};
exports.currenttier2summdelivered = {
    key: ["energy_tier_2"],
    convertGet: async (entity, key, meta) => {
        utils.assertEndpoint(entity);
        const ep = (0, utils_1.determineEndpoint)(entity, meta, "seMetering");
        await ep.read("seMetering", ["currentTier2SummDelivered"]);
    },
};
exports.currenttier3summdelivered = {
    key: ["energy_tier_3"],
    convertGet: async (entity, key, meta) => {
        utils.assertEndpoint(entity);
        const ep = (0, utils_1.determineEndpoint)(entity, meta, "seMetering");
        await ep.read("seMetering", ["currentTier3SummDelivered"]);
    },
};
exports.currenttier4summdelivered = {
    key: ["energy_tier_4"],
    convertGet: async (entity, key, meta) => {
        utils.assertEndpoint(entity);
        const ep = (0, utils_1.determineEndpoint)(entity, meta, "seMetering");
        await ep.read("seMetering", ["currentTier4SummDelivered"]);
    },
};
exports.currentsummreceived = {
    key: ["produced_energy"],
    convertGet: async (entity, key, meta) => {
        utils.assertEndpoint(entity);
        const ep = (0, utils_1.determineEndpoint)(entity, meta, "seMetering");
        await ep.read("seMetering", ["currentSummReceived"]);
    },
};
exports.frequency = {
    key: ["ac_frequency"],
    convertGet: async (entity, key, meta) => {
        const ep = (0, utils_1.determineEndpoint)(entity, meta, "haElectricalMeasurement");
        await ep.read("haElectricalMeasurement", ["acFrequency"]);
    },
};
exports.electrical_measurement_power_reactive = {
    key: ["power_reactive"],
    convertGet: async (entity, key, meta) => {
        await entity.read("haElectricalMeasurement", ["reactivePower"]);
    },
};
exports.powerfactor = {
    key: ["power_factor"],
    convertGet: async (entity, key, meta) => {
        const ep = (0, utils_1.determineEndpoint)(entity, meta, "haElectricalMeasurement");
        await ep.read("haElectricalMeasurement", ["powerFactor"]);
    },
};
exports.acvoltage = {
    key: ["voltage"],
    convertGet: async (entity, key, meta) => {
        const ep = (0, utils_1.determineEndpoint)(entity, meta, "haElectricalMeasurement");
        await ep.read("haElectricalMeasurement", ["rmsVoltage"]);
    },
};
exports.acvoltage_phase_b = {
    key: ["voltage_phase_b"],
    convertGet: async (entity, key, meta) => {
        const ep = (0, utils_1.determineEndpoint)(entity, meta, "haElectricalMeasurement");
        await ep.read("haElectricalMeasurement", ["rmsVoltagePhB"]);
    },
};
exports.acvoltage_phase_c = {
    key: ["voltage_phase_c"],
    convertGet: async (entity, key, meta) => {
        const ep = (0, utils_1.determineEndpoint)(entity, meta, "haElectricalMeasurement");
        await ep.read("haElectricalMeasurement", ["rmsVoltagePhC"]);
    },
};
exports.accurrent = {
    key: ["current"],
    convertGet: async (entity, key, meta) => {
        const ep = (0, utils_1.determineEndpoint)(entity, meta, "haElectricalMeasurement");
        await ep.read("haElectricalMeasurement", ["rmsCurrent"]);
    },
};
exports.accurrent_phase_b = {
    key: ["current_phase_b"],
    convertGet: async (entity, key, meta) => {
        const ep = (0, utils_1.determineEndpoint)(entity, meta, "haElectricalMeasurement");
        await ep.read("haElectricalMeasurement", ["rmsCurrentPhB"]);
    },
};
exports.accurrent_phase_c = {
    key: ["current_phase_c"],
    convertGet: async (entity, key, meta) => {
        const ep = (0, utils_1.determineEndpoint)(entity, meta, "haElectricalMeasurement");
        await ep.read("haElectricalMeasurement", ["rmsCurrentPhC"]);
    },
};
exports.accurrent_neutral = {
    key: ["current_neutral"],
    convertGet: async (entity, key, meta) => {
        const ep = (0, utils_1.determineEndpoint)(entity, meta, "haElectricalMeasurement");
        await ep.read("haElectricalMeasurement", ["neutralCurrent"]);
    },
};
exports.dccurrent = {
    key: ["current"],
    convertGet: async (entity, key, meta) => {
        const ep = (0, utils_1.determineEndpoint)(entity, meta, "haElectricalMeasurement");
        await ep.read("haElectricalMeasurement", ["dcCurrent"]);
    },
};
exports.dcvoltage = {
    key: ["voltage"],
    convertGet: async (entity, key, meta) => {
        const ep = (0, utils_1.determineEndpoint)(entity, meta, "haElectricalMeasurement");
        await ep.read("haElectricalMeasurement", ["dcVoltage"]);
    },
};
exports.dcpower = {
    key: ["power"],
    convertGet: async (entity, key, meta) => {
        const ep = (0, utils_1.determineEndpoint)(entity, meta, "haElectricalMeasurement");
        await ep.read("haElectricalMeasurement", ["dcPower"]);
    },
};
exports.temperature = {
    key: ["temperature"],
    convertGet: async (entity, key, meta) => {
        await entity.read("msTemperatureMeasurement", ["measuredValue"]);
    },
};
exports.humidity = {
    key: ["humidity"],
    convertGet: async (entity, key, meta) => {
        await entity.read("msRelativeHumidity", ["measuredValue"]);
    },
};
// #endregion
// #region Non-generic converters
// biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
exports.TYZB01_on_off = {
    key: ["state", "time_in_seconds"],
    convertSet: async (entity, key, value, meta) => {
        const result = await exports.on_off.convertSet(entity, key, value, meta);
        utils.assertString(value, key);
        const lowerCaseValue = value.toLowerCase();
        if (!["on", "off"].includes(lowerCaseValue)) {
            return result;
        }
        const messageKeys = Object.keys(meta.message);
        const timeInSecondsValue = (() => {
            if (messageKeys.includes("state")) {
                return meta.message.time_in_seconds;
            }
            if (meta.endpoint_name) {
                return meta.message[`time_in_seconds_${meta.endpoint_name}`];
            }
            return null;
        })();
        if (!timeInSecondsValue) {
            return result;
        }
        const timeInSeconds = Number(timeInSecondsValue);
        if (!Number.isInteger(timeInSeconds) || timeInSeconds < 0 || timeInSeconds > 0xfffe) {
            throw new Error("The time_in_seconds value must be convertible to an integer in the range: <0x0000, 0xFFFE>");
        }
        const on = lowerCaseValue === "on";
        await entity.command("genOnOff", "onWithTimedOff", {
            ctrlbits: 0,
            ontime: on ? 0 : timeInSeconds.valueOf(),
            offwaittime: on ? timeInSeconds.valueOf() : 0,
        }, utils.getOptions(meta.mapped, entity));
        return result;
    },
    convertGet: async (entity, key, meta) => {
        await entity.read("genOnOff", ["onOff"]);
    },
};
exports.power_source = {
    key: ["power_source", "charging"],
    convertGet: async (entity, key, meta) => {
        await entity.read("genBasic", ["powerSource"]);
    },
};
exports.scene_store = {
    key: ["scene_store"],
    convertSet: async (entity, key, value, meta) => {
        const isGroup = utils.isGroup(entity);
        const groupid = isGroup ? entity.groupID : value.group_id != null ? value.group_id : 0;
        let sceneid = value;
        let scenename = null;
        if (typeof value === "object") {
            sceneid = value.ID;
            scenename = value.name;
        }
        utils.assertNumber(sceneid, "ID");
        if (groupid === 0 && sceneid === 0) {
            // From Zigbee spec:
            // "Scene identifier 0x00, along with group identifier 0x0000, is reserved for the global scene used by the OnOff cluster"
            throw new Error("Scene ID 0 cannot be used with group ID 0 (reserved).");
        }
        const response = await entity.command("genScenes", "store", { groupid, sceneid }, utils.getOptions(meta.mapped, entity));
        if (isGroup) {
            if (meta.membersState) {
                for (const member of entity.members) {
                    utils.saveSceneState(member, sceneid, groupid, meta.membersState[member.getDevice().ieeeAddr], scenename);
                }
            }
        }
        else if (response.status === 0) {
            utils.saveSceneState(entity, sceneid, groupid, meta.state, scenename);
        }
        else {
            throw new Error(`Scene add not successful ('${zigbee_herdsman_1.Zcl.Status[response.status]}')`);
        }
        logger_1.logger.info("Successfully stored scene", NS);
        return { state: {} };
    },
};
exports.scene_recall = {
    key: ["scene_recall"],
    convertSet: async (entity, key, value, meta) => {
        const groupid = utils.isGroup(entity) ? entity.groupID : 0;
        utils.assertNumber(value);
        const sceneid = value;
        await entity.command("genScenes", "recall", { groupid, sceneid, transitionTime: 0xffff }, utils.getOptions(meta.mapped, entity));
        const addColorMode = (newState) => {
            if (newState.color_temp !== undefined) {
                newState.color_mode = constants.colorModeLookup[2];
            }
            else if (newState.color !== undefined) {
                if (newState.color.x !== undefined) {
                    newState.color_mode = constants.colorModeLookup[1];
                }
                else {
                    newState.color_mode = constants.colorModeLookup[0];
                }
            }
            return newState;
        };
        if (utils.isGroup(entity)) {
            const membersState = {};
            for (const member of entity.members) {
                let recalledState = utils.getSceneState(member, sceneid, groupid);
                if (recalledState) {
                    // add color_mode if saved state does not contain it
                    if (recalledState.color_mode === undefined) {
                        recalledState = addColorMode(recalledState);
                    }
                    Object.assign(recalledState, libColor.syncColorState(recalledState, meta.state, entity, meta.options));
                    membersState[member.getDevice().ieeeAddr] = recalledState;
                }
                else {
                    logger_1.logger.warning(`Unknown scene was recalled for ${member.getDevice().ieeeAddr}, can't restore state.`, NS);
                    membersState[member.getDevice().ieeeAddr] = {};
                }
            }
            logger_1.logger.info("Successfully recalled group scene", NS);
            return { membersState };
        }
        let recalledState = utils.getSceneState(entity, sceneid, groupid);
        if (recalledState) {
            // add color_mode if saved state does not contain it
            if (recalledState.color_mode === undefined) {
                recalledState = addColorMode(recalledState);
            }
            Object.assign(recalledState, libColor.syncColorState(recalledState, meta.state, entity, meta.options));
            logger_1.logger.info("Successfully recalled scene", NS);
            return { state: recalledState };
        }
        logger_1.logger.warning(`Unknown scene was recalled for ${entity.deviceIeeeAddress}, can't restore state.`, NS);
        return { state: {} };
    },
};
exports.scene_add = {
    key: ["scene_add"],
    convertSet: async (entity, key, value, meta) => {
        utils.assertObject(value);
        utils.assertNumber(value.ID, "ID");
        if (value.color_temp != null && value.color != null) {
            throw new Error(`Don't specify both 'color_temp' and 'color'`);
        }
        const isGroup = utils.isGroup(entity);
        const groupid = isGroup ? entity.groupID : value.group_id != null ? value.group_id : 0;
        const sceneid = value.ID;
        const scenename = value.name;
        const transtime = value.transition != null ? value.transition : 0;
        if (groupid === 0 && sceneid === 0) {
            // From Zigbee spec:
            // "Scene identifier 0x00, along with group identifier 0x0000, is reserved for the global scene used by the OnOff cluster"
            throw new Error("Scene ID 0 cannot be used with group ID 0 (reserved).");
        }
        const state = {};
        const extensionfieldsets = [];
        for (const attribute of Object.keys(value)) {
            let val = value[attribute];
            if (attribute === "state") {
                extensionfieldsets.push({ clstId: 6, len: 1, extField: [val.toLowerCase() === "on" ? 1 : 0] });
                state.state = val.toUpperCase();
            }
            else if (attribute === "brightness") {
                extensionfieldsets.push({ clstId: 8, len: 1, extField: [val] });
                state.brightness = val;
            }
            else if (attribute === "position") {
                const invert = utils.getMetaValue(entity, meta.mapped, "coverInverted", "allEqual", false)
                    ? !meta.options.invert_cover
                    : meta.options.invert_cover;
                extensionfieldsets.push({ clstId: 258, len: 1, extField: [invert ? 100 - val : val] });
                state.position = val;
            }
            else if (attribute === "color_temp") {
                /*
                 * ZCL version 7 added support for ColorTemperatureMireds
                 *
                 * Currently no devices seem to support this, so always fallback to XY conversion. In the future if a device
                 * supports this, or other features get added this the following commit contains an implementation:
                 * https://github.com/Koenkk/zigbee-herdsman-converters/pull/1837/commits/c22175b946b83230ce4e711c2a3796cf2029e78f
                 *
                 * Conversion to XY is allowed according to the ZCL:
                 * `Since there is a direct relation between ColorTemperatureMireds and XY,
                 *  color temperature, if supported, is stored as XY in the scenes table.`
                 *
                 * See https://github.com/Koenkk/zigbee2mqtt/issues/4926#issuecomment-735947705
                 */
                const [colorTempMin, colorTempMax] = light.findColorTempRange(entity);
                val = light.clampColorTemp(val, colorTempMin, colorTempMax);
                const xy = libColor.ColorXY.fromMireds(val);
                const xScaled = utils.mapNumberRange(xy.x, 0, 1, 0, 65535);
                const yScaled = utils.mapNumberRange(xy.y, 0, 1, 0, 65535);
                extensionfieldsets.push({ clstId: 768, len: 4, extField: [xScaled, yScaled] });
                state.color_mode = constants.colorModeLookup[2];
                state.color_temp = val;
            }
            else if (attribute === "color") {
                try {
                    val = JSON.parse(val);
                }
                catch {
                    /* empty */
                }
                const newColor = libColor.Color.fromConverterArg(val);
                if (newColor.isXY()) {
                    const xScaled = utils.mapNumberRange(newColor.xy.x, 0, 1, 0, 65535);
                    const yScaled = utils.mapNumberRange(newColor.xy.y, 0, 1, 0, 65535);
                    extensionfieldsets.push({
                        clstId: 768,
                        len: 4,
                        extField: [xScaled, yScaled],
                    });
                    state.color_mode = constants.colorModeLookup[1];
                    state.color = newColor.xy.toObject();
                }
                else if (newColor.isHSV()) {
                    const hsvCorrected = newColor.hsv.colorCorrected(meta);
                    if (utils.getMetaValue(entity, meta.mapped, "supportsEnhancedHue", "allEqual", false)) {
                        const hScaled = utils.mapNumberRange(hsvCorrected.hue, 0, 360, 0, 65535);
                        const sScaled = utils.mapNumberRange(hsvCorrected.saturation, 0, 100, 0, 254);
                        extensionfieldsets.push({
                            clstId: 768,
                            len: 13,
                            extField: [0, 0, hScaled, sScaled, 0, 0, 0, 0],
                        });
                    }
                    else {
                        // The extensionFieldSet is always EnhancedCurrentHue according to ZCL
                        // When the bulb or all bulbs in a group do not support enhanchedHue,
                        const colorXY = hsvCorrected.toXY();
                        const xScaled = utils.mapNumberRange(colorXY.x, 0, 1, 0, 65535);
                        const yScaled = utils.mapNumberRange(colorXY.y, 0, 1, 0, 65535);
                        extensionfieldsets.push({
                            clstId: 768,
                            len: 4,
                            extField: [xScaled, yScaled],
                        });
                    }
                    state.color_mode = constants.colorModeLookup[0];
                    state.color = newColor.hsv.toObject(false, false);
                }
            }
        }
        /*
         * Remove scene first
         *
         * Multiple add scene calls will result in the current and previous
         * payloads to be merged. Resulting in unexpected behavior when
         * trying to replace a scene.
         *
         * We accept a SUCCESS or NOT_FOUND as a result of the remove call.
         */
        const removeresp = await entity.command("genScenes", "remove", { groupid, sceneid }, utils.getOptions(meta.mapped, entity));
        if (isGroup || (utils.isObject(removeresp) && (removeresp.status === 0 || removeresp.status === 133 || removeresp.status === 139))) {
            const addSceneCommand = Number.isInteger(transtime) ? "add" : "enhancedAdd";
            const commandTransitionTime = addSceneCommand === "enhancedAdd" ? Math.floor(transtime * 10) : transtime;
            const response = await entity.command("genScenes", addSceneCommand, { groupid, sceneid, scenename: "", transtime: commandTransitionTime, extensionfieldsets }, utils.getOptions(meta.mapped, entity));
            if (isGroup) {
                if (meta.membersState) {
                    for (const member of entity.members) {
                        utils.saveSceneState(member, sceneid, groupid, state, scenename);
                    }
                }
            }
            else {
                utils.assertObject(response);
                if (response.status === 0) {
                    utils.saveSceneState(entity, sceneid, groupid, state, scenename);
                }
                else {
                    throw new Error(`Scene add not successful ('${zigbee_herdsman_1.Zcl.Status[response.status]}')`);
                }
            }
        }
        else {
            const status = utils.isObject(removeresp) ? zigbee_herdsman_1.Zcl.Status[removeresp.status] : "unknown";
            throw new Error(`Scene add unable to remove existing scene ('${status}')`);
        }
        logger_1.logger.info("Successfully added scene", NS);
        return { state: {} };
    },
};
exports.scene_remove = {
    key: ["scene_remove"],
    convertSet: async (entity, key, value, meta) => {
        const isGroup = utils.isGroup(entity);
        utils.assertNumber(value);
        const groupid = isGroup ? entity.groupID : 0;
        const sceneid = value;
        const response = await entity.command("genScenes", "remove", { groupid, sceneid }, utils.getOptions(meta.mapped, entity));
        if (isGroup) {
            if (meta.membersState) {
                for (const member of entity.members) {
                    utils.deleteSceneState(member, sceneid, groupid);
                }
            }
        }
        else if (response.status === 0) {
            utils.deleteSceneState(entity, sceneid, groupid);
        }
        else {
            throw new Error(`Scene remove not successful ('${zigbee_herdsman_1.Zcl.Status[response.status]}')`);
        }
        logger_1.logger.info("Successfully removed scene", NS);
    },
};
exports.scene_remove_all = {
    key: ["scene_remove_all"],
    convertSet: async (entity, key, value, meta) => {
        const groupid = utils.isGroup(entity) ? entity.groupID : 0;
        // In case `entity` is a group, the response is `undefined`, mock it.
        const response = (await entity.command("genScenes", "removeAll", { groupid }, utils.getOptions(meta.mapped, entity))) ?? { status: 0 };
        utils.assertObject(response);
        if (utils.isGroup(entity)) {
            if (meta.membersState) {
                for (const member of entity.members) {
                    utils.deleteSceneState(member);
                }
            }
        }
        else if (response.status === 0) {
            utils.deleteSceneState(entity);
        }
        else {
            throw new Error(`Scene remove all not successful ('${zigbee_herdsman_1.Zcl.Status[response.status]}')`);
        }
        logger_1.logger.info("Successfully removed all scenes", NS);
    },
};
exports.scene_rename = {
    key: ["scene_rename"],
    convertSet: (entity, key, value, meta) => {
        utils.assertObject(value);
        const isGroup = utils.isGroup(entity);
        const sceneid = value.ID;
        const scenename = value.name;
        const groupid = isGroup ? entity.groupID : value.group_id != null ? value.group_id : 0;
        if (isGroup) {
            if (meta.membersState) {
                for (const member of entity.members) {
                    const state = utils.getSceneState(member, sceneid, groupid);
                    if (state) {
                        utils.saveSceneState(member, sceneid, groupid, state, scenename);
                    }
                }
            }
        }
        else {
            const state = utils.getSceneState(entity, sceneid, groupid);
            if (!state) {
                throw new Error("No such scene in device meta data");
            }
            utils.saveSceneState(entity, sceneid, groupid, state, scenename);
        }
        logger_1.logger.info("Successfully renamed scene", NS);
    },
};
// #endregion
// #region Ignore converters
exports.ignore_transition = {
    key: ["transition"],
    convertSet: async (entity, key, value, meta) => { },
};
exports.ignore_rate = {
    key: ["rate"],
    convertSet: async (entity, key, value, meta) => { },
};
// #endregion
//# sourceMappingURL=toZigbee.js.map