UNPKG

zigbee-herdsman-converters

Version:

Collection of device converters to be used with zigbee-herdsman

869 lines (868 loc) • 216 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.livolo_cover_state = exports.livolo_dimmer_level = exports.livolo_switch_on_off = exports.livolo_socket_switch_on_off = exports.elko_local_temperature_calibration = exports.elko_relay_state = exports.elko_power_status = exports.humidity = exports.temperature = 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.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.eurotronic_error_status = exports.eurotronic_host_flags = exports.EMIZB_132_mode = exports.tuya_led_controller = exports.tuya_led_control = exports.easycode_auto_relock = exports.namron_thermostat_child_lock = exports.namron_thermostat = exports.ZMCSW032D_cover_position = exports.danfoss_multimaster_role = exports.danfoss_system_status_water = exports.danfoss_system_status_code = exports.danfoss_floor_max_setpoint = exports.danfoss_floor_min_setpoint = exports.danfoss_floor_sensor_mode = exports.danfoss_room_status_code = exports.danfoss_output_status = exports.danfoss_regulation_setpoint_offset = exports.danfoss_adaptation_control = exports.danfoss_adaptation_settings = exports.danfoss_adaptation_status = exports.danfoss_preheat_status = exports.danfoss_load_estimate = exports.danfoss_load_room_mean = exports.danfoss_load_balancing_enable = exports.danfoss_window_open_external = exports.danfoss_window_open_internal = exports.danfoss_window_open_feature = exports.danfoss_trigger_time = exports.danfoss_day_of_week = exports.danfoss_heat_required = exports.danfoss_heat_available = exports.danfoss_algorithm_scale_factor = exports.danfoss_viewing_direction = exports.danfoss_radiator_covered = exports.danfoss_external_measured_room_sensor = exports.danfoss_thermostat_vertical_orientation = exports.danfoss_mounted_mode_control = exports.danfoss_mounted_mode_active = exports.danfoss_thermostat_occupied_heating_setpoint_scheduled = exports.danfoss_thermostat_occupied_heating_setpoint = exports.hue_wall_switch_device_mode = exports.kmpcil_res005_on_off = exports.tuya_relay_din_led_indicator = exports.SPZ01_power_outage_memory = exports.STS_PRS_251_beep = exports.LS21001_alert_behaviour = exports.ZigUP_lock = exports.livolo_cover_options = exports.livolo_cover_position = void 0; exports.schneider_dimmer_mode = exports.schneider_pilot_mode = exports.idlock_relock_enabled = exports.idlock_lock_mode = exports.idlock_service_mode = exports.idlock_rfid_enable = exports.idlock_master_pin_mode = exports.dawondns_only_off = exports.viessmann_assembly_mode = exports.viessmann_window_open_force = exports.viessmann_window_open = exports.TS0210_sensitivity = exports.ZM35HQ_attr = exports.moes_cover_calibration = exports.tuya_cover_reversal = exports.tuya_cover_calibration = exports.ts0216_alarm = exports.ts0216_volume = exports.ts0216_duration = exports.TS0003_curtain_switch = exports.scene_rename = exports.scene_remove_all = exports.scene_remove = exports.scene_add = exports.scene_recall = exports.scene_store = exports.heiman_ir_remote = exports.ts0201_temperature_humidity_alarm = exports.power_source = exports.diyruz_zintercom_config = exports.diyruz_airsense_config = exports.diyruz_geiger_config = exports.TYZB01_on_off = exports.diyruz_freepad_on_off_config = exports.legrand_power_alarm = exports.legrand_pilot_wire_mode = exports.legrand_device_mode = exports.bticino_4027C_cover_position = exports.bticino_4027C_cover_state = exports.tint_scene = exports.ptvo_switch_analog_input = exports.ptvo_switch_uart = exports.ptvo_switch_trigger = exports.DTB190502A1_LED = exports.stelpro_thermostat_outdoor_temperature = exports.eurotronic_mirror_display = exports.eurotronic_child_lock = exports.eurotronic_trv_mode = exports.eurotronic_valve_position = exports.eurotronic_current_heating_setpoint = void 0; exports.TS110E_light_onoff_brightness = exports.TS110E_onoff_brightness = exports.TS110E_options = exports.ptvo_switch_light_brightness = exports.light_onoff_restorable_brightness = exports.ignore_rate = exports.ignore_transition = exports.led_on_motion = exports.tuya_operation_mode = exports.sihas_set_people = exports.wiser_sed_thermostat_keypad_lockout = exports.wiser_sed_thermostat_local_temperature_calibration = exports.wiser_sed_occupied_heating_setpoint = exports.wiser_sed_zone_mode = exports.wiser_vact_calibrate_valve = exports.wiser_zone_mode = exports.wiser_hact_config = exports.wiser_fip_setting = exports.schneider_thermostat_keypad_lockout = exports.schneider_thermostat_pi_heating_demand = exports.schneider_thermostat_control_sequence_of_operation = exports.schneider_thermostat_occupied_heating_setpoint = exports.schneider_thermostat_system_mode = exports.schneider_temperature_measured_value = exports.wiser_dimmer_mode = 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 modernExtend_1 = require("../lib/modernExtend"); const globalStore = __importStar(require("../lib/store")); const utils = __importStar(require("../lib/utils")); const NS = "zhc:tz"; const manufacturerOptions = { sunricher: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.SHENZHEN_SUNRICHER_TECHNOLOGY_LTD }, lumi: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.LUMI_UNITED_TECHOLOGY_LTD_SHENZHEN, disableDefaultResponse: true }, eurotronic: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.NXP_SEMICONDUCTORS }, danfoss: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.DANFOSS_A_S }, hue: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V }, 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 }, legrand: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.LEGRAND_GROUP, disableDefaultResponse: true }, viessmann: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.VIESSMANN_ELEKTRONIK_GMBH }, }; 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 Error("The on_time value must be a number!"); } if (typeof offWaitTime !== "number") { throw Error("The off_wait_time value must be a number!"); } const payload = { 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) => { // biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress` let command; const newColor = libColor.Color.fromConverterArg(value); const newState = {}; const zclData = { transtime: utils.getTransition(entity, key, meta).time }; const supportsHueAndSaturation = utils.getMetaValue(entity, meta.mapped, "supportsHueAndSaturation", "allEqual", true); const supportsEnhancedHue = utils.getMetaValue(entity, meta.mapped, "supportsEnhancedHue", "allEqual", true); if (newColor.isHSV() && !supportsHueAndSaturation) { // The color we got is HSV but the bulb does not support Hue/Saturation mode throw new Error("This light does not support Hue/Saturation, please use X/Y instead."); } if (newColor.isRGB() || newColor.isXY()) { // Convert RGB to XY color mode because Zigbee doesn't support RGB (only x/y and hue/saturation) const xy = newColor.isRGB() ? newColor.rgb.gammaCorrected().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(); zclData.colorx = utils.mapNumberRange(xy.x, 0, 1, 0, 65535); zclData.colory = utils.mapNumberRange(xy.y, 0, 1, 0, 65535); command = "moveToColor"; } else if (newColor.isHSV()) { const hsv = newColor.hsv; const hsvCorrected = hsv.colorCorrected(meta); newState.color_mode = constants.colorModeLookup[0]; newState.color = hsv.toObject(false); if (hsv.hue !== null) { if (supportsEnhancedHue) { zclData.enhancehue = utils.mapNumberRange(hsvCorrected.hue, 0, 360, 0, 65535); } else { zclData.hue = utils.mapNumberRange(hsvCorrected.hue, 0, 360, 0, 254); } // @ts-expect-error ignore zclData.direction = value.direction || 0; } if (hsv.saturation != null) { zclData.saturation = utils.mapNumberRange(hsvCorrected.saturation, 0, 100, 0, 254); } if (hsv.value !== null) { // fallthrough to genLevelCtrl // @ts-expect-error ignore value.brightness = utils.mapNumberRange(hsvCorrected.value, 0, 100, 0, 254); } if (hsv.hue !== null && hsv.saturation !== null) { if (supportsEnhancedHue) { command = "enhancedMoveToHueAndSaturation"; } else { command = "moveToHueAndSaturation"; } } else if (hsv.hue !== null) { if (supportsEnhancedHue) { command = "enhancedMoveToHue"; } else { command = "moveToHue"; } } else if (hsv.saturation !== null) { command = "moveToSaturation"; } } if (utils.isObject(value) && value.brightness != null) { await entity.command("genLevelCtrl", "moveToLevelWithOnOff", { level: Number(value.brightness), transtime: utils.getTransition(entity, key, meta).time }, utils.getOptions(meta.mapped, entity)); } await entity.command("lightingColorCtrl", command, zclData, utils.getOptions(meta.mapped, entity)); 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); // biome-ignore lint/style/noParameterAssign: ignored using `--suppress` value = utils .mapNumberRange(value, 0, 100, colorTempMin != null ? colorTempMin : 154, colorTempMax != null ? colorTempMax : 500) .toString(); } if (utils.isString(value) && value in preset) { // biome-ignore lint/style/noParameterAssign: ignored using `--suppress` value = utils.getFromLookup(value, preset); } // biome-ignore lint/style/noParameterAssign: ignored using `--suppress` value = Number(value); // ensure value within range utils.assertNumber(value); // biome-ignore lint/style/noParameterAssign: ignored using `--suppress` value = light.clampColorTemp(value, colorTempMin, colorTempMax); const payload = { colortemp: value, transtime: utils.getTransition(entity, key, meta).time }; await entity.command("lightingColorCtrl", "moveToColorTemp", payload, 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; } globalStore.putValue(entity, "panelStatus", panelStatus); const payload = { panelstatus: panelStatus, secondsremain: 0, audiblenotif: 0, 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); // biome-ignore lint/style/noParameterAssign: ignored using `--suppress` 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 (e) { if (e.message.includes("UNSUPPORTED_ATTRIBUTE")) { throw new Error("Got `UNSUPPORTED_ATTRIBUTE` error, device does not support power on behaviour"); } throw e; } 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: pincode }, 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); // biome-ignore lint/style/noParameterAssign: ignored using `--suppress` value = value.toLowerCase(); if (value === "stop") { await entity.command("genLevelCtrl", "stop", {}, utils.getOptions(meta.mapped, entity)); return; } const lookup = { open: 100, close: 0 }; // biome-ignore lint/style/noParameterAssign: ignored using `--suppress` 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).toString(), transtime: 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"); 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 { 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); // biome-ignore lint/style/noParameterAssign: ignored using `--suppress` value = value.toLowerCase(); await entity.command("closuresWindowCovering", utils.getFromLookup(value, 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); // biome-ignore lint/style/noParameterAssign: ignored using `--suppress` 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 { // biome-ignore lint/style/noParameterAssign: ignored using `--suppress` 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); 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 }); } // onTransitionTime - range 0x0000 to 0xffff - optional // 0xffff = use onOffTransitionTime if (value.on_transition_time != null) { let onTransitionTimeValue = value.on_transition_time; 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: 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 === "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: 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") { // biome-ignore lint/style/noParameterAssign: ignored using `--suppress` 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 [ "ballast_status", "min_level", "max_level", "power_on_level", "power_on_fade_time", "intrinsic_ballast_factor", "ballast_factor_adjustment", "lamp_quantity", "lamp_type", "lamp_manufacturer", "lamp_rated_hours", "lamp_burn_hours", "lamp_alarm_mode", "lamp_burn_hours_trip_point", ]) { try { // @ts-expect-error ignore result = { ...result, ...(await entity.read("lightingBallastCfg", [utils.toCamelCase(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"; // biome-ignore lint/style/noParameterAssign: ignored using `--suppress` value = Number(value); utils.assertNumber(value, key); const mode = value > 0 ? 0 : 1; const transition = utils.getTransition(entity, key, meta).time; const payload = { stepmode: mode, stepsize: Math.abs(value), transtime: transition }; await entity.command("genLevelCtrl", command, payload, 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) {