UNPKG

zigbee-herdsman-converters

Version:

Collection of device converters to be used with zigbee-herdsman

1,002 lines 71.7 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 (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __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.bindCluster = exports.ignoreClusterReport = exports.deviceEndpoints = exports.reconfigureReportingsOnDeviceAnnounce = exports.quirkCheckinInterval = exports.quirkAddEndpointCluster = exports.actionEnumLookup = exports.binary = exports.numeric = exports.enumLookup = exports.ota = exports.electricityMeter = exports.iasWarning = exports.iasZoneAlarm = exports.commandsWindowCovering = exports.windowCovering = exports.lock = exports.commandsColorCtrl = exports.commandsLevelCtrl = exports.light = exports.pm25 = exports.co2 = exports.occupancy = exports.soilMoisture = exports.humidity = exports.flow = exports.pressure = exports.temperature = exports.illuminance = exports.customTimeResponse = exports.commandsOnOff = exports.onOff = exports.identify = exports.deviceTemperature = exports.battery = exports.linkQuality = exports.forcePowerSource = exports.forceDeviceType = exports.setupConfigureForBinding = exports.setupConfigureForReporting = exports.setupAttributes = exports.timeLookup = void 0; const zigbee_herdsman_1 = require("zigbee-herdsman"); const toZigbee_1 = __importDefault(require("../converters/toZigbee")); const fromZigbee_1 = __importDefault(require("../converters/fromZigbee")); const globalLegacy = __importStar(require("../lib/legacy")); const ota_1 = require("../lib/ota"); const globalStore = __importStar(require("../lib/store")); const exposes_1 = require("./exposes"); const light_1 = require("./light"); const utils_1 = require("./utils"); const logger_1 = require("../lib/logger"); 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; } exports.timeLookup = { 'MAX': 65000, '4_HOURS': 14400, '1_HOUR': 3600, '30_MINUTES': 1800, '5_MINUTES': 300, '2_MINUTES': 120, '1_MINUTE': 60, '10_SECONDS': 10, '1_SECOND': 1, 'MIN': 0, }; function convertReportingConfigTime(time) { if ((0, utils_1.isString)(time)) { if (!(time in exports.timeLookup)) throw new Error(`Reporting time '${time}' is unknown`); return exports.timeLookup[time]; } else { 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'); if (configureReporting) { await endpoint.bind(cluster, coordinatorEndpoint); await endpoint.configureReporting(cluster, config.map((a) => ({ minimumReportInterval: convertReportingConfigTime(a.min), maximumReportInterval: convertReportingConfigTime(a.max), reportableChange: a.change, attribute: a.attribute, }))); } if (read) { try { // Don't fail configuration if reading this attribute fails // https://github.com/Koenkk/zigbee-herdsman-converters/pull/7074 await endpoint.read(cluster, config.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'); } } } } exports.setupAttributes = setupAttributes; function setupConfigureForReporting(cluster, attribute, config, access, endpointNames) { 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 entities = [device]; if (endpointNames) { const definitionEndpoints = definition.endpoint(device); const endpointIds = endpointNames.map((e) => definitionEndpoints[e]); entities = device.endpoints.filter((e) => endpointIds.includes(e.ID)); } for (const entity of entities) { await setupAttributes(entity, coordinatorEndpoint, cluster, [reportConfig], configureReporting, read); } }; return configure; } else { return undefined; } } exports.setupConfigureForReporting = setupConfigureForReporting; 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; } exports.setupConfigureForBinding = setupConfigureForBinding; // #region General function forceDeviceType(args) { const configure = async (device, coordinatorEndpoint, definition) => { device.type = args.type; device.save(); }; return { configure, isModernExtend: true }; } exports.forceDeviceType = forceDeviceType; function forcePowerSource(args) { const configure = async (device, coordinatorEndpoint, definition) => { device.powerSource = args.powerSource; device.save(); }; return { configure, isModernExtend: true }; } exports.forcePowerSource = forcePowerSource; function linkQuality(args) { args = { reporting: false, attribute: 'modelId', reportingConfig: { min: '1_HOUR', max: '4_HOURS', change: 0 }, ...args }; const exposes = [ exposes_1.presets.numeric('linkquality', exposes_1.access.STATE).withUnit('lqi').withDescription('Link quality (signal strength)') .withValueMin(0).withValueMax(255).withCategory('diagnostic'), ]; const fromZigbee = [{ cluster: 'genBasic', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { return { linkquality: msg.linkquality }; }, }]; const result = { exposes, fromZigbee, isModernExtend: true }; if (args.reporting) { result.configure = async (device, coordinatorEndpoint) => { setupAttributes(device, coordinatorEndpoint, 'genBasic', [{ attribute: args.attribute, ...args.reportingConfig }]); }; } return result; } exports.linkQuality = linkQuality; function battery(args) { args = { percentage: true, voltage: false, lowStatus: false, percentageReporting: true, voltageReporting: false, dontDividePercentage: false, ...args, }; const exposes = []; if (args.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 (args.voltage) { exposes.push(exposes_1.presets.numeric('voltage', exposes_1.access.STATE_GET).withUnit('mV') .withDescription('Reported battery voltage in millivolts').withCategory('diagnostic')); } if (args.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.hasOwnProperty('batteryPercentageRemaining') && (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). const dontDividePercentage = args.dontDividePercentage; let percentage = msg.data['batteryPercentageRemaining']; percentage = dontDividePercentage ? percentage : percentage / 2; if (args.percentage) payload.battery = (0, utils_1.precisionRound)(percentage, 2); } if (msg.data.hasOwnProperty('batteryVoltage') && (msg.data['batteryVoltage'] < 255)) { // Deprecated: voltage is = mV now but should be V if (args.voltage) payload.voltage = msg.data['batteryVoltage'] * 100; if (args.voltageToPercentage) { payload.battery = (0, utils_1.batteryVoltageToPercentage)(payload.voltage, args.voltageToPercentage); } } if (msg.data.hasOwnProperty('batteryAlarmState')) { 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 (args.lowStatus) payload.battery_low = battery1Low || battery2Low || battery3Low; } return payload; }, }]; const toZigbee = [ { key: ['battery', 'voltage'], convertGet: async (entity, key, meta) => { // Don't fail GET reqest if reading fails // Split reading is needed for more clear debug logs try { await entity.read('genPowerCfg', ['batteryPercentageRemaining']); } catch (e) { logger_1.logger.debug(`Reading batteryPercentageRemaining failed: ${e}, device probably doesn't support it`, 'zhc:setupattribute'); } try { await entity.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, isModernExtend: true }; const defaultReporting = { min: '1_HOUR', max: 'MAX', change: 10 }; if (args.percentageReporting || args.voltageReporting) { result.configure = async (device, coordinatorEndpoint) => { if (args.percentageReporting) { await setupAttributes(device, coordinatorEndpoint, 'genPowerCfg', [ { attribute: 'batteryPercentageRemaining', ...(args.percentageReportingConfig ?? defaultReporting) }, ]); } if (args.voltageReporting) { await setupAttributes(device, coordinatorEndpoint, 'genPowerCfg', [ { attribute: 'batteryVoltage', ...(args.voltageReportingConfig ?? defaultReporting) }, ]); } }; } if (args.voltageToPercentage || args.dontDividePercentage) { const meta = { battery: {} }; if (args.voltageToPercentage) meta.battery.voltageToPercentage = args.voltageToPercentage; if (args.dontDividePercentage) meta.battery.dontDividePercentage = args.dontDividePercentage; result.meta = meta; } return result; } exports.battery = battery; 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, }); } exports.deviceTemperature = deviceTemperature; function identify(args) { args = { isSleepy: false, ...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 indetify command.') .withCategory('config'); const exposes = args.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 }; } exports.identify = identify; function onOff(args) { args = { powerOnBehavior: true, skipDuplicateTransaction: false, configureReporting: true, ...args }; const exposes = args.endpointNames ? args.endpointNames.map((ep) => exposes_1.presets.switch().withEndpoint(ep)) : [exposes_1.presets.switch()]; const fromZigbee = [(args.skipDuplicateTransaction ? fromZigbee_1.default.on_off_skip_duplicate_transaction : fromZigbee_1.default.on_off)]; const toZigbee = [toZigbee_1.default.on_off]; if (args.powerOnBehavior) { exposes.push(exposes_1.presets.power_on_behavior(['off', 'on', 'toggle', 'previous'])); fromZigbee.push(fromZigbee_1.default.power_on_behavior); toZigbee.push(toZigbee_1.default.power_on_behavior); } const result = { exposes, fromZigbee, toZigbee, isModernExtend: true }; if (args.ota) result.ota = args.ota; if (args.configureReporting) { result.configure = async (device, coordinatorEndpoint) => { await setupAttributes(device, coordinatorEndpoint, 'genOnOff', [{ attribute: 'onOff', min: 'MIN', max: 'MAX', change: 1 }]); if (args.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; } } } }; } return result; } exports.onOff = onOff; function commandsOnOff(args) { args = { commands: ['on', 'off', 'toggle'], bind: true, legacyAction: false, ...args }; let actions = args.commands; if (args.endpointNames) { actions = args.commands.map((c) => args.endpointNames.map((e) => `${c}_${e}`)).flat(); } 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; }, }, ]; if (args.legacyAction) { fromZigbee.push(...[globalLegacy.fromZigbee.genOnOff_cmdOn, globalLegacy.fromZigbee.genOnOff_cmdOff]); } const result = { exposes, fromZigbee, isModernExtend: true }; if (args.bind) result.configure = setupConfigureForBinding('genOnOff', 'output', args.endpointNames); return result; } exports.commandsOnOff = commandsOnOff; 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 }; } exports.customTimeResponse = customTimeResponse; // #endregion // #region Measurement and Sensing function illuminance(args) { const luxScale = (value, type) => { let result = value; if (type === 'from') { result = Math.pow(10, (result - 1) / 10000); } return result; }; const rawIllinance = numeric({ name: 'illuminance', cluster: 'msIlluminanceMeasurement', attribute: 'measuredValue', description: 'Raw measured illuminance', access: 'STATE_GET', ...args, }); const illiminanceLux = numeric({ name: 'illuminance_lux', cluster: 'msIlluminanceMeasurement', attribute: 'measuredValue', reporting: { min: '10_SECONDS', max: '1_HOUR', change: 5 }, // 5 lux description: 'Measured illuminance in lux', unit: 'lx', scale: luxScale, access: 'STATE_GET', ...args, }); const result = illiminanceLux; result.fromZigbee.push(...rawIllinance.fromZigbee); result.toZigbee.push(...rawIllinance.toZigbee); result.exposes.push(...rawIllinance.exposes); return result; } exports.illuminance = illuminance; 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, }); } exports.temperature = temperature; 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, }); } exports.pressure = pressure; 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, }); } exports.flow = flow; 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, }); } exports.humidity = humidity; 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, }); } exports.soilMoisture = soilMoisture; function occupancy(args) { const name = 'occupancy'; const cluster = 'msOccupancySensing'; const attribute = 'occupancy'; const valueOn = [true, true]; const valueOff = [false, false]; const result = binary({ name: name, cluster: cluster, attribute: attribute, reporting: { attribute: attribute, min: '10_SECONDS', max: '1_MINUTE', change: 0 }, description: 'Indicates whether the device detected occupancy', access: 'STATE_GET', valueOn: valueOn, valueOff: valueOff, ...args, }); const fromZigbeeOverride = { cluster: cluster.toString(), type: ['attributeReport', 'readResponse'], options: [exposes_1.options.no_occupancy_since_false()], convert: (model, msg, publish, options, meta) => { if (attribute in msg.data && (!args?.endpointName || (0, utils_1.getEndpointName)(msg, model, meta) === args?.endpointName)) { const payload = { [name]: (msg.data[attribute] % 2) > 0 }; (0, utils_1.noOccupancySince)(msg.endpoint, options, publish, payload.occupancy ? 'stop' : 'start'); return payload; } }, }; result.fromZigbee[0] = fromZigbeeOverride; return result; } exports.occupancy = occupancy; 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, }); } exports.co2 = co2; 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, }); } exports.pm25 = pm25; function light(args) { args = { effect: true, powerOnBehavior: true, configureReporting: false, ...args }; if (args.colorTemp) { args.colorTemp = { startup: true, ...args.colorTemp }; } const argsColor = args.color ? { modes: ['xy'], applyRedFix: false, enhancedHue: true, ...((0, utils_1.isObject)(args.color) ? args.color : {}), } : false; const lightExpose = args.endpointNames ? args.endpointNames.map((ep) => exposes_1.presets.light().withBrightness().withEndpoint(ep)) : [exposes_1.presets.light().withBrightness()]; const fromZigbee = [fromZigbee_1.default.on_off, fromZigbee_1.default.brightness, fromZigbee_1.default.ignore_basic_report, fromZigbee_1.default.level_config]; const toZigbee = [ toZigbee_1.default.light_onoff_brightness, toZigbee_1.default.ignore_transition, toZigbee_1.default.level_config, toZigbee_1.default.ignore_rate, toZigbee_1.default.light_brightness_move, toZigbee_1.default.light_brightness_step, ]; const meta = {}; if (args.colorTemp || argsColor) { fromZigbee.push(fromZigbee_1.default.color_colortemp); if (args.colorTemp && argsColor) toZigbee.push(toZigbee_1.default.light_color_colortemp); else if (args.colorTemp) toZigbee.push(toZigbee_1.default.light_colortemp); else if (argsColor) toZigbee.push(toZigbee_1.default.light_color); toZigbee.push(toZigbee_1.default.light_color_mode, toZigbee_1.default.light_color_options); } if (args.colorTemp) { lightExpose.forEach((e) => e.withColorTemp(args.colorTemp.range)); toZigbee.push(toZigbee_1.default.light_colortemp_move, toZigbee_1.default.light_colortemp_step); if (args.colorTemp.startup) { toZigbee.push(toZigbee_1.default.light_colortemp_startup); lightExpose.forEach((e) => e.withColorTempStartup(args.colorTemp.range)); } } if (argsColor) { lightExpose.forEach((e) => e.withColor(argsColor.modes)); toZigbee.push(toZigbee_1.default.light_hue_saturation_move, toZigbee_1.default.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 (args.levelConfig) { lightExpose.forEach((e) => e.withLevelConfig(args.levelConfig.disabledFeatures ?? [])); toZigbee.push(toZigbee_1.default.level_config); } const exposes = lightExpose; if (args.effect) { exposes.push(exposes_1.presets.effect()); toZigbee.push(toZigbee_1.default.effect); } if (args.powerOnBehavior) { exposes.push(exposes_1.presets.power_on_behavior(['off', 'on', 'toggle', 'previous'])); fromZigbee.push(fromZigbee_1.default.power_on_behavior); toZigbee.push(toZigbee_1.default.power_on_behavior); } if (args.hasOwnProperty('turnsOffAtBrightness1')) { meta.turnsOffAtBrightness1 = args.turnsOffAtBrightness1; } const configure = async (device, coordinatorEndpoint, definition) => { await (0, light_1.configure)(device, coordinatorEndpoint, true); if (args.configureReporting) { await setupAttributes(device, coordinatorEndpoint, 'genOnOff', [{ attribute: 'onOff', min: 'MIN', max: 'MAX', change: 1 }]); await setupAttributes(device, coordinatorEndpoint, 'genLevelCtrl', [{ attribute: 'currentLevel', min: '10_SECONDS', max: 'MAX', change: 1 }]); if (args.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); } } }; const result = { exposes, fromZigbee, toZigbee, configure, meta, isModernExtend: true }; if (args.ota) result.ota = args.ota; return result; } exports.light = light; function commandsLevelCtrl(args) { args = { commands: [ 'brightness_move_to_level', 'brightness_move_up', 'brightness_move_down', 'brightness_step_up', 'brightness_step_down', 'brightness_stop', ], bind: true, legacyAction: false, ...args }; let actions = args.commands; if (args.endpointNames) { actions = args.commands.map((c) => args.endpointNames.map((e) => `${c}_${e}`)).flat(); } const exposes = [ exposes_1.presets.enum('action', exposes_1.access.STATE, actions).withDescription('Triggered action (e.g. a button click)').withCategory('diagnostic'), ]; const fromZigbee = [ fromZigbee_1.default.command_move_to_level, fromZigbee_1.default.command_move, fromZigbee_1.default.command_step, fromZigbee_1.default.command_stop, ]; if (args.legacyAction) { // Legacy converters with removed hasAlreadyProcessedMessage and redirects const legacyFromZigbee = [ { cluster: 'genLevelCtrl', type: ['commandMove', 'commandMoveWithOnOff'], options: [exposes_1.options.legacy(), exposes_1.options.simulated_brightness(' Note: will only work when legacy: false is set.')], convert: (model, msg, publish, options, meta) => { if ((0, utils_1.isLegacyEnabled)(options)) { globalLegacy.ictcg1(model, msg, publish, options, 'move'); const direction = msg.data.movemode === 1 ? 'left' : 'right'; return { action: `rotate_${direction}`, rate: msg.data.rate }; } }, }, { cluster: 'genLevelCtrl', type: ['commandStop', 'commandStopWithOnOff'], options: [exposes_1.options.legacy()], convert: (model, msg, publish, options, meta) => { if ((0, utils_1.isLegacyEnabled)(options)) { const value = globalLegacy.ictcg1(model, msg, publish, options, 'stop'); return { action: `rotate_stop`, brightness: value }; } }, }, { cluster: 'genLevelCtrl', type: 'commandMoveToLevelWithOnOff', options: [exposes_1.options.legacy()], convert: (model, msg, publish, options, meta) => { if ((0, utils_1.isLegacyEnabled)(options)) { const value = globalLegacy.ictcg1(model, msg, publish, options, 'level'); const direction = msg.data.level === 0 ? 'left' : 'right'; return { action: `rotate_${direction}_quick`, level: msg.data.level, brightness: value }; } }, }, ]; fromZigbee.push(...legacyFromZigbee); } const result = { exposes, fromZigbee, isModernExtend: true }; if (args.bind) result.configure = setupConfigureForBinding('genLevelCtrl', 'output', args.endpointNames); return result; } exports.commandsLevelCtrl = commandsLevelCtrl; function commandsColorCtrl(args) { args = { 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', '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, ...args, }; let actions = args.commands; if (args.endpointNames) { actions = args.commands.map((c) => args.endpointNames.map((e) => `${c}_${e}`)).flat(); } const exposes = [ exposes_1.presets.enum('action', exposes_1.access.STATE, actions).withDescription('Triggered action (e.g. a button click)').withCategory('diagnostic'), ]; const fromZigbee = [ fromZigbee_1.default.command_move_color_temperature, fromZigbee_1.default.command_step_color_temperature, fromZigbee_1.default.command_ehanced_move_to_hue_and_saturation, fromZigbee_1.default.command_step_hue, fromZigbee_1.default.command_step_saturation, fromZigbee_1.default.command_color_loop_set, fromZigbee_1.default.command_move_to_color_temp, fromZigbee_1.default.command_move_to_color, fromZigbee_1.default.command_move_hue, fromZigbee_1.default.command_move_to_saturation, fromZigbee_1.default.command_move_to_hue, ]; const result = { exposes, fromZigbee, isModernExtend: true }; if (args.bind) result.configure = setupConfigureForBinding('lightingColorCtrl', 'output', args.endpointNames); return result; } exports.commandsColorCtrl = commandsColorCtrl; function lock(args) { args = { ...args }; const fromZigbee = [fromZigbee_1.default.lock, fromZigbee_1.default.lock_operation_event, fromZigbee_1.default.lock_programming_event, fromZigbee_1.default.lock_pin_code_response, fromZigbee_1.default.lock_user_status_response]; const toZigbee = [toZigbee_1.default.lock, toZigbee_1.default.pincode_lock, toZigbee_1.default.lock_userstatus, toZigbee_1.default.lock_auto_relock_time, toZigbee_1.default.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 = async (device, coordinatorEndpoint, definition) => { await setupAttributes(device, coordinatorEndpoint, 'closuresDoorLock', [ { attribute: 'lockState', min: 'MIN', max: '1_HOUR', change: 0 } ]); }; const meta = { pinCodeCount: args.pinCodeCount }; return { fromZigbee, toZigbee, exposes, configure, meta, isModernExtend: true }; } exports.lock = lock; function windowCovering(args) { args = { stateSource: 'lift', configureReporting: true, ...args }; let coverExpose = exposes_1.presets.cover(); if (args.controls.includes('lift')) coverExpose = coverExpose.withPosition(); if (args.controls.includes('tilt')) coverExpose = coverExpose.withTilt(); const exposes = [coverExpose]; const fromZigbee = [fromZigbee_1.default.cover_position_tilt]; const toZigbee = [toZigbee_1.default.cover_state, toZigbee_1.default.cover_position_tilt]; const result = { exposes, fromZigbee, toZigbee, isModernExtend: true }; if (args.configureReporting) { result.configure = async (device, coordinatorEndpoint) => { if (args.controls.includes('lift')) { await setupAttributes(device, coordinatorEndpoint, 'closuresWindowCovering', [{ attribute: 'currentPositionLiftPercentage', min: '1_SECOND', max: 'MAX', change: 1 }]); } if (args.controls.includes('tilt')) { await setupAttributes(device, coordinatorEndpoint, 'closuresWindowCovering', [{ attribute: 'currentPositionTiltPercentage', min: '1_SECOND', max: 'MAX', change: 1 }]); } }; } if (args.coverInverted || args.stateSource === 'tilt') { const meta = {}; if (args.coverInverted) meta.coverInverted = true; if (args.stateSource === 'tilt') meta.coverStateFromTilt = true; result.meta = meta; } return result; } exports.windowCovering = windowCovering; function commandsWindowCovering(args) { args = { commands: ['open', 'close', 'stop'], bind: true, legacyAction: false, ...args }; let actions = args.commands; if (args.endpointNames) { actions = args.commands.map((c) => args.endpointNames.map((e) => `${c}_${e}`)).flat(); } const exposes = [ exposes_1.presets.enum('action', exposes_1.access.STATE, actions).withDescription('Triggered action (e.g. a button click)').withCategory('diagnostic'), ]; const actionPayloadLookup = { 'commandUpOpen': 'open', 'commandDownClose': 'close', 'commandStop': 'stop', }; const fromZigbee = [ { cluster: 'closuresWindowCovering', type: ['commandUpOpen', 'commandDownClose', 'commandStop'], 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; }, }, ]; if (args.legacyAction) { fromZigbee.push(...[globalLegacy.fromZigbee.cover_open, globalLegacy.fromZigbee.cover_close, globalLegacy.fromZigbee.cover_stop]); } const result = { exposes, fromZigbee, isModernExtend: true }; if (args.bind) result.configure = setupConfigureForBinding('closuresWindowCovering', 'output', args.endpointNames); return result; } exports.commandsWindowCovering = commandsWindowCovering; function iasZoneAlarm(args) { const exposeList = { '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'), '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'), 'battery_defect': exposes_1.presets.binary('battery_defect', exposes_1.access.STATE, true, false).withDescription('Indicates whether the device battery is defective') .withCategory('diagnostic'), }; const exposes = []; const invertAlarmPayload = args.zoneType === 'contact'; const bothAlarms = args.zoneAttributes.includes('alarm_1') && (args.zoneAttributes.includes('alarm_2')); let alarm1Name = 'alarm_1'; let alarm2Name = 'alarm_2'; if (args.zoneType === 'generic') { args.zoneAttributes.map((attr) => exposes.push(exposeList[attr])); } else { if (bothAlarms) { exposes.push(exposes_1.presets.binary(args.zoneType + '_alarm_1', exposes_1.access.STATE, true, false) .withDescription(exposeList[args.zoneType].description + ' (alarm_1)')); alarm1Name = args.zoneType + '_alarm_1'; exposes.push(exposes_1.presets.binary(args.zoneType + '_alarm_2', exposes_1.access.STATE, true, false) .withDescription(exposeList[args.zoneType].description + ' (alarm_2)')); alarm2Name = args.zoneType + '_alarm_2'; } else { exposes.push(exposeList[args.zoneType]); alarm1Name = args.zoneType; alarm2Name = args.zoneType; } args.zoneAttributes.map((attr) => { if (attr !== 'alarm_1' && attr !== 'alarm_2') exposes.push(exposeList[attr]); }); } const timeoutProperty = `${args.zoneType}_timeout`; const fromZigbee = [{ cluster: 'ssIasZone', type: ['commandStatusChangeNotification', 'attributeReport', 'readResponse'], options: args.alarmTimeout ? [exposes_1.presets.numeric(timeoutProperty, exposes_1.access.SET).withValueMin(0) .withDescription(`Time in seconds after which ${args.zoneType} is cleared after detecting it (default 90 seconds).`)] : [], convert: (model, msg, publish, options, meta) => { const zoneStatus = msg.type === 'commandStatusChangeNotification' ? msg.data.zonestatus : msg.data.zoneStatus; if (args.alarmTimeout) { const timeout = options?.hasOwnProperty(timeoutProperty) ? Number(options[timeoutProperty]) : 90; clearTimeout(globalStore.getValue(msg.endpoint, 'timer')); if (timeout !== 0) { const timer = setTimeout(() => publish({ [alarm1Name]: false, [alarm2Name]: false }), timeout * 1000); globalStore.putValue(msg.endpoint, 'timer', timer); } } let payload = { tamper: (zoneStatus & 1 << 2) > 0, battery_low: (zoneStatus & 1 << 3) > 0, supervision_reports: (zoneStatus & 1 << 4) > 0, restore_reports: (zoneStatus & 1 << 5) > 0, trouble: (zoneStatus & 1 << 6) > 0, ac_status: (zoneStatus & 1 << 7) > 0, test: (zoneStatus & 1 << 8) > 0, battery_defect: (zoneStatus & 1 << 9) > 0, }; let alarm1Payload = (zoneStatus & 1) > 0; let alarm2Payload = (zoneStatus & 1 << 1) > 0; if (invertAlarmPayload) { alarm1Payload = !alarm1Payload; alarm2Payload = !alarm2Payload; } if (bothAlarms) { payload = { [alarm1Name]: alarm1Payload, ...payload }; payload = { [alarm2Name]: alarm2Payload, ...payload }; } else if (args.zoneAttributes.includes('alarm_1')) { payload = { [alarm1Name]: alarm1Payload, ...payload }; } else if (args.zoneAttributes.includes('alarm_2')) { payload = { [alarm2Name]: alarm2Payload, ...payload }; } return payload; }, }]; return { fromZigbee, exposes, isModernExtend: true }; } exports.iasZoneAlarm = iasZoneAlarm; function iasWarning(args) { const warningMode = { 'stop': 0, 'burglar': 1, 'fire': 2, 'emergency': 3, 'police_panic': 4, 'fire_panic': 5, 'emergency_panic': 6 }; // levels for siren, strobe and squawk are identical const level = { 'low': 0, 'medium': 1, 'high': 2, 'very_high': 3 }; const exposes = [ exposes_1.presets.composite('warning', 'warning', exposes_1.access.SET) .withFeature(exposes_1.presets.enum('mode', exposes_1.access.SET, Object.keys(warningMode)).withDescription('Mode of the warning (sound effect)')) .withFeature(exposes_1.presets.enum('level', exposes_1.access.SET, Object.keys(level)).withDescription('Sound level')) .withFeature(exposes_1.presets.enum('strobe_level', exposes_1.access.SET, Object.keys(level)).withDescription('Intensity of the strobe')) .withFeature(exposes_1.presets.binary('strobe', exposes_1.access.SET, true, false).withDescription('Turn on/off the strobe (light) during warning')) .withFeature(exposes_1.presets.numeric('strobe_duty_cycle', exposes_1.access.SET).withValueMax(10).withValueMin(0).withDescription('Length of the flash cycle')) .withFeature(exposes_1