UNPKG

zigbee-herdsman-converters

Version:

Collection of device converters to be used with zigbee-herdsman

1,102 lines 110 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; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.GPDF_COMMANDS = exports.TIME_LOOKUP = void 0; exports.setupAttributes = setupAttributes; exports.setupConfigureForReporting = setupConfigureForReporting; exports.setupConfigureForBinding = setupConfigureForBinding; exports.setupConfigureForReading = setupConfigureForReading; exports.determineEndpoint = determineEndpoint; exports.forceDeviceType = forceDeviceType; exports.forcePowerSource = forcePowerSource; exports.linkQuality = linkQuality; exports.battery = battery; exports.deviceTemperature = deviceTemperature; exports.identify = identify; exports.onOff = onOff; exports.commandsOnOff = commandsOnOff; exports.customTimeResponse = customTimeResponse; exports.illuminance = illuminance; exports.temperature = temperature; exports.pressure = pressure; exports.flow = flow; exports.humidity = humidity; exports.soilMoisture = soilMoisture; exports.occupancy = occupancy; exports.co2 = co2; exports.pm25 = pm25; exports.light = light; exports.commandsLevelCtrl = commandsLevelCtrl; exports.commandsColorCtrl = commandsColorCtrl; exports.lightingBallast = lightingBallast; exports.lock = lock; exports.windowCovering = windowCovering; exports.commandsWindowCovering = commandsWindowCovering; exports.iasZoneAlarm = iasZoneAlarm; exports.iasWarning = iasWarning; exports.electricityMeter = electricityMeter; exports.gasMeter = gasMeter; exports.genericGreenPower = genericGreenPower; exports.commandsScenes = commandsScenes; exports.enumLookup = enumLookup; exports.numeric = numeric; exports.binary = binary; exports.text = text; exports.actionEnumLookup = actionEnumLookup; exports.quirkAddEndpointCluster = quirkAddEndpointCluster; exports.quirkCheckinInterval = quirkCheckinInterval; exports.reconfigureReportingsOnDeviceAnnounce = reconfigureReportingsOnDeviceAnnounce; exports.skipDefaultResponse = skipDefaultResponse; exports.deviceEndpoints = deviceEndpoints; exports.deviceAddCustomCluster = deviceAddCustomCluster; exports.ignoreClusterReport = ignoreClusterReport; exports.bindCluster = bindCluster; const zigbee_herdsman_1 = require("zigbee-herdsman"); const node_assert_1 = __importDefault(require("node:assert")); const fz = __importStar(require("../converters/fromZigbee")); const tz = __importStar(require("../converters/toZigbee")); const logger_1 = require("../lib/logger"); const globalStore = __importStar(require("../lib/store")); const exposes_1 = require("./exposes"); const light_1 = require("./light"); const utils_1 = require("./utils"); function getEndpointsWithCluster(device, cluster, type) { if (!device.endpoints) { throw new Error(`${device.ieeeAddr} ${device.endpoints}`); } const endpoints = type === "input" ? device.endpoints.filter((ep) => ep.getInputClusters().find((c) => ((0, utils_1.isNumber)(cluster) ? c.ID === cluster : c.name === cluster))) : device.endpoints.filter((ep) => ep.getOutputClusters().find((c) => ((0, utils_1.isNumber)(cluster) ? c.ID === cluster : c.name === cluster))); if (endpoints.length === 0) { throw new Error(`Device ${device.ieeeAddr} has no ${type} cluster ${cluster}`); } return endpoints; } const IAS_EXPOSE_LOOKUP = { occupancy: exposes_1.presets.binary("occupancy", exposes_1.access.STATE, true, false).withDescription("Indicates whether the device detected occupancy"), contact: exposes_1.presets.binary("contact", exposes_1.access.STATE, false, true).withDescription("Indicates whether the device is opened or closed"), smoke: exposes_1.presets.binary("smoke", exposes_1.access.STATE, true, false).withDescription("Indicates whether the device detected smoke"), water_leak: exposes_1.presets.binary("water_leak", exposes_1.access.STATE, true, false).withDescription("Indicates whether the device detected a water leak"), carbon_monoxide: exposes_1.presets.binary("carbon_monoxide", exposes_1.access.STATE, true, false).withDescription("Indicates whether the device detected carbon monoxide"), sos: exposes_1.presets.binary("sos", exposes_1.access.STATE, true, false).withLabel("SOS").withDescription("Indicates whether the SOS alarm is triggered"), vibration: exposes_1.presets.binary("vibration", exposes_1.access.STATE, true, false).withDescription("Indicates whether the device detected vibration"), alarm: exposes_1.presets.binary("alarm", exposes_1.access.STATE, true, false).withDescription("Indicates whether the alarm is triggered"), gas: exposes_1.presets.binary("gas", exposes_1.access.STATE, true, false).withDescription("Indicates whether the device detected gas"), alarm_1: exposes_1.presets.binary("alarm_1", exposes_1.access.STATE, true, false).withDescription("Indicates whether IAS Zone alarm 1 is active"), alarm_2: exposes_1.presets.binary("alarm_2", exposes_1.access.STATE, true, false).withDescription("Indicates whether IAS Zone alarm 2 is active"), tamper: exposes_1.presets.binary("tamper", exposes_1.access.STATE, true, false).withDescription("Indicates whether the device is tampered").withCategory("diagnostic"), rain: exposes_1.presets.binary("rain", exposes_1.access.STATE, true, false).withDescription("Indicates whether the device detected rainfall"), battery_low: exposes_1.presets .binary("battery_low", exposes_1.access.STATE, true, false) .withDescription("Indicates whether the battery of the device is almost empty") .withCategory("diagnostic"), supervision_reports: exposes_1.presets .binary("supervision_reports", exposes_1.access.STATE, true, false) .withDescription("Indicates whether the device issues reports on zone operational status") .withCategory("diagnostic"), restore_reports: exposes_1.presets .binary("restore_reports", exposes_1.access.STATE, true, false) .withDescription("Indicates whether the device issues reports on alarm no longer being present") .withCategory("diagnostic"), ac_status: exposes_1.presets .binary("ac_status", exposes_1.access.STATE, true, false) .withDescription("Indicates whether the device mains voltage supply is at fault") .withCategory("diagnostic"), test: exposes_1.presets .binary("test", exposes_1.access.STATE, true, false) .withDescription("Indicates whether the device is currently performing a test") .withCategory("diagnostic"), trouble: exposes_1.presets .binary("trouble", exposes_1.access.STATE, true, false) .withDescription("Indicates whether the device is currently having trouble") .withCategory("diagnostic"), battery_defect: exposes_1.presets .binary("battery_defect", exposes_1.access.STATE, true, false) .withDescription("Indicates whether the device battery is defective") .withCategory("diagnostic"), }; exports.TIME_LOOKUP = { MAX: 65000, "4_HOURS": 14400, "1_HOUR": 3600, "30_MINUTES": 1800, "5_MINUTES": 300, "2_MINUTES": 120, "1_MINUTE": 60, "10_SECONDS": 10, "5_SECONDS": 5, "1_SECOND": 1, MIN: 0, }; function convertReportingConfigTime(time) { if ((0, utils_1.isString)(time)) { if (!(time in exports.TIME_LOOKUP)) throw new Error(`Reporting time '${time}' is unknown`); return exports.TIME_LOOKUP[time]; } return time; } async function setupAttributes(entity, coordinatorEndpoint, cluster, config, configureReporting = true, read = true) { const endpoints = (0, utils_1.isEndpoint)(entity) ? [entity] : getEndpointsWithCluster(entity, cluster, "input"); const ieeeAddr = (0, utils_1.isEndpoint)(entity) ? entity.deviceIeeeAddress : entity.ieeeAddr; for (const endpoint of endpoints) { logger_1.logger.debug(`Configure reporting: ${configureReporting}, read: ${read} for ${ieeeAddr}/${endpoint.ID} ${cluster} ${JSON.stringify(config)}`, "zhc:setupattribute"); // Split into chunks of 4 to prevent to message becoming too big. const chunks = (0, utils_1.splitArrayIntoChunks)(config, 4); if (configureReporting) { await endpoint.bind(cluster, coordinatorEndpoint); for (const chunk of chunks) { await endpoint.configureReporting(cluster, chunk.map((a) => ({ minimumReportInterval: convertReportingConfigTime(a.min), maximumReportInterval: convertReportingConfigTime(a.max), reportableChange: a.change, attribute: a.attribute, }))); } } if (read) { for (const chunk of chunks) { try { // Don't fail configuration if reading this attribute fails // https://github.com/Koenkk/zigbee-herdsman-converters/pull/7074 await endpoint.read(cluster, chunk.map((a) => ((0, utils_1.isString)(a) ? a : (0, utils_1.isObject)(a.attribute) ? a.attribute.ID : a.attribute))); } catch (e) { logger_1.logger.debug(`Reading attribute failed: ${e}`, "zhc:setupattribute"); } } } } } function setupConfigureForReporting(cluster, attribute, args) { const { config = false, access = undefined, endpointNames = undefined, singleEndpoint = false } = args; const configureReporting = !!config; const read = !!(access & exposes_1.access.GET); if (configureReporting || read) { const configure = async (device, coordinatorEndpoint, definition) => { const reportConfig = config ? { ...config, attribute: attribute } : { attribute, min: -1, max: -1, change: -1 }; let endpoints; if (endpointNames) { (0, node_assert_1.default)(!singleEndpoint, "`endpointNames` cannot be used together with `singleEndpoint`"); const definitionEndpoints = definition.endpoint(device); const endpointIds = endpointNames.map((e) => definitionEndpoints[e]); endpoints = device.endpoints.filter((e) => endpointIds.includes(e.ID)); } else { endpoints = getEndpointsWithCluster(device, cluster, "input"); if (singleEndpoint) { endpoints = [endpoints[0]]; } } for (const endpoint of endpoints) { await setupAttributes(endpoint, coordinatorEndpoint, cluster, [reportConfig], configureReporting, read); } }; return configure; } return undefined; } function setupConfigureForBinding(cluster, clusterType, endpointNames) { const configure = async (device, coordinatorEndpoint, definition) => { if (endpointNames) { const definitionEndpoints = definition.endpoint(device); const endpointIds = endpointNames.map((e) => definitionEndpoints[e]); const endpoints = device.endpoints.filter((e) => endpointIds.includes(e.ID)); for (const endpoint of endpoints) { await endpoint.bind(cluster, coordinatorEndpoint); } } else { const endpoints = getEndpointsWithCluster(device, cluster, clusterType); for (const endpoint of endpoints) { await endpoint.bind(cluster, coordinatorEndpoint); } } }; return configure; } function setupConfigureForReading(cluster, attributes, endpointNames) { const configure = async (device, coordinatorEndpoint, definition) => { if (endpointNames) { const definitionEndpoints = definition.endpoint(device); const endpointIds = endpointNames.map((e) => definitionEndpoints[e]); const endpoints = device.endpoints.filter((e) => endpointIds.includes(e.ID)); for (const endpoint of endpoints) { await endpoint.read(cluster, attributes); } } else { const endpoints = getEndpointsWithCluster(device, cluster, "input"); for (const endpoint of endpoints) { await endpoint.read(cluster, attributes); } } }; return configure; } function determineEndpoint(entity, meta, cluster) { const { device, endpoint_name } = meta; if (endpoint_name !== undefined) { // In case an explicit endpoint is given, always send it to that endpoint return entity; } // In case no endpoint is given, match the first endpoint which support the cluster. return device.endpoints.find((e) => e.supportsInputCluster(cluster)) ?? device.endpoints[0]; } // #region General function forceDeviceType(args) { const configure = [ (device, coordinatorEndpoint, definition) => { device.type = args.type; device.save(); }, ]; return { configure, isModernExtend: true }; } function forcePowerSource(args) { const configure = [ (device, coordinatorEndpoint, definition) => { device.powerSource = args.powerSource; device.save(); }, ]; return { configure, isModernExtend: true }; } function linkQuality(args = {}) { const { reporting = false, attribute = "zclVersion", reportingConfig = { min: "1_HOUR", max: "4_HOURS", change: 0 } } = args; // Exposes is empty because the application (e.g. Z2M) adds a linkquality sensor // for every device already. const exposes = []; const fromZigbee = [ { cluster: "genBasic", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { return { linkquality: msg.linkquality }; }, }, ]; const result = { exposes, fromZigbee, isModernExtend: true }; if (reporting) { result.configure = [setupConfigureForReporting("genBasic", attribute, { config: reportingConfig, access: exposes_1.access.GET })]; } return result; } function battery(args = {}) { const { percentage = true, voltage = false, lowStatus = false, percentageReporting = true, voltageReporting = false, dontDividePercentage = false, percentageReportingConfig = { min: "1_HOUR", max: "MAX", change: 10 }, voltageReportingConfig = { min: "1_HOUR", max: "MAX", change: 10 }, lowStatusReportingConfig = undefined, voltageToPercentage = undefined, } = args; const exposes = []; if (percentage) { exposes.push(exposes_1.presets .numeric("battery", exposes_1.access.STATE_GET) .withUnit("%") .withDescription("Remaining battery in %") .withValueMin(0) .withValueMax(100) .withCategory("diagnostic")); } if (voltage) { exposes.push(exposes_1.presets.numeric("voltage", exposes_1.access.STATE_GET).withUnit("mV").withDescription("Reported battery voltage in millivolts").withCategory("diagnostic")); } if (lowStatus) { exposes.push(exposes_1.presets.binary("battery_low", exposes_1.access.STATE, true, false).withDescription("Empty battery indicator").withCategory("diagnostic")); } const fromZigbee = [ { cluster: "genPowerCfg", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const payload = {}; if (msg.data.batteryPercentageRemaining !== undefined && msg.data.batteryPercentageRemaining < 255) { // Some devices do not comply to the ZCL and report a // batteryPercentageRemaining of 100 when the battery is full (should be 200). let percentage = msg.data.batteryPercentageRemaining; percentage = dontDividePercentage ? percentage : percentage / 2; if (percentage) payload.battery = (0, utils_1.precisionRound)(percentage, 2); } if (msg.data.batteryVoltage !== undefined && msg.data.batteryVoltage < 255) { // Deprecated: voltage is = mV now but should be V if (voltage) payload.voltage = msg.data.batteryVoltage * 100; if (voltageToPercentage) { payload.battery = (0, utils_1.batteryVoltageToPercentage)(payload.voltage, voltageToPercentage); } } if (msg.data.batteryAlarmState !== undefined) { const battery1Low = (msg.data.batteryAlarmState & (1 << 0) || msg.data.batteryAlarmState & (1 << 1) || msg.data.batteryAlarmState & (1 << 2) || msg.data.batteryAlarmState & (1 << 3)) > 0; const battery2Low = (msg.data.batteryAlarmState & (1 << 10) || msg.data.batteryAlarmState & (1 << 11) || msg.data.batteryAlarmState & (1 << 12) || msg.data.batteryAlarmState & (1 << 13)) > 0; const battery3Low = (msg.data.batteryAlarmState & (1 << 20) || msg.data.batteryAlarmState & (1 << 21) || msg.data.batteryAlarmState & (1 << 22) || msg.data.batteryAlarmState & (1 << 23)) > 0; if (lowStatus) payload.battery_low = battery1Low || battery2Low || battery3Low; } return payload; }, }, ]; const toZigbee = [ { key: ["battery", "voltage"], convertGet: async (entity, key, meta) => { // Don't fail GET request if reading fails // Split reading is needed for more clear debug logs const ep = determineEndpoint(entity, meta, "genPowerCfg"); try { await ep.read("genPowerCfg", ["batteryPercentageRemaining"]); } catch (e) { logger_1.logger.debug(`Reading batteryPercentageRemaining failed: ${e}, device probably doesn't support it`, "zhc:setupattribute"); } try { await ep.read("genPowerCfg", ["batteryVoltage"]); } catch (e) { logger_1.logger.debug(`Reading batteryVoltage failed: ${e}, device probably doesn't support it`, "zhc:setupattribute"); } }, }, ]; const result = { exposes, fromZigbee, toZigbee, configure: [], isModernExtend: true }; if (percentageReporting || voltageReporting) { if (percentageReporting) { result.configure.push(setupConfigureForReporting("genPowerCfg", "batteryPercentageRemaining", { config: percentageReportingConfig, access: exposes_1.access.STATE_GET, singleEndpoint: true, })); } if (voltageReporting) { result.configure.push(setupConfigureForReporting("genPowerCfg", "batteryVoltage", { config: voltageReportingConfig, access: exposes_1.access.STATE_GET, singleEndpoint: true, })); } result.configure.push((0, utils_1.configureSetPowerSourceWhenUnknown)("Battery")); } if (voltageToPercentage || dontDividePercentage) { const meta = { battery: {} }; if (voltageToPercentage) meta.battery.voltageToPercentage = voltageToPercentage; if (dontDividePercentage) meta.battery.dontDividePercentage = dontDividePercentage; result.meta = meta; } if (lowStatusReportingConfig) { result.configure.push(setupConfigureForReporting("genPowerCfg", "batteryAlarmState", { config: lowStatusReportingConfig, access: exposes_1.access.STATE_GET, singleEndpoint: true, })); } return result; } function deviceTemperature(args = {}) { return numeric({ name: "device_temperature", cluster: "genDeviceTempCfg", attribute: "currentTemperature", reporting: { min: "5_MINUTES", max: "1_HOUR", change: 1 }, description: "Temperature of the device", unit: "°C", access: "STATE_GET", entityCategory: "diagnostic", ...args, }); } function identify(args = { isSleepy: false }) { const { isSleepy } = args; const normal = exposes_1.presets.enum("identify", exposes_1.access.SET, ["identify"]).withDescription("Initiate device identification").withCategory("config"); const sleepy = exposes_1.presets .enum("identify", exposes_1.access.SET, ["identify"]) .withDescription("Initiate device identification. This device is asleep by default." + "You may need to wake it up first before sending the identify command.") .withCategory("config"); const exposes = isSleepy ? [sleepy] : [normal]; const identifyTimeout = exposes_1.presets .numeric("identify_timeout", exposes_1.access.SET) .withDescription("Sets the duration of the identification procedure in seconds (i.e., how long the device would flash)." + "The value ranges from 1 to 30 seconds (default: 3).") .withValueMin(1) .withValueMax(30); const toZigbee = [ { key: ["identify"], options: [identifyTimeout], convertSet: async (entity, key, value, meta) => { const identifyTimeout = meta.options.identify_timeout ?? 3; await entity.command("genIdentify", "identify", { identifytime: identifyTimeout }, (0, utils_1.getOptions)(meta.mapped, entity)); }, }, ]; return { exposes, toZigbee, isModernExtend: true }; } function onOff(args = {}) { const { powerOnBehavior = true, skipDuplicateTransaction = false, configureReporting = true, endpointNames = undefined, description = undefined, ota = false, } = args; const exposes = description ? (0, utils_1.exposeEndpoints)(exposes_1.presets.switch(description), endpointNames) : (0, utils_1.exposeEndpoints)(exposes_1.presets.switch(), endpointNames); const fromZigbee = [skipDuplicateTransaction ? fz.on_off_skip_duplicate_transaction : fz.on_off]; const toZigbee = [endpointNames ? { ...tz.on_off, endpoints: endpointNames } : tz.on_off]; if (powerOnBehavior) { exposes.push(...(0, utils_1.exposeEndpoints)(exposes_1.presets.power_on_behavior(["off", "on", "toggle", "previous"]), endpointNames)); fromZigbee.push(fz.power_on_behavior); toZigbee.push(tz.power_on_behavior); } const result = { exposes, fromZigbee, toZigbee, isModernExtend: true }; if (ota) result.ota = ota; if (configureReporting) { result.configure = [ async (device, coordinatorEndpoint) => { await setupAttributes(device, coordinatorEndpoint, "genOnOff", [{ attribute: "onOff", min: "MIN", max: "MAX", change: 1 }]); if (powerOnBehavior) { try { // Don't fail configure if reading this attribute fails, some devices don't support it. await setupAttributes(device, coordinatorEndpoint, "genOnOff", [{ attribute: "startUpOnOff", min: "MIN", max: "MAX", change: 1 }], false); } catch (e) { if (e.message.includes("UNSUPPORTED_ATTRIBUTE")) { logger_1.logger.debug("Reading startUpOnOff failed, this features is unsupported", "zhc:onoff"); } else { throw e; } } } }, (0, utils_1.configureSetPowerSourceWhenUnknown)("Mains (single phase)"), ]; } return result; } function commandsOnOff(args = {}) { const { commands = ["on", "off", "toggle"], bind = true, endpointNames = undefined } = args; let actions = commands; if (endpointNames) { actions = commands.flatMap((c) => endpointNames.map((e) => `${c}_${e}`)); } const exposes = [exposes_1.presets.enum("action", exposes_1.access.STATE, actions).withDescription("Triggered action (e.g. a button click)")]; const actionPayloadLookup = { commandOn: "on", commandOff: "off", commandOffWithEffect: "off", commandToggle: "toggle", }; const fromZigbee = [ { cluster: "genOnOff", type: ["commandOn", "commandOff", "commandOffWithEffect", "commandToggle"], convert: (model, msg, publish, options, meta) => { if ((0, utils_1.hasAlreadyProcessedMessage)(msg, model)) return; const payload = { action: (0, utils_1.postfixWithEndpointName)(actionPayloadLookup[msg.type], msg, model, meta) }; (0, utils_1.addActionGroup)(payload, msg, model); return payload; }, }, ]; const result = { exposes, fromZigbee, isModernExtend: true }; if (bind) result.configure = [setupConfigureForBinding("genOnOff", "output", endpointNames)]; return result; } function customTimeResponse(start) { // The Zigbee Cluster Library specification states that the genTime.time response should be the // number of seconds since 1st Jan 2000 00:00:00 UTC. This extend modifies that: // 1970_UTC: number of seconds since the Unix Epoch (1st Jan 1970 00:00:00 UTC) // 2000_LOCAL: seconds since 1 January in the local time zone. // Disable the responses of zigbee-herdsman and respond here instead. const onEvent = [ async (type, data, device, options, state) => { if (!device.customReadResponse) { device.customReadResponse = (frame, endpoint) => { if (frame.isCluster("genTime")) { const payload = {}; if (start === "1970_UTC") { const time = Math.round(new Date().getTime() / 1000); payload.time = time; payload.localTime = time - new Date().getTimezoneOffset() * 60; } else if (start === "2000_LOCAL") { const oneJanuary2000 = new Date("January 01, 2000 00:00:00 UTC+00:00").getTime(); const secondsUTC = Math.round((new Date().getTime() - oneJanuary2000) / 1000); payload.time = secondsUTC - new Date().getTimezoneOffset() * 60; } endpoint.readResponse("genTime", frame.header.transactionSequenceNumber, payload).catch((e) => { logger_1.logger.warning(`Custom time response failed for '${device.ieeeAddr}': ${e}`, "zhc:customtimeresponse"); }); return true; } return false; }; } }, ]; return { onEvent, isModernExtend: true }; } // #endregion // #region Measurement and Sensing function illuminance(args = {}) { const luxScale = (value, type) => { let result = value; if (type === "from") { result = 10 ** ((result - 1) / 10000); } return result; }; const result = numeric({ name: "illuminance", cluster: "msIlluminanceMeasurement", attribute: "measuredValue", reporting: { min: "10_SECONDS", max: "1_HOUR", change: 5 }, // 5 lux description: "Measured illuminance", unit: "lx", scale: luxScale, access: "STATE_GET", ...args, }); const fzIlluminanceRaw = { cluster: "msIlluminanceMeasurement", type: ["attributeReport", "readResponse"], options: [exposes_1.options.illuminance_raw()], convert: (model, msg, publish, options, meta) => { if (options.illuminance_raw) { return { illuminance_raw: msg.data.measuredValue }; } }, }; result.fromZigbee.push(fzIlluminanceRaw); const exposeIlluminanceRaw = (device, options) => { return options?.illuminance_raw ? [exposes_1.presets.illuminance_raw()] : []; }; result.exposes.push(exposeIlluminanceRaw); return result; } function temperature(args = {}) { return numeric({ name: "temperature", cluster: "msTemperatureMeasurement", attribute: "measuredValue", reporting: { min: "10_SECONDS", max: "1_HOUR", change: 100 }, description: "Measured temperature value", unit: "°C", scale: 100, access: "STATE_GET", ...args, }); } function pressure(args = {}) { return numeric({ name: "pressure", cluster: "msPressureMeasurement", attribute: "measuredValue", reporting: { min: "10_SECONDS", max: "1_HOUR", change: 50 }, // 5 kPa description: "The measured atmospheric pressure", unit: "kPa", scale: 10, access: "STATE_GET", ...args, }); } function flow(args = {}) { return numeric({ name: "flow", cluster: "msFlowMeasurement", attribute: "measuredValue", reporting: { min: "10_SECONDS", max: "1_HOUR", change: 10 }, description: "Measured water flow", unit: "m³/h", scale: 10, access: "STATE_GET", ...args, }); } function humidity(args = {}) { return numeric({ name: "humidity", cluster: "msRelativeHumidity", attribute: "measuredValue", reporting: { min: "10_SECONDS", max: "1_HOUR", change: 100 }, description: "Measured relative humidity", unit: "%", scale: 100, access: "STATE_GET", ...args, }); } function soilMoisture(args = {}) { return numeric({ name: "soil_moisture", cluster: "msSoilMoisture", attribute: "measuredValue", reporting: { min: "10_SECONDS", max: "1_HOUR", change: 100 }, description: "Measured soil moisture value", unit: "%", scale: 100, access: "STATE_GET", ...args, }); } function occupancy(args = {}) { const { reporting = true, reportingConfig = { min: "MIN", max: "1_HOUR", change: 0 }, pirConfig = undefined, ultrasonicConfig = undefined, contactConfig = undefined, endpointNames = undefined, } = args; const templateExposes = [exposes_1.presets.occupancy().withAccess(exposes_1.access.STATE_GET)]; const exposes = endpointNames ? templateExposes.flatMap((exp) => endpointNames.map((ep) => exp.withEndpoint(ep))) : templateExposes; const fromZigbee = [ { cluster: "msOccupancySensing", type: ["attributeReport", "readResponse"], options: [exposes_1.options.no_occupancy_since_false()], convert: (model, msg, publish, options, meta) => { if ("occupancy" in msg.data && (!endpointNames || endpointNames.includes((0, utils_1.getEndpointName)(msg, model, meta).toString()))) { const propertyName = (0, utils_1.postfixWithEndpointName)("occupancy", msg, model, meta); const payload = { [propertyName]: (msg.data.occupancy & 1) > 0 }; (0, utils_1.noOccupancySince)(msg.endpoint, options, publish, payload[propertyName] ? "stop" : "start"); return payload; } }, }, ]; const toZigbee = [ { key: ["occupancy"], convertGet: async (entity, key, meta) => { await determineEndpoint(entity, meta, "msOccupancySensing").read("msOccupancySensing", ["occupancy"]); }, }, ]; const settingsExtends = []; const settingsTemplate = { cluster: "msOccupancySensing", description: "", endpointNames: endpointNames, access: "ALL", entityCategory: "config", }; const attributesForReading = []; if (pirConfig) { if (pirConfig.includes("otu_delay")) { settingsExtends.push(numeric({ name: "occupancy_timeout", attribute: "pirOToUDelay", valueMin: 0, valueMax: 65534, unit: "s", ...settingsTemplate, description: "Time in seconds before occupancy is cleared after the last detected movement.", })); attributesForReading.push("pirOToUDelay"); } if (pirConfig.includes("uto_delay")) { settingsExtends.push(numeric({ name: "pir_uto_delay", attribute: "pirUToODelay", valueMin: 0, valueMax: 65534, ...settingsTemplate, })); attributesForReading.push("pirUToODelay"); } if (pirConfig.includes("uto_threshold")) { settingsExtends.push(numeric({ name: "pir_uto_threshold", attribute: "pirUToOThreshold", valueMin: 1, valueMax: 254, ...settingsTemplate, })); attributesForReading.push("pirUToOThreshold"); } } if (ultrasonicConfig) { if (pirConfig.includes("otu_delay")) { settingsExtends.push(numeric({ name: "ultrasonic_otu_delay", attribute: "ultrasonicOToUDelay", valueMin: 0, valueMax: 65534, ...settingsTemplate, })); attributesForReading.push("ultrasonicOToUDelay"); } if (pirConfig.includes("uto_delay")) { settingsExtends.push(numeric({ name: "ultrasonic_uto_delay", attribute: "ultrasonicUToODelay", valueMin: 0, valueMax: 65534, ...settingsTemplate, })); attributesForReading.push("ultrasonicUToODelay"); } if (pirConfig.includes("uto_threshold")) { settingsExtends.push(numeric({ name: "ultrasonic_uto_threshold", attribute: "ultrasonicUToOThreshold", valueMin: 1, valueMax: 254, ...settingsTemplate, })); attributesForReading.push("ultrasonicUToOThreshold"); } } if (contactConfig) { if (pirConfig.includes("otu_delay")) { settingsExtends.push(numeric({ name: "contact_otu_delay", attribute: "contactOToUDelay", valueMin: 0, valueMax: 65534, ...settingsTemplate, })); attributesForReading.push("contactOToUDelay"); } if (pirConfig.includes("uto_delay")) { settingsExtends.push(numeric({ name: "contact_uto_delay", attribute: "contactUToODelay", valueMin: 0, valueMax: 65534, ...settingsTemplate, })); attributesForReading.push("contactUToODelay"); } if (pirConfig.includes("uto_threshold")) { settingsExtends.push(numeric({ name: "contact_uto_threshold", attribute: "contactUToOThreshold", valueMin: 1, valueMax: 254, ...settingsTemplate, })); attributesForReading.push("contactUToOThreshold"); } } settingsExtends.map((extend) => exposes.push(...extend.exposes)); settingsExtends.map((extend) => fromZigbee.push(...extend.fromZigbee)); settingsExtends.map((extend) => toZigbee.push(...extend.toZigbee)); const configure = []; if (attributesForReading.length > 0) configure.push(setupConfigureForReading("msOccupancySensing", attributesForReading, endpointNames)); if (reporting) { configure.push(setupConfigureForReporting("msOccupancySensing", "occupancy", { config: reportingConfig, access: exposes_1.access.STATE_GET, endpointNames: endpointNames, })); } return { exposes, fromZigbee, toZigbee, configure, isModernExtend: true }; } function co2(args = {}) { return numeric({ name: "co2", cluster: "msCO2", label: "CO2", attribute: "measuredValue", reporting: { min: "10_SECONDS", max: "1_HOUR", change: 0.00005 }, // 50 ppm change description: "Measured value", unit: "ppm", scale: 0.000001, access: "STATE_GET", ...args, }); } function pm25(args = {}) { return numeric({ name: "pm25", cluster: "pm25Measurement", attribute: "measuredValue", reporting: { min: "10_SECONDS", max: "1_HOUR", change: 1 }, description: "Measured PM2.5 (particulate matter) concentration", unit: "µg/m³", access: "STATE_GET", ...args, }); } function light(args = {}) { const { effect = true, powerOnBehavior = true, configureReporting = false, ota = false, color = undefined, levelConfig = undefined, turnsOffAtBrightness1 = false, endpointNames = undefined, levelReportingConfig = undefined, } = args; let { colorTemp = undefined } = args; if (colorTemp) { colorTemp = { startup: true, ...colorTemp }; } const argsColor = color ? { modes: ["xy"], applyRedFix: false, enhancedHue: true, ...((0, utils_1.isObject)(color) ? color : {}), } : false; const lightExpose = (0, utils_1.exposeEndpoints)(exposes_1.presets.light().withBrightness(), endpointNames); const fromZigbee = [fz.on_off, fz.brightness, fz.ignore_basic_report, fz.level_config]; const toZigbee = [ endpointNames ? { ...tz.light_onoff_brightness, endpoints: endpointNames } : tz.light_onoff_brightness, tz.ignore_transition, tz.level_config, tz.ignore_rate, tz.light_brightness_move, tz.light_brightness_step, ]; const meta = {}; if (colorTemp || argsColor) { fromZigbee.push(fz.color_colortemp); if (colorTemp && argsColor) toZigbee.push(tz.light_color_colortemp); else if (colorTemp) toZigbee.push(tz.light_colortemp); else if (argsColor) toZigbee.push(tz.light_color); toZigbee.push(tz.light_color_mode, tz.light_color_options); } if (colorTemp) { // biome-ignore lint/complexity/noForEach: ignored using `--suppress` lightExpose.forEach((e) => e.withColorTemp(colorTemp.range)); toZigbee.push(tz.light_colortemp_move, tz.light_colortemp_step); if (colorTemp.startup) { toZigbee.push(tz.light_colortemp_startup); // biome-ignore lint/complexity/noForEach: ignored using `--suppress` lightExpose.forEach((e) => e.withColorTempStartup(colorTemp.range)); } } if (argsColor) { // biome-ignore lint/complexity/noForEach: ignored using `--suppress` lightExpose.forEach((e) => e.withColor(argsColor.modes)); toZigbee.push(tz.light_hue_saturation_move, tz.light_hue_saturation_step); if (argsColor.modes.includes("hs")) { meta.supportsHueAndSaturation = true; } if (argsColor.applyRedFix) { meta.applyRedFix = true; } if (!argsColor.enhancedHue) { meta.supportsEnhancedHue = false; } } if (levelConfig) { // biome-ignore lint/complexity/noForEach: ignored using `--suppress` lightExpose.forEach((e) => (levelConfig.features ? e.withLevelConfig(levelConfig.features) : e.withLevelConfig())); toZigbee.push(tz.level_config); } const exposes = lightExpose; if (effect) { const effects = exposes_1.presets.effect(); if (color) { effects.values.push("colorloop", "stop_colorloop"); } exposes.push(...(0, utils_1.exposeEndpoints)(effects, endpointNames)); toZigbee.push(tz.effect); } if (powerOnBehavior) { exposes.push(...(0, utils_1.exposeEndpoints)(exposes_1.presets.power_on_behavior(["off", "on", "toggle", "previous"]), endpointNames)); fromZigbee.push(fz.power_on_behavior); toZigbee.push(tz.power_on_behavior); } if (turnsOffAtBrightness1) { meta.turnsOffAtBrightness1 = turnsOffAtBrightness1; } const configure = [ async (device, coordinatorEndpoint, definition) => { await (0, light_1.configure)(device, coordinatorEndpoint, true); if (configureReporting) { await setupAttributes(device, coordinatorEndpoint, "genOnOff", [{ attribute: "onOff", min: "MIN", max: "MAX", change: 1 }]); await setupAttributes(device, coordinatorEndpoint, "genLevelCtrl", [ { attribute: "currentLevel", min: "5_SECONDS", max: "MAX", change: 1, ...(levelReportingConfig || {}) }, ]); if (colorTemp) { await setupAttributes(device, coordinatorEndpoint, "lightingColorCtrl", [ { attribute: "colorTemperature", min: "10_SECONDS", max: "MAX", change: 1 }, ]); } if (argsColor) { const attributes = []; if (argsColor.modes.includes("xy")) { attributes.push({ attribute: "currentX", min: "10_SECONDS", max: "MAX", change: 1 }, { attribute: "currentY", min: "10_SECONDS", max: "MAX", change: 1 }); } if (argsColor.modes.includes("hs")) { attributes.push({ attribute: argsColor.enhancedHue ? "enhancedCurrentHue" : "currentHue", min: "10_SECONDS", max: "MAX", change: 1 }, { attribute: "currentSaturation", min: "10_SECONDS", max: "MAX", change: 1 }); } await setupAttributes(device, coordinatorEndpoint, "lightingColorCtrl", attributes); } } }, (0, utils_1.configureSetPowerSourceWhenUnknown)("Mains (single phase)"), ]; const result = { exposes, fromZigbee, toZigbee, configure, meta, isModernExtend: true }; if (ota) result.ota = ota; return result; } function commandsLevelCtrl(args = {}) { const { commands = [ "brightness_move_to_level", "brightness_move_up", "brightness_move_down", "brightness_step_up", "brightness_step_down", "brightness_stop", ], bind = true, endpointNames = undefined, } = args; let actions = commands; if (endpointNames) { actions = commands.flatMap((c) => endpointNames.map((e) => `${c}_${e}`)); } const exposes = [ exposes_1.presets.enum("action", exposes_1.access.STATE, actions).withDescription("Triggered action (e.g. a button click)").withCategory("diagnostic"), ]; const fromZigbee = [fz.command_move_to_level, fz.command_move, fz.command_step, fz.command_stop]; const result = { exposes, fromZigbee, isModernExtend: true }; if (bind) result.configure = [setupConfigureForBinding("genLevelCtrl", "output", endpointNames)]; return result; } function commandsColorCtrl(args = {}) { const { commands = [ "color_temperature_move_stop", "color_temperature_move_up", "color_temperature_move_down", "color_temperature_step_up", "color_temperature_step_down", "enhanced_move_to_hue_and_saturation", "move_to_hue_and_saturation", "color_hue_step_up", "color_hue_step_down", "color_saturation_step_up", "color_saturation_step_down", "color_loop_set", "color_temperature_move", "color_move", "hue_move", "hue_stop", "move_to_saturation", "move_to_hue", ], bind = true, endpointNames = undefined, } = args; let actions = commands; if (endpointNames) { actions = commands.flatMap((c) => endpointNames.map((e) => `${c}_${e}`)); } const exposes = [ exposes_1.presets.enum("action", exposes_1.access.STATE, actions).withDescription("Triggered action (e.g. a button click)").withCategory("diagnostic"), ]; const fromZigbee = [ fz.command_move_color_temperature, fz.command_step_color_temperature, fz.command_enhanced_move_to_hue_and_saturation, fz.command_move_to_hue_and_saturation, fz.command_step_hue, fz.command_step_saturation, fz.command_color_loop_set, fz.command_move_to_color_temp, fz.command_move_to_color, fz.command_move_hue, fz.command_move_to_saturation, fz.command_move_to_hue, ]; const result = { exposes, fromZigbee, isModernExtend: true }; if (bind) result.configure = [setupConfigureForBinding("lightingColorCtrl", "output", endpointNames)]; return result; } function lightingBallast() { const result = { fromZigbee: [fz.lighting_ballast_configuration], toZigbee: [tz.ballast_config], exposes: [ new exposes_1.Numeric("ballast_minimum_level", exposes_1.access.ALL) .withValueMin(1) .withValueMax(254) .withDescription("Specifies the minimum light output of the ballast"), new exposes_1.Numeric("ballast_maximum_level", exposes_1.access.ALL) .withValueMin(1) .withValueMax(254) .withDescription("Specifies the maximum light output of the ballast"), ], configure: [setupConfigureForReading("lightingBallastCfg", ["minLevel", "maxLevel"])], isModernExtend: true, }; return result; } function lock(args) { const { endpointNames = undefined, pinCodeCount } = args; const fromZigbee = [fz.lock, fz.lock_operation_event, fz.lock_programming_event, fz.lock_pin_code_response, fz.lock_user_status_response]; const toZigbee = [{ ...tz.lock, endpoints: endpointNames }, tz.pincode_lock, tz.lock_userstatus, tz.lock_auto_relock_time, tz.lock_sound_volume]; const exposes = [ exposes_1.presets.lock(), exposes_1.presets.pincode(), exposes_1.presets.lock_action(), exposes_1.presets.lock_action_source_name(), exposes_1.presets.lock_action_user(), exposes_1.presets.auto_relock_time().withValueMin(0).withValueMax(3600), exposes_1.presets.sound_volume(), ]; const configure = [ setupConfigureForReporting("closuresDoorLock", "lockState", { config: { min: "MIN", max: "1_HOUR", change: 0 }, access: exposes_1.access.STATE_GET }), ]; const meta = { pinCodeCount: pinCodeCount }; const result = { fromZigbee, toZigbee, exposes, configure, meta, isModernExtend: true }; if (endpointNames) { result.exposes = (0, utils_1.flatten)(exposes.map((expose) => endpointNames.map((endpoint) => expose.clone().withEndpoint(endpoint)))); } return result; } function windowCovering(args) { const { stateSource = "lift", configureReporting = true, controls, coverInverted = false, coverMode, endpointNames = undefined } = args; let coverExpose = exposes_1.presets.cover(); if (controls.includes("lift")) coverExpose = coverExpose.withPosition(); if (controls.includes("tilt")) coverExpose = coverExpose.withTilt(); const exposes = [coverExpose]; const fromZigbee = [fz.cover_position_tilt]; const toZigbee = [{ ...tz.cover_state, endpoints: endpointNames }, tz.cover_position_tilt]; const result = { exposes, fromZigbee, toZigbee, isModernExtend: true }; if (configureReporting) { const configure = []; if (controls.includes("lift")) { configure.push(setupConfigureForReporting("closuresWindowCovering", "currentPositionLiftPercentage", { config: { min: "1_SECOND", max: "MAX", change: 1 }, access: exposes_1.access.STATE_GET, })); } if (controls.includes("til