UNPKG

zigbee-herdsman-converters

Version:

Collection of device converters to be used with zigbee-herdsman

936 lines 131 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_keypad_lockout = exports.thermostat_temperature_display_mode = exports.thermostat_programming_operation_mode = exports.thermostat_control_sequence_of_operation = exports.acova_thermostat_system_mode = 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.ias_max_duration = 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.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 = exports.thermostat_temperature_setpoint_hold_duration = exports.thermostat_temperature_setpoint_hold = void 0; exports.ptvo_switch_light_brightness = exports.light_onoff_restorable_brightness = exports.ignore_rate = exports.ignore_transition = exports.sihas_set_people = exports.dawondns_only_off = exports.TS0003_curtain_switch = exports.scene_rename = exports.scene_remove_all = exports.scene_remove = exports.scene_add = exports.scene_recall = exports.scene_store = exports.power_source = exports.TYZB01_on_off = exports.tint_scene = exports.ptvo_switch_analog_input = exports.ptvo_switch_uart = exports.ptvo_switch_trigger = exports.ZMCSW032D_cover_position = 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"; const manufacturerOptions = { lumi: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.LUMI_UNITED_TECHOLOGY_LTD_SHENZHEN, disableDefaultResponse: true }, ikea: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.IKEA_OF_SWEDEN }, sinope: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.SINOPE_TECHNOLOGIES }, tint: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.MUELLER_LICHT_INTERNATIONAL_INC }, }; 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); 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, optionsMask: 0, optionsOverride: 0 }, 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, optionsMask: 0, optionsOverride: 0 }, utils.getOptions(meta.mapped, entity)); } else { const hue = utils.mapNumberRange(hsvCorrected.hue, 0, 360, 0, 254); await entity.command("lightingColorCtrl", "moveToHueAndSaturation", { transtime, hue, saturation, optionsMask: 0, optionsOverride: 0 }, 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, optionsMask: 0, optionsOverride: 0 }, utils.getOptions(meta.mapped, entity)); } else { const hue = utils.mapNumberRange(hsvCorrected.hue, 0, 360, 0, 254); await entity.command("lightingColorCtrl", "moveToHue", { transtime, hue, direction, optionsMask: 0, optionsOverride: 0 }, 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, optionsMask: 0, optionsOverride: 0 }, 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, optionsMask: 0, optionsOverride: 0 }, 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 }; 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, optionsMask: 0, optionsOverride: 0 }, 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.ias_max_duration = { key: ["max_duration"], convertSet: async (entity, key, value, meta) => { await entity.write("ssIasWd", { maxDuration: value }); return { state: { max_duration: value } }; }, convertGet: async (entity, key, meta) => { await entity.read("ssIasWd", ["maxDuration"]); }, }; 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"], convertSet: async (entity, key, value, meta) => { const lookup = { open: "upOpen", close: "downClose", stop: "stop", on: "upOpen", off: "downClose", }; utils.assertString(value, key); await entity.command("closuresWindowCovering", utils.getFromLookup(value.toLowerCase(), lookup), {}, 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 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, optionsMask: 0, optionsOverride: 0 }, 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) => { if (value === "stop" || value === 0) { await entity.command("genLevelCtrl", "stop", { optionsMask: 0, optionsOverride: 0 }, 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), optionsMask: 0, optionsOverride: 0 }, utils.getOptions(meta.mapped, entity)); }, }; exports.light_col