UNPKG

zigbee-herdsman-converters

Version:

Collection of device converters to be used with zigbee-herdsman

1,098 lines 64.1 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.definitions = void 0; const zigbee_herdsman_1 = require("zigbee-herdsman"); const fz = __importStar(require("../converters/fromZigbee")); const tz = __importStar(require("../converters/toZigbee")); const exposes = __importStar(require("../lib/exposes")); const legacy = __importStar(require("../lib/legacy")); const m = __importStar(require("../lib/modernExtend")); const reporting = __importStar(require("../lib/reporting")); const utils = __importStar(require("../lib/utils")); const utils_1 = require("../lib/utils"); const e = exposes.presets; const ea = exposes.access; const switchTypesList = { switch: 0x00, "multi-click": 0x02, }; const tzLocal = { tirouter: { key: ["transmit_power"], convertSet: async (entity, key, value, meta) => { await entity.write("genBasic", { 4919: { value, type: 0x28 } }); return { state: { [key]: value } }; }, convertGet: async (entity, key, meta) => { await entity.read("genBasic", [0x1337]); }, }, multi_zig_sw_switch_type: { key: ["switch_type_1", "switch_type_2", "switch_type_3", "switch_type_4"], convertGet: async (entity, key, meta) => { await entity.read("genOnOffSwitchCfg", ["switchType"]); }, convertSet: async (entity, key, value, meta) => { const data = (0, utils_1.getFromLookup)(value, switchTypesList); const payload = { switchType: data }; await entity.write("genOnOffSwitchCfg", payload); return { state: { [`${key}`]: value } }; }, }, ptvo_on_off: { key: ["state"], convertSet: async (entity, key, value, meta) => { return await tz.on_off.convertSet(entity, key, value, meta); }, convertGet: async (entity, key, meta) => { const cluster = "genOnOff"; if ((0, utils_1.isEndpoint)(entity) && (entity.supportsInputCluster(cluster) || entity.supportsOutputCluster(cluster))) { return await tz.on_off.convertGet(entity, key, meta); } return; }, }, // biome-ignore lint/style/useNamingConvention: ignored using `--suppress` ZigUP_lock: { key: ["led"], convertSet: async (entity, key, value, meta) => { const lookup = { off: "lockDoor", on: "unlockDoor", toggle: "toggleDoor" }; await entity.command("closuresDoorLock", utils.getFromLookup(value, lookup), { pincodevalue: Buffer.alloc(0) }); }, }, }; const fzLocal = { tirouter: { cluster: "genBasic", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const result = { linkquality: msg.linkquality }; if (msg.data["4919"]) result.transmit_power = msg.data["4919"]; return result; }, }, humidity2: { cluster: "msRelativeHumidity", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { // multi-endpoint version based on the stastard onverter 'fz.humidity' let humidity = msg.data.measuredValue / 100.0; humidity = (0, utils_1.calibrateAndPrecisionRoundOptions)(humidity, options, "humidity"); // https://github.com/Koenkk/zigbee2mqtt/issues/798 // Sometimes the sensor publishes non-realistic vales, it should only publish message // in the 0 - 100 range, don't produce messages beyond these values. if (humidity >= 0 && humidity <= 100) { const multiEndpoint = model.meta?.multiEndpoint; const property = multiEndpoint ? (0, utils_1.postfixWithEndpointName)("humidity", msg, model, meta) : "humidity"; return { [property]: humidity }; } }, }, illuminance2: { cluster: "msIlluminanceMeasurement", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { // multi-endpoint version based on the stastard onverter 'fz.illuminance' const illuminance = msg.data.measuredValue; let illuminanceLux = illuminance === 0 ? 0 : 10 ** ((illuminance - 1) / 10000); illuminanceLux = (0, utils_1.calibrateAndPrecisionRoundOptions)(illuminanceLux, options, "illuminance"); const multiEndpoint = model.meta?.multiEndpoint; const property1 = multiEndpoint ? (0, utils_1.postfixWithEndpointName)("illuminance", msg, model, meta) : "illuminance"; return { [property1]: illuminanceLux }; }, }, pressure2: { cluster: "msPressureMeasurement", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { // multi-endpoint version based on the stastard onverter 'fz.pressure' let pressure = 0; if (msg.data.scaledValue !== undefined) { const scale = msg.endpoint.getClusterAttributeValue("msPressureMeasurement", "scale"); pressure = msg.data.scaledValue / 10 ** scale / 100.0; // convert to hPa } else { pressure = msg.data.measuredValue; } pressure = (0, utils_1.calibrateAndPrecisionRoundOptions)(pressure, options, "pressure"); const multiEndpoint = model.meta?.multiEndpoint; const property = multiEndpoint ? (0, utils_1.postfixWithEndpointName)("pressure", msg, model, meta) : "pressure"; return { [property]: pressure }; }, }, multi_zig_sw_battery: { cluster: "genPowerCfg", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const voltage = msg.data.batteryVoltage * 100; const battery = (voltage - 2200) / 8; return { battery: battery > 100 ? 100 : battery, voltage: voltage }; }, }, multi_zig_sw_switch_buttons: { cluster: "genMultistateInput", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const button = (0, utils_1.getKey)(model.endpoint?.(msg.device) ?? {}, msg.endpoint.ID); const actionLookup = { 0: "release", 1: "single", 2: "double", 3: "triple", 4: "hold" }; const value = msg.data.presentValue; const action = actionLookup[value]; return { action: `${button}_${action}` }; }, }, multi_zig_sw_switch_config: { cluster: "genOnOffSwitchCfg", type: ["readResponse", "attributeReport"], convert: (model, msg, publish, options, meta) => { const channel = (0, utils_1.getKey)(model.endpoint?.(msg.device) ?? {}, msg.endpoint.ID); const { switchType } = msg.data; return { [`switch_type_${channel}`]: (0, utils_1.getKey)(switchTypesList, switchType) }; }, }, acw02_clean_status: { cluster: "genOnOff", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { if (msg.endpoint.ID === 7 && Object.hasOwn(msg.data, "onOff")) { return { filter_clean_status: msg.data["onOff"] === 1 ? "ON" : "OFF" }; } }, }, acw02_error_status: { cluster: "genOnOff", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { if (msg.endpoint.ID === 9 && Object.hasOwn(msg.data, "onOff")) { return { ac_error_status: msg.data["onOff"] === 1 ? "ON" : "OFF" }; } }, }, acw02_error_text: { cluster: "genBasic", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { if (msg.endpoint.ID === 1 && msg.data["locationDesc"] !== undefined) { let errorText = ""; const locationDesc = msg.data["locationDesc"]; if (typeof locationDesc === "string") { errorText = locationDesc; } else if (Array.isArray(locationDesc) && locationDesc.length > 0) { const textLength = locationDesc[0]; const textData = locationDesc.slice(1, 1 + textLength); errorText = String.fromCharCode(...textData); } return { error_text: errorText.trim() }; } }, }, acw02_thermostat: { cluster: "hvacThermostat", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const result = {}; if (Object.hasOwn(msg.data, "localTemp")) { result.local_temperature = msg.data.localTemp / 100; } if (Object.hasOwn(msg.data, "runningMode")) { const modeMap = { 0: "idle", 3: "cool", 4: "heat", 7: "fan_only", }; result.running_state = modeMap[msg.data.runningMode] || "idle"; } if (Object.hasOwn(msg.data, "systemMode")) { const sysModeMap = { 0: "off", 1: "auto", 3: "cool", 4: "heat", 7: "fan_only", 8: "dry", }; result.system_mode = sysModeMap[msg.data.systemMode] || "off"; } if (Object.hasOwn(msg.data, "occupiedHeatingSetpoint")) { result.occupied_heating_setpoint = msg.data.occupiedHeatingSetpoint / 100; } else if (Object.hasOwn(msg.data, "occupiedCoolingSetpoint")) { result.occupied_heating_setpoint = msg.data.occupiedCoolingSetpoint / 100; } return result; }, }, ZigUP: { cluster: "genOnOff", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const lookup = { "0": "timer", "1": "key", "2": "dig-in", }; let ds18b20Id = null; let ds18b20Value = null; if (msg.data["41368"]) { ds18b20Id = msg.data["41368"].split(":")[0]; ds18b20Value = utils.precisionRound(Number.parseFloat(msg.data["41368"].split(":")[1]), 2); } return { state: msg.data.onOff === 1 ? "ON" : "OFF", cpu_temperature: utils.precisionRound(msg.data["41361"], 2), external_temperature: utils.precisionRound(msg.data["41362"], 1), external_humidity: utils.precisionRound(msg.data["41363"], 1), s0_counts: msg.data["41364"], adc_volt: utils.precisionRound(msg.data["41365"], 3), dig_input: msg.data["41366"], reason: lookup[msg.data["41367"]], [`${ds18b20Id}`]: ds18b20Value, }; }, }, // biome-ignore lint/style/useNamingConvention: ignored using `--suppress` CC2530ROUTER_led: { cluster: "genOnOff", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { return { led: msg.data.onOff === 1 }; }, }, // biome-ignore lint/style/useNamingConvention: ignored using `--suppress` CC2530ROUTER_meta: { cluster: "genBinaryValue", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const data = msg.data; return { description: data.description, type: data.inactiveText, rssi: data.presentValue, }; }, }, // biome-ignore lint/style/useNamingConvention: ignored using `--suppress` DNCKAT_S00X_buttons: { cluster: "genOnOff", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const action = msg.data.onOff === 1 ? "release" : "hold"; return { action: (0, utils_1.postfixWithEndpointName)(action, msg, model, meta) }; }, }, }; function ptvoGetMetaOption(device, key, defaultValue) { if (!utils.isDummyDevice(device) && device.meta) { const value = device.meta[key]; if (value === undefined) { return defaultValue; } return value; } return defaultValue; } function ptvoSetMetaOption(device, key, value) { if (device != null && key != null) { device.meta[key] = value; } } function ptvoAddStandardExposes(endpoint, expose, options, deviceOptions) { const epId = endpoint.ID; const epName = `l${epId}`; if (endpoint.supportsInputCluster("lightingColorCtrl")) { expose.push(e.light_brightness_colorxy().withEndpoint(epName)); options.exposed_onoff = true; options.exposed_analog = true; options.exposed_colorcontrol = true; } else if (endpoint.supportsInputCluster("genLevelCtrl")) { expose.push(e.light_brightness().withEndpoint(epName)); options.exposed_onoff = true; options.exposed_analog = true; options.exposed_levelcontrol = true; } if (endpoint.supportsInputCluster("genOnOff")) { if (!options.exposed_onoff) { expose.push(e.switch().withEndpoint(epName)); } } if (endpoint.supportsInputCluster("genAnalogInput") || endpoint.supportsOutputCluster("genAnalogInput")) { if (!options.exposed_analog) { options.exposed_analog = true; expose.push(e.text(epName, ea.ALL).withEndpoint(epName).withProperty(epName).withDescription("State or sensor value")); } } if (endpoint.supportsInputCluster("msTemperatureMeasurement")) { expose.push(e.temperature().withEndpoint(epName)); } if (endpoint.supportsInputCluster("msRelativeHumidity")) { expose.push(e.humidity().withEndpoint(epName)); } if (endpoint.supportsInputCluster("msPressureMeasurement")) { expose.push(e.pressure().withEndpoint(epName)); } if (endpoint.supportsInputCluster("msIlluminanceMeasurement")) { expose.push(e.illuminance().withEndpoint(epName)); } if (endpoint.supportsInputCluster("msCO2")) { expose.push(e.co2()); } if (endpoint.supportsInputCluster("pm25Measurement")) { expose.push(e.pm25()); } if (endpoint.supportsInputCluster("haElectricalMeasurement")) { // haElectricalMeasurement may expose only one value defined explicitly if (!(options.exposed_voltage || options.exposed_current || options.exposed_power)) { expose.push(e.voltage().withEndpoint(epName)); expose.push(e.current().withEndpoint(epName)); expose.push(e.power().withEndpoint(epName)); } } if (endpoint.supportsInputCluster("seMetering")) { if (!options.exposed_energy) { expose.push(e.energy().withEndpoint(epName)); } } if (endpoint.supportsInputCluster("genPowerCfg")) { deviceOptions.expose_battery = true; } if (endpoint.supportsInputCluster("genMultistateInput") || endpoint.supportsOutputCluster("genMultistateInput") || Object.hasOwn(endpoint.clusters, "genMultistateInput")) { deviceOptions.expose_action = true; } } exports.definitions = [ { /** @see https://github.com/Nerivec/silabs-firmware-builder/releases */ fingerprint: [ { modelID: "ZGA008", manufacturerName: "Aeotec", applicationVersion: 200 }, { modelID: "ZB-GW04", manufacturerName: "easyiot", applicationVersion: 200 }, { modelID: "ZB-GW04-1v1", manufacturerName: "easyiot", applicationVersion: 200 }, { modelID: "ZB-GW04-1v2", manufacturerName: "easyiot", applicationVersion: 200 }, { modelID: "ZBM-MG24", manufacturerName: "Inswift", applicationVersion: 200 }, { modelID: "SkyConnect", manufacturerName: "NabuCasa", applicationVersion: 200 }, { modelID: "ZBT-2", manufacturerName: "NabuCasa", applicationVersion: 200 }, { modelID: "SLZB-06M", manufacturerName: "SMLIGHT", applicationVersion: 200 }, { modelID: "SLZB-06MG24", manufacturerName: "SMLIGHT", applicationVersion: 200 }, { modelID: "SLZB-06MG26", manufacturerName: "SMLIGHT", applicationVersion: 200 }, { modelID: "SLZB-06MG26U", manufacturerName: "SMLIGHT", applicationVersion: 200 }, { modelID: "SLZB-07", manufacturerName: "SMLIGHT", applicationVersion: 200 }, { modelID: "SLZB-07MG24", manufacturerName: "SMLIGHT", applicationVersion: 200 }, { modelID: "DONGLE-E", manufacturerName: "SONOFF", applicationVersion: 200 }, { modelID: "Dongle-LMG21", manufacturerName: "SONOFF", applicationVersion: 200 }, { modelID: "Dongle-M", manufacturerName: "SONOFF", applicationVersion: 200 }, { modelID: "Dongle-PMG24", manufacturerName: "SONOFF", applicationVersion: 200 }, { modelID: "MGM240P", manufacturerName: "SparkFun", applicationVersion: 200 }, { modelID: "MGM24", manufacturerName: "TubesZB", applicationVersion: 200 }, { modelID: "BM24", manufacturerName: "TubesZB", applicationVersion: 200 }, ], model: "Silabs series 2 router", vendor: "Silabs", description: "Silabs series 2 adapter with router firmware", toZigbee: [tz.factory_reset], exposes: [ e .enum("reset", ea.SET, ["reset"]) .withDescription("Resets and launches the bootloader for flashing. If USB, ensure the device is already connected to the machine where you intend to flash it before triggering this."), ], extend: [m.linkQuality({ reporting: true })], // prevent timeout with tz.factory_reset (reboots adapter into bootloader, hence disconnected) // since this is the only tz, it's not a problem to disable this globally meta: { disableDefaultResponse: true }, }, { zigbeeModel: ["ti.router"], model: "ti.router", vendor: "Custom devices (DiY)", description: "Texas Instruments router", fromZigbee: [fzLocal.tirouter], toZigbee: [tzLocal.tirouter], exposes: [ e .numeric("transmit_power", ea.ALL) .withValueMin(-20) .withValueMax(20) .withValueStep(1) .withUnit("dBm") .withDescription("Transmit power, supported from firmware 20221102. The max for CC1352 is 20 dBm and 5 dBm for CC2652" + " (any higher value is converted to 5dBm)"), ], configure: async (device, coordinatorEndpoint) => { const endpoint = device.getEndpoint(8); const payload = [{ attribute: "zclVersion", minimumReportInterval: 0, maximumReportInterval: 3600, reportableChange: 0 }]; await reporting.bind(endpoint, coordinatorEndpoint, ["genBasic"]); await endpoint.configureReporting("genBasic", payload); }, }, { zigbeeModel: ["lumi.router"], model: "CC2530.ROUTER", vendor: "Custom devices (DiY)", description: "CC2530 router", fromZigbee: [fzLocal.CC2530ROUTER_led, fzLocal.CC2530ROUTER_meta], toZigbee: [tz.ptvo_switch_trigger], exposes: [e.binary("led", ea.STATE, true, false)], }, { zigbeeModel: ["cc2538.router.v1"], model: "CC2538.ROUTER.V1", vendor: "Custom devices (DiY)", description: "MODKAM stick CC2538 router", fromZigbee: [], toZigbee: [], exposes: [], }, { zigbeeModel: ["cc2538.router.v2"], model: "CC2538.ROUTER.V2", vendor: "Custom devices (DiY)", description: "MODKAM stick CC2538 router with temperature sensor", fromZigbee: [fz.device_temperature], toZigbee: [], exposes: [e.device_temperature()], }, { zigbeeModel: ["ptvo.switch"], model: "ptvo.switch", vendor: "Custom devices (DiY)", description: "Multi-functional device", fromZigbee: [ fz.battery, fz.on_off, fz.ptvo_multistate_action, fz.ptvo_switch_uart, fz.ptvo_switch_analog_input, fz.brightness, fz.temperature, fzLocal.humidity2, fzLocal.pressure2, fzLocal.illuminance2, fz.electrical_measurement, fz.metering, fz.co2, fz.color_colortemp, ], toZigbee: [ tz.ptvo_switch_trigger, tz.ptvo_switch_uart, tz.ptvo_switch_analog_input, tz.ptvo_switch_light_brightness, tzLocal.ptvo_on_off, tz.light_color, ], exposes: (device, options) => { const expose = []; const exposeDeviceOptions = {}; const deviceConfig = ptvoGetMetaOption(device, "device_config", ""); if (deviceConfig === "" || utils.isDummyDevice(device)) { if (!utils.isDummyDevice(device)) { for (const endpoint of device.endpoints) { const exposeEpOptions = {}; ptvoAddStandardExposes(endpoint, expose, exposeEpOptions, exposeDeviceOptions); } } else { // fallback code for (let epId = 1; epId <= 8; epId++) { const epName = `l${epId}`; expose.push(e.text(epName, ea.ALL).withEndpoint(epName).withProperty(epName).withDescription("State or sensor value")); expose.push(e.switch().withEndpoint(epName)); } } } else { // device configuration description from a device const deviceConfigArray = deviceConfig.split(/[\r\n]+/); const allEndpoints = {}; const allEndpointsSorted = []; // biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress` let epConfig; for (let i = 0; i < deviceConfigArray.length; i++) { epConfig = deviceConfigArray[i]; const matches = epConfig.match(/^([0-9A-F]+)/); if (!matches || matches.length === 0) { continue; } const epId = Number.parseInt(matches[0], 16); const epId2 = epId < 10 ? `0${epId}` : epId; epConfig = epConfig.replace(/^[0-9A-F]+/, epId2); allEndpoints[epId] = "1"; allEndpointsSorted.push(epConfig); } for (const endpoint of device.endpoints) { if (allEndpoints[endpoint.ID] !== undefined) { continue; } epConfig = endpoint.ID.toString(); if (endpoint.ID < 10) { epConfig = `0${epConfig}`; } allEndpointsSorted.push(epConfig); } allEndpointsSorted.sort(); for (let i = 0; i < allEndpointsSorted.length; i++) { epConfig = allEndpointsSorted[i]; const epId = Number.parseInt(epConfig.substr(0, 2), 10); epConfig = epConfig.substring(2); const epName = `l${epId}`; const epValueAccessRights = epConfig.substr(0, 1); const epStateType = epValueAccessRights === "W" || epValueAccessRights === "*" ? ea.STATE_SET : ea.STATE; const valueConfig = epConfig.substr(1); const valueConfigItems = valueConfig ? valueConfig.split(",") : []; let valueId = valueConfigItems[0] ? valueConfigItems[0] : ""; let valueDescription = valueConfigItems[1] ? valueConfigItems[1] : ""; let valueUnit = valueConfigItems[2] !== undefined ? valueConfigItems[2] : ""; if (exposeDeviceOptions[epName] === undefined) { exposeDeviceOptions[epName] = {}; } const exposeEpOptions = exposeDeviceOptions[epName]; if (valueId === "*") { // GPIO output (Generic) exposeEpOptions.exposed_onoff = true; expose.push(e.switch().withEndpoint(epName)); } else if (valueId === "#") { // GPIO state (contact, gas, noise, occupancy, presence, smoke, sos, tamper, vibration, water leak) exposeEpOptions.exposed_onoff = true; let exposeObj; switch (valueDescription) { case "g": exposeObj = e.gas(); break; case "n": exposeObj = e.noise_detected(); break; case "o": exposeObj = e.occupancy(); break; case "p": exposeObj = e.presence(); break; case "m": exposeObj = e.smoke(); break; case "s": exposeObj = e.sos(); break; case "t": exposeObj = e.tamper(); break; case "v": exposeObj = e.vibration(); break; case "w": exposeObj = e.water_leak(); break; default: // 'c' exposeObj = e.contact(); } expose.push(exposeObj.withProperty("state").withEndpoint(epName)); } else if (valueConfigItems.length > 0) { let valueName; // name in Z2M let valueNumIndex; const idxPos = valueId.search(/(\d+)$/); if (valueId.startsWith("mcpm") || valueId.startsWith("ncpm")) { const num = Number.parseInt(valueId.substr(4, 1), 16); valueName = valueId.substr(0, 4) + num; } else if (idxPos >= 0) { valueNumIndex = valueId.substr(idxPos); valueId = valueId.substr(0, idxPos); } // analog value // 1: value name (if empty, use the EP name) // 2: description (if empty or undefined, use the value name) // 3: units (if undefined, use the key name) const infoLookup = { C: "temperature", "%": "humidity", m: "altitude", Pa: "pressure", ppm: "quality", psize: "particle_size", V: "voltage", A: "current", Wh: "energy", W: "power", Hz: "frequency", pf: "power_factor", lx: "illuminance", }; valueName = valueName !== undefined ? valueName : infoLookup[valueId]; if (valueName === undefined && valueNumIndex) { valueName = `val${valueNumIndex}`; } if (valueName) { exposeEpOptions[`exposed_${valueName}`] = true; } valueName = valueName === undefined ? epName : `${valueName}_${epName}`; if (valueDescription === undefined || valueDescription === "") { if (infoLookup[valueId]) { valueDescription = infoLookup[valueId]; valueDescription = valueDescription.replace("_", " "); } else { valueDescription = "Sensor value"; } } valueDescription = valueDescription.substring(0, 1).toUpperCase() + valueDescription.substring(1); if (valueNumIndex) { valueDescription = `${valueDescription} ${valueNumIndex}`; } if ((valueUnit === undefined || valueUnit === "") && infoLookup[valueId]) { valueUnit = valueId; } exposeEpOptions.exposed_analog = true; expose.push(e .numeric(valueName, epStateType) .withValueMin(-9999999) .withValueMax(9999999) .withValueStep(1) .withDescription(valueDescription) .withUnit(valueUnit)); } const epConfigNext = allEndpointsSorted[i + 1] || "-1"; const epIdNext = Number.parseInt(epConfigNext.substr(0, 2), 10); if (epIdNext !== epId) { const endpoint = device.getEndpoint(epId); if (!endpoint) { continue; } ptvoAddStandardExposes(endpoint, expose, exposeEpOptions, exposeDeviceOptions); } } } if (exposeDeviceOptions.expose_action) { expose.push(e.action(["single", "double", "triple", "hold", "release"])); } if (exposeDeviceOptions.expose_battery) { expose.push(e.battery()); } return expose; }, meta: { multiEndpoint: true, tuyaThermostatPreset: legacy.fz /* for subclassed custom converters */ }, endpoint: (device) => { const endpointList = {}; let count = 0; const deviceConfig = ptvoGetMetaOption(device, "device_config", ""); if (device?.endpoints) { for (const endpoint of device.endpoints) { const epId = endpoint.ID; const epName = `l${epId}`; endpointList[epName] = epId; count++; } } if (deviceConfig === "") { if (count === 0) { // fallback code for (let epId = 1; epId <= 8; epId++) { const epName = `l${epId}`; endpointList[epName] = epId; } } } else { const deviceConfigArray = deviceConfig.split(/[\r\n]+/); // biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress` let epConfig; for (let i = 0; i < deviceConfigArray.length; i++) { epConfig = deviceConfigArray[i]; const matches = epConfig.match(/^([0-9A-F]+)/); if (!matches || matches.length === 0) { continue; } const epId = Number.parseInt(matches[0], 16); const epName = `l${epId}`; endpointList[epName] = epId; } } endpointList.action = 1; return endpointList; }, configure: async (device, coordinatorEndpoint) => { if (device != null) { const controlEp = device.getEndpoint(1); if (controlEp != null) { try { const deviceConfig = await controlEp.read("genBasic", [32768]); if (deviceConfig) { ptvoSetMetaOption(device, "device_config", deviceConfig[32768]); device.save(); } } catch { /* do nothing */ } } for (const endpoint of device.endpoints) { if (endpoint.supportsInputCluster("haElectricalMeasurement")) { endpoint.saveClusterAttributeKeyValue("haElectricalMeasurement", { dcCurrentDivisor: 1000, dcCurrentMultiplier: 1, dcPowerDivisor: 10, dcPowerMultiplier: 1, dcVoltageDivisor: 100, dcVoltageMultiplier: 1, acVoltageDivisor: 100, acVoltageMultiplier: 1, acCurrentDivisor: 1000, acCurrentMultiplier: 1, acPowerDivisor: 1, acPowerMultiplier: 1, }); } if (endpoint.supportsInputCluster("seMetering")) { endpoint.saveClusterAttributeKeyValue("seMetering", { divisor: 1000, multiplier: 1 }); } } } }, }, { zigbeeModel: ["DNCKAT_D001"], model: "DNCKATSD001", vendor: "Custom devices (DiY)", description: "DNCKAT single key wired wall dimmable light switch", extend: [m.light()], }, { zigbeeModel: ["DNCKAT_S001"], model: "DNCKATSW001", vendor: "Custom devices (DiY)", description: "DNCKAT single key wired wall light switch", extend: [m.onOff()], }, { zigbeeModel: ["DNCKAT_S002"], model: "DNCKATSW002", vendor: "Custom devices (DiY)", description: "DNCKAT double key wired wall light switch", fromZigbee: [fzLocal.DNCKAT_S00X_buttons], extend: [m.deviceEndpoints({ endpoints: { left: 1, right: 2 } }), m.onOff({ endpointNames: ["left", "right"] })], exposes: [e.action(["release_left", "hold_left", "release_right", "hold_right"])], }, { zigbeeModel: ["DNCKAT_S003"], model: "DNCKATSW003", vendor: "Custom devices (DiY)", description: "DNCKAT triple key wired wall light switch", fromZigbee: [fzLocal.DNCKAT_S00X_buttons], extend: [m.deviceEndpoints({ endpoints: { left: 1, center: 2, right: 3 } }), m.onOff({ endpointNames: ["left", "center", "right"] })], exposes: [e.action(["release_left", "hold_left", "release_right", "hold_right", "release_center", "hold_center"])], }, { zigbeeModel: ["DNCKAT_S004"], model: "DNCKATSW004", vendor: "Custom devices (DiY)", description: "DNCKAT quadruple key wired wall light switch", fromZigbee: [fzLocal.DNCKAT_S00X_buttons], extend: [ m.deviceEndpoints({ endpoints: { bottom_left: 1, bottom_right: 2, top_left: 3, top_right: 4 } }), m.onOff({ endpointNames: ["bottom_left", "bottom_right", "top_left", "top_right"] }), ], exposes: [ e.action([ "release_bottom_left", "hold_bottom_left", "release_bottom_right", "hold_bottom_right", "release_top_left", "hold_top_left", "release_top_right", "hold_top_right", ]), ], }, { zigbeeModel: ["ZigUP"], model: "ZigUP", vendor: "Custom devices (DiY)", description: "CC2530 based Zigbee relais, switch, sensor and router", fromZigbee: [fzLocal.ZigUP], toZigbee: [tz.on_off, tz.light_color, tzLocal.ZigUP_lock], exposes: [e.switch()], }, { zigbeeModel: ["ZWallRemote0"], model: "ZWallRemote0", vendor: "Custom devices (DiY)", description: "Matts Wall Switch Remote", fromZigbee: [fz.command_toggle], toZigbee: [], exposes: [e.action(["toggle"])], }, { zigbeeModel: ["ZeeFlora"], model: "ZeeFlora", vendor: "Custom devices (DiY)", description: "Flower sensor with rechargeable battery", fromZigbee: [fz.temperature, fz.soil_moisture, fz.battery], toZigbee: [], meta: { multiEndpoint: true }, configure: async (device, coordinatorEndpoint) => { const firstEndpoint = device.getEndpoint(1); await reporting.bind(firstEndpoint, coordinatorEndpoint, ["genPowerCfg", "msTemperatureMeasurement", "msSoilMoisture"]); const overrides = { min: 0, max: 3600, change: 0 }; await reporting.batteryVoltage(firstEndpoint, overrides); await reporting.batteryPercentageRemaining(firstEndpoint, overrides); await reporting.temperature(firstEndpoint, overrides); await reporting.soil_moisture(firstEndpoint, overrides); }, exposes: [e.soil_moisture(), e.battery(), e.temperature()], extend: [m.illuminance()], }, { zigbeeModel: ["UT-01"], model: "EFR32MG21.Router.1", vendor: "Custom devices (DiY)", description: "EFR32MG21 Zigbee bridge router", extend: [m.forcePowerSource({ powerSource: "Mains (single phase)" })], }, { zigbeeModel: ["UT-02"], model: "EFR32MG21.Router.2", vendor: "Custom devices (DiY)", description: "EFR32MG21 router", fromZigbee: [], toZigbee: [], exposes: [], }, { zigbeeModel: ["b-parasite"], model: "b-parasite", vendor: "Custom devices (DiY)", description: "b-parasite open source soil moisture sensor", fromZigbee: [fz.temperature, fz.humidity, fz.battery, fz.soil_moisture], toZigbee: [], exposes: [e.temperature(), e.humidity(), e.battery(), e.soil_moisture()], configure: async (device, coordinatorEndpoint) => { const endpoint = device.getEndpoint(10); await reporting.bind(endpoint, coordinatorEndpoint, ["genPowerCfg", "msTemperatureMeasurement", "msRelativeHumidity", "msSoilMoisture"]); await reporting.batteryPercentageRemaining(endpoint); await reporting.temperature(endpoint); await reporting.humidity(endpoint); await reporting.soil_moisture(endpoint); }, extend: [m.illuminance(), m.identify()], }, { zigbeeModel: ["MULTI-ZIG-SW"], model: "MULTI-ZIG-SW", vendor: "smarthjemmet.dk", description: "Multi switch from Smarthjemmet.dk", fromZigbee: [fzLocal.multi_zig_sw_switch_buttons, fzLocal.multi_zig_sw_battery, fzLocal.multi_zig_sw_switch_config], toZigbee: [tzLocal.multi_zig_sw_switch_type], exposes: [ ...[e.enum("switch_type_1", exposes.access.ALL, Object.keys(switchTypesList)).withEndpoint("button_1")], ...[e.enum("switch_type_2", exposes.access.ALL, Object.keys(switchTypesList)).withEndpoint("button_2")], ...[e.enum("switch_type_3", exposes.access.ALL, Object.keys(switchTypesList)).withEndpoint("button_3")], ...[e.enum("switch_type_4", exposes.access.ALL, Object.keys(switchTypesList)).withEndpoint("button_4")], e.battery(), e.action(["single", "double", "triple", "hold", "release"]), e.battery_voltage(), ], meta: { multiEndpoint: true }, endpoint: (device) => { return { button_1: 2, button_2: 3, button_3: 4, button_4: 5 }; }, configure: async (device, coordinatorEndpoint) => { const endpoint = device.getEndpoint(1); await endpoint.read("genBasic", ["modelId", "swBuildId", "powerSource"]); }, }, { // https://github.com/devbis/z03mmc/ zigbeeModel: ["LYWSD03MMC"], model: "LYWSD03MMC", vendor: "Custom devices (DiY)", description: "Xiaomi temperature & humidity sensor with custom firmware", extend: [ m.quirkAddEndpointCluster({ endpointID: 1, outputClusters: [], inputClusters: ["genPowerCfg", "msTemperatureMeasurement", "msRelativeHumidity", "hvacUserInterfaceCfg"], }), m.battery(), m.temperature({ reporting: { min: 10, max: 300, change: 10 } }), m.humidity({ reporting: { min: 10, max: 300, change: 50 } }), m.enumLookup({ name: "temperature_display_mode", lookup: { celsius: 0, fahrenheit: 1 }, cluster: "hvacUserInterfaceCfg", attribute: "tempDisplayMode", description: "The units of the temperature displayed on the device screen.", }), m.binary({ name: "show_smiley", valueOn: ["SHOW", 1], valueOff: ["HIDE", 0], cluster: "hvacUserInterfaceCfg", attribute: { ID: 0x0010, type: zigbee_herdsman_1.Zcl.DataType.BOOLEAN }, description: "Whether to show a smiley on the device screen.", }), m.binary({ name: "enable_display", valueOn: ["ON", 1], valueOff: ["OFF", 0], cluster: "hvacUserInterfaceCfg", attribute: { ID: 0x0011, type: zigbee_herdsman_1.Zcl.DataType.BOOLEAN }, description: "Whether to turn display on/off.", }), m.numeric({ name: "temperature_calibration", unit: "°C", cluster: "msTemperatureMeasurement", attribute: { ID: 0x0010, type: zigbee_herdsman_1.Zcl.DataType.INT16 }, valueMin: -100.0, valueMax: 100.0, valueStep: 0.01, scale: 100, description: "The temperature calibration offset is set in 0.01° steps.", }), m.numeric({ name: "humidity_calibration", unit: "%", cluster: "msRelativeHumidity", attribute: { ID: 0x0010, type: zigbee_herdsman_1.Zcl.DataType.INT16 }, valueMin: -100.0, valueMax: 100.0, valueStep: 0.01, scale: 100, description: "The humidity calibration offset is set in 0.01 % steps.", }), m.numeric({ name: "comfort_temperature_min", unit: "°C", cluster: "hvacUserInterfaceCfg", attribute: { ID: 0x0102, type: zigbee_herdsman_1.Zcl.DataType.INT16 }, valueMin: -100.0, valueMax: 100.0, scale: 100, description: "Comfort parameters/Temperature minimum, in 0.01°C steps.", }), m.numeric({ name: "comfort_temperature_max", unit: "°C", cluster: "hvacUserInterfaceCfg", attribute: { ID: 0x0103, type: zigbee_herdsman_1.Zcl.DataType.INT16 }, valueMin: -100.0, valueMax: 100.0, scale: 100, description: "Comfort parameters/Temperature maximum, in 0.01°C steps.", }), m.numeric({ name: "comfort_humidity_min", unit: "%", cluster: "hvacUserInterfaceCfg", attribute: { ID: 0x0104, type: zigbee_herdsman_1.Zcl.DataType.UINT16 }, valueMin: 0.0, valueMax: 100.0, scale: 100, description: "Comfort parameters/Humidity minimum, in 0.01% steps.", }), m.numeric({ name: "comfort_humidity_max", unit: "%", cluster: "hvacUserInterfaceCfg", attribute: { ID: 0x0105, type: zigbee_herdsman_1.Zcl.DataType.UINT16 }, valueMin: 0.0, valueMax: 100.0, scale: 100, description: "Comfort parameters/Humidity maximum, in 0.01% steps.", }), ], ota: true, configure: async (device, coordinatorEndpoint) => { const endpoint = device.getEndpoint(1); const bindClusters = ["msTemperatureMeasurement", "msRelativeHumidity", "genPowerCfg"]; await reporting.bind(endpoint, coordinatorEndpoint, bindClusters); await reporting.temperature(endpoint, { min: 10, max: 300, change: 10 }); await reporting.humidity(endpoint, { min: 10, max: 300, change: 50 }); await reporting.batteryPercentageRemaining(endpoint); try { await endpoint.read("hvacThermostat", [0x0010, 0x0011, 0x0102, 0x0103, 0x0104, 0x0105]); await endpoint.read("msTemperatureMeasurement", [0x0010]); await endpoint.read("msRelativeHumidity", [0x0010]); } catch { /* backward compatibility */ } }, }, { zigbeeModel: ["MHO-C401N"], model: "MHO-C401N", vendor: "Custom devices (DiY)", description: "Xiaomi temperature & humidity sensor with custom firmware", extend: [ m.quirkAddEndpointCluster({ endpointID: 1, outputClusters: ["hvacUserInterfaceCfg"], inputClusters: ["genPowerCfg", "msTemperatureMeasurement", "msRelativeHumidity", "hvacUserInterfaceCfg"], }), m.battery(), m.temperature({ reporting: { min: 10, max: 300, change: 10 } }), m.humidity({ reporting: { min: 10, max: 300, change: 50 } }), // Temperature display and show smile. // For details, see: https://github.com/pvvx/ZigbeeTLc/issues/28#issue-2033984519 m.enumLookup({ name: "temperature_display_mode", lookup: { celsius: 0, fahrenheit: 1 }, cluster: "hvacUserInterfaceCfg", attribute: "tempDisplayMode", description: "The units of the temperature displayed on the device screen.", }), m.binary({ name: "show_smile", valueOn: ["HIDE", 1], valueOff: ["SHOW", 0], cluster: "hvacUserInterfaceCfg", attribute: "programmingVisibility", description: "Whether to show a smile on the device screen.", }), // Setting offsets for temperature and humidity. // For details, see: https://github.com/pvvx/ZigbeeTLc/issues/30 m.numeric({ name: "temperature_calibration", unit: "C", cluster: "hvacUserInterfaceCfg", attribute: { ID: 0x0100, type: 40 }, valueMin: