zigbee-herdsman-converters
Version:
Collection of device converters to be used with zigbee-herdsman
869 lines (868 loc) • 216 kB
JavaScript
"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) {