UNPKG

zigbee-herdsman-converters

Version:

Collection of device converters to be used with zigbee-herdsman

390 lines • 19.5 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.setLogger = exports.onEvent = exports.findByModel = exports.generateExternalDefinitionSource = exports.findDefinition = exports.findByDevice = exports.addDefinition = exports.postProcessConvertedFromZigbeeMessage = exports.definitions = exports.getConfigureKey = exports.ota = exports.fromZigbee = exports.toZigbee = void 0; const configureKey = __importStar(require("./lib/configureKey")); const exposesLib = __importStar(require("./lib/exposes")); const exposes_1 = require("./lib/exposes"); const toZigbee_1 = __importDefault(require("./converters/toZigbee")); exports.toZigbee = toZigbee_1.default; const fromZigbee_1 = __importDefault(require("./converters/fromZigbee")); exports.fromZigbee = fromZigbee_1.default; const assert_1 = __importDefault(require("assert")); const ota = __importStar(require("./lib/ota")); exports.ota = ota; const devices_1 = __importDefault(require("./devices")); const utils = __importStar(require("./lib/utils")); const generateDefinition_1 = require("./lib/generateDefinition"); const zigbee_herdsman_1 = require("zigbee-herdsman"); const logger = __importStar(require("./lib/logger")); const NS = 'zhc'; exports.getConfigureKey = configureKey.getConfigureKey; // key: zigbeeModel, value: array of definitions (most of the times 1) const lookup = new Map(); exports.definitions = []; function arrayEquals(as, bs) { if (as.length !== bs.length) return false; for (const a of as) if (!bs.includes(a)) return false; return true; } function addToLookup(zigbeeModel, definition) { zigbeeModel = zigbeeModel ? zigbeeModel.toLowerCase() : null; if (!lookup.has(zigbeeModel)) { lookup.set(zigbeeModel, []); } if (!lookup.get(zigbeeModel).includes(definition)) { lookup.get(zigbeeModel).splice(0, 0, definition); } } function getFromLookup(zigbeeModel) { zigbeeModel = zigbeeModel ? zigbeeModel.toLowerCase() : null; if (lookup.has(zigbeeModel)) { return lookup.get(zigbeeModel); } zigbeeModel = zigbeeModel ? zigbeeModel.replace(/\0(.|\n)*$/g, '').trim() : null; return lookup.get(zigbeeModel); } const converterRequiredFields = { model: 'String', vendor: 'String', description: 'String', fromZigbee: 'Array', toZigbee: 'Array', }; function validateDefinition(definition) { for (const [field, expectedType] of Object.entries(converterRequiredFields)) { // @ts-expect-error assert_1.default.notStrictEqual(null, definition[field], `Converter field ${field} is null`); // @ts-expect-error assert_1.default.notStrictEqual(undefined, definition[field], `Converter field ${field} is undefined`); // @ts-expect-error const msg = `Converter field ${field} expected type doenst match to ${definition[field]}`; // @ts-expect-error assert_1.default.strictEqual(definition[field].constructor.name, expectedType, msg); } assert_1.default.ok(Array.isArray(definition.exposes) || typeof definition.exposes === 'function', 'Exposes incorrect'); } function processExtensions(definition) { if ('extend' in definition) { if (!Array.isArray(definition.extend)) { assert_1.default.fail(`'${definition.model}' has legacy extend which is not supported anymore`); } // Modern extend, merges properties, e.g. when both extend and definition has toZigbee, toZigbee will be combined let { extend, toZigbee, fromZigbee, exposes, meta, endpoint, configure: definitionConfigure, onEvent: definitionOnEvent, ota, ...definitionWithoutExtend } = definition; if (typeof exposes === 'function') { assert_1.default.fail(`'${definition.model}' has function exposes which is not allowed`); } exposes = [...exposes ?? []]; toZigbee = [...toZigbee ?? []]; fromZigbee = [...fromZigbee ?? []]; const configures = definitionConfigure ? [definitionConfigure] : []; const onEvents = definitionOnEvent ? [definitionOnEvent] : []; for (const ext of extend) { if (!ext.isModernExtend) { assert_1.default.fail(`'${definition.model}' has legacy extend in modern extend`); } if (ext.toZigbee) toZigbee.push(...ext.toZigbee); if (ext.fromZigbee) fromZigbee.push(...ext.fromZigbee); if (ext.exposes) exposes.push(...ext.exposes); if (ext.meta) meta = { ...ext.meta, ...meta }; if (ext.configure) configures.push(ext.configure); if (ext.onEvent) onEvents.push(ext.onEvent); if (ext.ota) { if (ota && ext.ota !== ota) { assert_1.default.fail(`'${definition.model}' has multiple 'ota', this is not allowed`); } ota = ext.ota; } if (ext.endpoint) { if (endpoint) { assert_1.default.fail(`'${definition.model}' has multiple 'endpoint', this is not allowed`); } endpoint = ext.endpoint; } } // Filtering out action exposes to combine them one const actionExposes = exposes.filter((e) => e.name === 'action'); exposes = exposes.filter((e) => e.name !== 'action'); if (actionExposes.length > 0) { const actions = []; for (const expose of actionExposes) { if (expose instanceof exposes_1.Enum) { for (const action of expose.values) { actions.push(action.toString()); } } } const uniqueActions = actions.filter((value, index, array) => array.indexOf(value) === index); exposes.push(exposesLib.presets.action(uniqueActions)); } let configure = null; if (configures.length !== 0) { configure = async (device, coordinatorEndpoint, configureDefinition) => { for (const func of configures) { await func(device, coordinatorEndpoint, configureDefinition); } }; } let onEvent = null; if (onEvents.length !== 0) { onEvent = async (type, data, device, settings, state) => { for (const func of onEvents) { await func(type, data, device, settings, state); } }; } definition = { toZigbee, fromZigbee, exposes, meta, configure, endpoint, onEvent, ota, ...definitionWithoutExtend }; } return definition; } function prepareDefinition(definition) { definition = processExtensions(definition); definition.toZigbee.push(toZigbee_1.default.scene_store, toZigbee_1.default.scene_recall, toZigbee_1.default.scene_add, toZigbee_1.default.scene_remove, toZigbee_1.default.scene_remove_all, toZigbee_1.default.scene_rename, toZigbee_1.default.read, toZigbee_1.default.write, toZigbee_1.default.command, toZigbee_1.default.factory_reset, toZigbee_1.default.zcl_command); if (definition.exposes && Array.isArray(definition.exposes) && !definition.exposes.find((e) => e.name === 'linkquality')) { definition.exposes = definition.exposes.concat([exposesLib.presets.linkquality()]); } validateDefinition(definition); // Add all the options if (!definition.options) definition.options = []; const optionKeys = definition.options.map((o) => o.name); // Add calibration/precision options based on expose for (const expose of Array.isArray(definition.exposes) ? definition.exposes : definition.exposes(null, null)) { if (!optionKeys.includes(expose.name) && utils.isNumericExposeFeature(expose) && expose.name in utils.calibrateAndPrecisionRoundOptionsDefaultPrecision) { // Battery voltage is not calibratable if (expose.name === 'voltage' && expose.unit === 'mV') continue; const type = utils.calibrateAndPrecisionRoundOptionsIsPercentual(expose.name) ? 'percentual' : 'absolute'; definition.options.push(exposesLib.options.calibration(expose.name, type)); if (utils.calibrateAndPrecisionRoundOptionsDefaultPrecision[expose.name] !== 0) { definition.options.push(exposesLib.options.precision(expose.name)); } optionKeys.push(expose.name); } } for (const converter of [...definition.toZigbee, ...definition.fromZigbee]) { if (converter.options) { const options = typeof converter.options === 'function' ? converter.options(definition) : converter.options; for (const option of options) { if (!optionKeys.includes(option.name)) { definition.options.push(option); optionKeys.push(option.name); } } } } return definition; } function postProcessConvertedFromZigbeeMessage(definition, payload, options) { // Apply calibration/precision options for (const [key, value] of Object.entries(payload)) { const definitionExposes = Array.isArray(definition.exposes) ? definition.exposes : definition.exposes(null, null); const expose = definitionExposes.find((e) => e.property === key); if (expose?.name in utils.calibrateAndPrecisionRoundOptionsDefaultPrecision && utils.isNumber(value)) { try { payload[key] = utils.calibrateAndPrecisionRoundOptions(value, options, expose.name); } catch (error) { logger.logger.error(`Failed to apply calibration to '${expose.name}': ${error.message}`, NS); } } } } exports.postProcessConvertedFromZigbeeMessage = postProcessConvertedFromZigbeeMessage; function addDefinition(definition) { definition = prepareDefinition(definition); exports.definitions.splice(0, 0, definition); if ('fingerprint' in definition) { for (const fingerprint of definition.fingerprint) { addToLookup(fingerprint.modelID, definition); } } if ('zigbeeModel' in definition) { for (const zigbeeModel of definition.zigbeeModel) { addToLookup(zigbeeModel, definition); } } } exports.addDefinition = addDefinition; for (const definition of devices_1.default) { addDefinition(definition); } async function findByDevice(device, generateForUnknown = false) { let definition = await findDefinition(device, generateForUnknown); if (definition && definition.whiteLabel) { const match = definition.whiteLabel.find((w) => 'fingerprint' in w && w.fingerprint.find((f) => isFingerprintMatch(f, device))); if (match) { definition = { ...definition, model: match.model, vendor: match.vendor, description: match.description || definition.description, }; } } return definition; } exports.findByDevice = findByDevice; async function findDefinition(device, generateForUnknown = false) { if (!device) { return null; } const candidates = getFromLookup(device.modelID); if (!candidates) { if (!generateForUnknown || device.type === 'Coordinator') { return null; } // Do not add this definition to cache, // as device configuration might change. return prepareDefinition((await (0, generateDefinition_1.generateDefinition)(device)).definition); } else if (candidates.length === 1 && candidates[0].zigbeeModel) { return candidates[0]; } else { // First try to match based on fingerprint, return the first matching one. const fingerprintMatch = { priority: null, definition: null }; for (const candidate of candidates) { if (candidate.fingerprint) { for (const fingerprint of candidate.fingerprint) { const priority = fingerprint.priority ?? 0; if (isFingerprintMatch(fingerprint, device) && (!fingerprintMatch.definition || priority > fingerprintMatch.priority)) { fingerprintMatch.definition = candidate; fingerprintMatch.priority = priority; } } } } if (fingerprintMatch.definition) { return fingerprintMatch.definition; } // Match based on fingerprint failed, return first matching definition based on zigbeeModel for (const candidate of candidates) { if (candidate.zigbeeModel && candidate.zigbeeModel.includes(device.modelID)) { return candidate; } } } return null; } exports.findDefinition = findDefinition; async function generateExternalDefinitionSource(device) { return (await (0, generateDefinition_1.generateDefinition)(device)).externalDefinitionSource; } exports.generateExternalDefinitionSource = generateExternalDefinitionSource; function isFingerprintMatch(fingerprint, device) { let match = (!fingerprint.applicationVersion || device.applicationVersion === fingerprint.applicationVersion) && (!fingerprint.manufacturerID || device.manufacturerID === fingerprint.manufacturerID) && (!fingerprint.type || device.type === fingerprint.type) && (!fingerprint.dateCode || device.dateCode === fingerprint.dateCode) && (!fingerprint.hardwareVersion || device.hardwareVersion === fingerprint.hardwareVersion) && (!fingerprint.manufacturerName || device.manufacturerName === fingerprint.manufacturerName) && (!fingerprint.modelID || device.modelID === fingerprint.modelID) && (!fingerprint.powerSource || device.powerSource === fingerprint.powerSource) && (!fingerprint.softwareBuildID || device.softwareBuildID === fingerprint.softwareBuildID) && (!fingerprint.stackVersion || device.stackVersion === fingerprint.stackVersion) && (!fingerprint.zclVersion || device.zclVersion === fingerprint.zclVersion) && (!fingerprint.ieeeAddr || device.ieeeAddr.match(fingerprint.ieeeAddr)) && (!fingerprint.endpoints || arrayEquals(device.endpoints.map((e) => e.ID), fingerprint.endpoints.map((e) => e.ID))); if (match && fingerprint.endpoints) { for (const fingerprintEndpoint of fingerprint.endpoints) { const deviceEndpoint = device.getEndpoint(fingerprintEndpoint.ID); match = match && (!fingerprintEndpoint.deviceID || deviceEndpoint.deviceID === fingerprintEndpoint.deviceID) && (!fingerprintEndpoint.profileID || deviceEndpoint.profileID === fingerprintEndpoint.profileID) && (!fingerprintEndpoint.inputClusters || arrayEquals(deviceEndpoint.inputClusters, fingerprintEndpoint.inputClusters)) && (!fingerprintEndpoint.outputClusters || arrayEquals(deviceEndpoint.outputClusters, fingerprintEndpoint.outputClusters)); } } return match; } function findByModel(model) { /* Search device description by definition model name. Useful when redefining, expanding device descriptions in external converters. */ model = model.toLowerCase(); return exports.definitions.find((definition) => { const whiteLabelMatch = definition.whiteLabel && definition.whiteLabel.find((dd) => dd.model.toLowerCase() === model); return definition.model.toLowerCase() == model || whiteLabelMatch; }); } exports.findByModel = findByModel; // Can be used to handle events for devices which are not fully paired yet (no modelID). // Example usecase: https://github.com/Koenkk/zigbee2mqtt/issues/2399#issuecomment-570583325 async function onEvent(type, data, device) { // support Legrand security protocol // when pairing, a powered device will send a read frame to every device on the network // it expects at least one answer. The payload contains the number of seconds // since when the device is powered. If the value is too high, it will leave & not pair // 23 works, 200 doesn't if (device.manufacturerID === zigbee_herdsman_1.Zcl.ManufacturerCode.LEGRAND_GROUP && !device.customReadResponse) { device.customReadResponse = (frame, endpoint) => { if (frame.isCluster('genBasic') && frame.payload.find((i) => i.attrId === 61440)) { const options = { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.LEGRAND_GROUP, disableDefaultResponse: true }; const payload = { 0xf00: { value: 23, type: 35 } }; endpoint.readResponse('genBasic', frame.header.transactionSequenceNumber, payload, options).catch((e) => { logger.logger.warning(`Legrand security read response failed: ${e}`, NS); }); return true; } return false; }; } // Aqara feeder C1 polls the time during the interview, need to send back the local time instead of the UTC. // The device.definition has not yet been set - therefore the device.definition.onEvent method does not work. if (device.modelID === 'aqara.feeder.acn001' && !device.customReadResponse) { device.customReadResponse = (frame, endpoint) => { if (frame.isCluster('genTime')) { const oneJanuary2000 = new Date('January 01, 2000 00:00:00 UTC+00:00').getTime(); const secondsUTC = Math.round(((new Date()).getTime() - oneJanuary2000) / 1000); const secondsLocal = secondsUTC - (new Date()).getTimezoneOffset() * 60; endpoint.readResponse('genTime', frame.header.transactionSequenceNumber, { time: secondsLocal }).catch((e) => { logger.logger.warning(`ZNCWWSQ01LM custom time response failed: ${e}`, NS); }); return true; } return false; }; } } exports.onEvent = onEvent; exports.setLogger = logger.setLogger; //# sourceMappingURL=index.js.map