UNPKG

zigbee-herdsman-converters

Version:

Collection of device converters to be used with zigbee-herdsman

543 lines • 27.1 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.externalDefinitions = exports.clearGlobalStore = exports.getConfigureKey = exports.setLogger = exports.ota = exports.fromZigbee = exports.toZigbee = exports.Fan = exports.Cover = exports.Lock = exports.Switch = exports.Climate = exports.Light = exports.List = exports.Composite = exports.Text = exports.Enum = exports.Binary = exports.Numeric = exports.access = void 0; exports.removeExternalDefinitions = removeExternalDefinitions; exports.addExternalDefinition = addExternalDefinition; exports.prepareDefinition = prepareDefinition; exports.postProcessConvertedFromZigbeeMessage = postProcessConvertedFromZigbeeMessage; exports.findByDevice = findByDevice; exports.findDefinition = findDefinition; exports.generateExternalDefinitionSource = generateExternalDefinitionSource; exports.generateExternalDefinition = generateExternalDefinition; exports.onEvent = onEvent; const node_assert_1 = __importDefault(require("node:assert")); const zigbee_herdsman_1 = require("zigbee-herdsman"); const fromZigbee = __importStar(require("./converters/fromZigbee")); exports.fromZigbee = fromZigbee; const toZigbee = __importStar(require("./converters/toZigbee")); exports.toZigbee = toZigbee; const exposes_1 = require("./lib/exposes"); Object.defineProperty(exports, "Binary", { enumerable: true, get: function () { return exposes_1.Binary; } }); Object.defineProperty(exports, "Climate", { enumerable: true, get: function () { return exposes_1.Climate; } }); Object.defineProperty(exports, "Composite", { enumerable: true, get: function () { return exposes_1.Composite; } }); Object.defineProperty(exports, "Cover", { enumerable: true, get: function () { return exposes_1.Cover; } }); Object.defineProperty(exports, "Enum", { enumerable: true, get: function () { return exposes_1.Enum; } }); Object.defineProperty(exports, "Fan", { enumerable: true, get: function () { return exposes_1.Fan; } }); Object.defineProperty(exports, "Light", { enumerable: true, get: function () { return exposes_1.Light; } }); Object.defineProperty(exports, "List", { enumerable: true, get: function () { return exposes_1.List; } }); Object.defineProperty(exports, "Lock", { enumerable: true, get: function () { return exposes_1.Lock; } }); Object.defineProperty(exports, "Numeric", { enumerable: true, get: function () { return exposes_1.Numeric; } }); Object.defineProperty(exports, "Switch", { enumerable: true, get: function () { return exposes_1.Switch; } }); Object.defineProperty(exports, "Text", { enumerable: true, get: function () { return exposes_1.Text; } }); Object.defineProperty(exports, "access", { enumerable: true, get: function () { return exposes_1.access; } }); const exposesLib = __importStar(require("./lib/exposes")); const generateDefinition_1 = require("./lib/generateDefinition"); const logger_1 = require("./lib/logger"); const utils = __importStar(require("./lib/utils")); // @ts-expect-error dynamically built const models_index_json_1 = __importDefault(require("./models-index.json")); const NS = "zhc"; const MODELS_INDEX = models_index_json_1.default; exports.ota = __importStar(require("./lib/ota")); var logger_2 = require("./lib/logger"); Object.defineProperty(exports, "setLogger", { enumerable: true, get: function () { return logger_2.setLogger; } }); var configureKey_1 = require("./lib/configureKey"); Object.defineProperty(exports, "getConfigureKey", { enumerable: true, get: function () { return configureKey_1.getConfigureKey; } }); var store_1 = require("./lib/store"); Object.defineProperty(exports, "clearGlobalStore", { enumerable: true, get: function () { return store_1.clear; } }); // key: zigbeeModel, value: array of definitions (most of the times 1) const externalDefinitionsLookup = new Map(); exports.externalDefinitions = []; // expected to be at the beginning of `definitions` array let externalDefinitionsCount = 0; 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 addToExternalDefinitionsLookup(zigbeeModel, definition) { const lookupModel = zigbeeModel ? zigbeeModel.toLowerCase() : "null"; if (!externalDefinitionsLookup.has(lookupModel)) { externalDefinitionsLookup.set(lookupModel, []); } // key created above // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` if (!externalDefinitionsLookup.get(lookupModel).includes(definition)) { // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` externalDefinitionsLookup.get(lookupModel).splice(0, 0, definition); } } function removeFromExternalDefinitionsLookup(zigbeeModel, definition) { const lookupModel = zigbeeModel ? zigbeeModel.toLowerCase() : "null"; if (externalDefinitionsLookup.has(lookupModel)) { // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` const i = externalDefinitionsLookup.get(lookupModel).indexOf(definition); if (i > -1) { // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` externalDefinitionsLookup.get(lookupModel).splice(i, 1); } // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` if (externalDefinitionsLookup.get(lookupModel).length === 0) { externalDefinitionsLookup.delete(lookupModel); } } } function getFromExternalDefinitionsLookup(zigbeeModel) { const lookupModel = zigbeeModel ? zigbeeModel.toLowerCase() : "null"; if (externalDefinitionsLookup.has(lookupModel)) { return externalDefinitionsLookup.get(lookupModel); } return externalDefinitionsLookup.get(lookupModel.replace(/\0(.|\n)*$/g, "").trim()); } function removeExternalDefinitions(converterName) { for (let i = 0; i < externalDefinitionsCount; i++) { const definition = exports.externalDefinitions[i]; if (converterName && definition.externalConverterName !== converterName) { continue; } if (definition.zigbeeModel) { for (const zigbeeModel of definition.zigbeeModel) { removeFromExternalDefinitionsLookup(zigbeeModel, definition); } } if (definition.fingerprint) { for (const fingerprint of definition.fingerprint) { removeFromExternalDefinitionsLookup(fingerprint.modelID, definition); } } exports.externalDefinitions.splice(i, 1); externalDefinitionsCount--; i--; } } function addExternalDefinition(definition) { exports.externalDefinitions.splice(0, 0, definition); externalDefinitionsCount++; if (definition.fingerprint) { for (const fingerprint of definition.fingerprint) { addToExternalDefinitionsLookup(fingerprint.modelID, definition); } } if (definition.zigbeeModel) { for (const zigbeeModel of definition.zigbeeModel) { addToExternalDefinitionsLookup(zigbeeModel, definition); } } } async function getDefinitions(indexes) { const indexedDefs = []; // local cache for models with lots of matches (tuya...) const defs = {}; for (const [moduleName, index] of indexes) { if (!defs[moduleName]) { // NOTE: modules are cached by nodejs until process is stopped // currently using `commonjs`, so strip `.js` file extension, XXX: creates a warning with vitest (expects static `.js`) const { definitions } = (await Promise.resolve(`${`./devices/${moduleName.slice(0, -3)}`}`).then(s => __importStar(require(s)))); defs[moduleName] = definitions; } indexedDefs.push(defs[moduleName][index]); } return indexedDefs; } async function getFromIndex(zigbeeModel) { const lookupModel = zigbeeModel ? zigbeeModel.toLowerCase() : "null"; let indexes = MODELS_INDEX[lookupModel]; if (indexes) { logger_1.logger.debug(`Getting definitions for: ${indexes}`, NS); return await getDefinitions(indexes); } indexes = MODELS_INDEX[lookupModel.replace(/\0(.|\n)*$/g, "").trim()]; if (indexes) { logger_1.logger.debug(`Getting definitions for: ${indexes}`, NS); return await getDefinitions(indexes); } } const converterRequiredFields = { model: "String", vendor: "String", description: "String", fromZigbee: "Array", toZigbee: "Array", }; function validateDefinition(definition) { for (const [field, expectedType] of Object.entries(converterRequiredFields)) { const val = definition[field]; (0, node_assert_1.default)(val !== null, `Converter field ${field} is null`); (0, node_assert_1.default)(val !== undefined, `Converter field ${field} is undefined`); (0, node_assert_1.default)(val.constructor.name === expectedType, `Converter field ${field} expected type doenst match to ${val}`); } node_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)) { node_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: definitionExposes, meta, endpoint, ota, configure: definitionConfigure, onEvent: definitionOnEvent, ...definitionWithoutExtend } = definition; // Exposes can be an Expose[] or DefinitionExposesFunction. In case it's only Expose[] we return an array // Otherwise return a DefinitionExposesFunction. const allExposesIsExposeOnly = (allExposes) => { return !allExposes.find((e) => typeof e === "function"); }; let allExposes = []; if (definitionExposes) { if (typeof definitionExposes === "function") { allExposes.push(definitionExposes); } else { allExposes.push(...definitionExposes); } } toZigbee = [...(toZigbee ?? [])]; fromZigbee = [...(fromZigbee ?? [])]; const configures = definitionConfigure ? [definitionConfigure] : []; const onEvents = definitionOnEvent ? [definitionOnEvent] : []; for (const ext of extend) { if (!ext.isModernExtend) { node_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) { allExposes.push(...ext.exposes); } if (ext.meta) { meta = Object.assign({}, ext.meta, meta); } // Filter `undefined` configures, e.g. returned by setupConfigureForReporting. if (ext.configure) { configures.push(...ext.configure.filter((c) => c !== undefined)); } if (ext.onEvent) { onEvents.push(...ext.onEvent.filter((c) => c !== undefined)); } if (ext.ota) { ota = ext.ota; } if (ext.endpoint) { if (endpoint) { node_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 = allExposes.filter((e) => typeof e !== "function" && e.name === "action"); allExposes = allExposes.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); allExposes.push(exposesLib.presets.action(uniqueActions)); } let configure; if (configures.length !== 0) { configure = async (device, coordinatorEndpoint, configureDefinition) => { for (const func of configures) { await func(device, coordinatorEndpoint, configureDefinition); } }; } let onEvent; if (onEvents.length !== 0) { onEvent = async (type, data, device, settings, state) => { for (const func of onEvents) { await func(type, data, device, settings, state); } }; } // In case there is a function in allExposes, return a function, otherwise just an array. let exposes; if (allExposesIsExposeOnly(allExposes)) { exposes = allExposes; } else { exposes = (device, options) => { const result = []; for (const item of allExposes) { if (typeof item === "function") { try { const deviceExposes = item(device, options); result.push(...deviceExposes); } catch (error) { logger_1.logger.error(`Failed to process exposes for '${device.ieeeAddr}' (${error.stack})`, NS); } } else { result.push(item); } } return result; }; } return { toZigbee, fromZigbee, exposes, meta, configure, endpoint, onEvent, ota, ...definitionWithoutExtend }; } return { ...definition }; } function prepareDefinition(definition) { const finalDefinition = processExtensions(definition); finalDefinition.toZigbee = [ ...finalDefinition.toZigbee, toZigbee.scene_store, toZigbee.scene_recall, toZigbee.scene_add, toZigbee.scene_remove, toZigbee.scene_remove_all, toZigbee.scene_rename, toZigbee.read, toZigbee.write, toZigbee.command, toZigbee.factory_reset, toZigbee.zcl_command, ]; if (definition.externalConverterName) { validateDefinition(finalDefinition); } // Add all the options finalDefinition.options = [...(finalDefinition.options ?? [])]; const optionKeys = finalDefinition.options.map((o) => o.name); // Add calibration/precision options based on expose for (const expose of Array.isArray(finalDefinition.exposes) ? finalDefinition.exposes : finalDefinition.exposes(undefined, undefined)) { if (!optionKeys.includes(expose.name) && utils.isNumericExpose(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"; finalDefinition.options.push(exposesLib.options.calibration(expose.name, type)); if (utils.calibrateAndPrecisionRoundOptionsDefaultPrecision[expose.name] !== 0) { finalDefinition.options.push(exposesLib.options.precision(expose.name)); } optionKeys.push(expose.name); } } for (const converter of [...finalDefinition.toZigbee, ...finalDefinition.fromZigbee]) { if (converter.options) { const options = typeof converter.options === "function" ? converter.options(finalDefinition) : converter.options; for (const option of options) { if (!optionKeys.includes(option.name)) { finalDefinition.options.push(option); optionKeys.push(option.name); } } } } return finalDefinition; } 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(undefined, undefined); const expose = definitionExposes.find((e) => e.property === key); if (expose?.name && expose.name in utils.calibrateAndPrecisionRoundOptionsDefaultPrecision && value !== "" && utils.isNumber(value)) { try { payload[key] = utils.calibrateAndPrecisionRoundOptions(value, options, expose.name); } catch (error) { logger_1.logger.error(`Failed to apply calibration to '${expose.name}': ${error.message}`, NS); } } } } async function findByDevice(device, generateForUnknown = false) { let definition = await findDefinition(device, generateForUnknown); if (definition) { if (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 prepareDefinition(definition); } } async function findDefinition(device, generateForUnknown = false) { if (!device) { return undefined; } let candidates = await getFromIndex(device.modelID); if (externalDefinitionsCount > 0) { const extCandidates = getFromExternalDefinitionsLookup(device.modelID); if (extCandidates) { if (candidates) { candidates.unshift(...extCandidates); } else { candidates = extCandidates; } } } if (candidates) { if (candidates.length === 1 && candidates[0].zigbeeModel) { return candidates[0]; } logger_1.logger.debug(() => `Candidates for ${device.ieeeAddr}/${device.modelID}: ${candidates.map((c) => `${c.model}/${c.vendor}`)}`, NS); // First try to match based on fingerprint, return the first matching one. const fingerprintMatch = { priority: undefined, definition: undefined }; for (const candidate of candidates) { if (candidate.fingerprint) { for (const fingerprint of candidate.fingerprint) { const priority = fingerprint.priority ?? 0; if (isFingerprintMatch(fingerprint, device) && (fingerprintMatch.priority === undefined || 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 && device.modelID && candidate.zigbeeModel.includes(device.modelID)) { return candidate; } } } if (!generateForUnknown || device.type === "Coordinator") { return undefined; } const { definition } = await (0, generateDefinition_1.generateDefinition)(device); return definition; } async function generateExternalDefinitionSource(device) { return (await (0, generateDefinition_1.generateDefinition)(device)).externalDefinitionSource; } async function generateExternalDefinition(device) { const { definition } = await (0, generateDefinition_1.generateDefinition)(device); return prepareDefinition(definition); } function isFingerprintMatch(fingerprint, device) { let match = (fingerprint.applicationVersion === undefined || device.applicationVersion === fingerprint.applicationVersion) && (fingerprint.manufacturerID === undefined || device.manufacturerID === fingerprint.manufacturerID) && (!fingerprint.type || device.type === fingerprint.type) && (!fingerprint.dateCode || device.dateCode === fingerprint.dateCode) && (fingerprint.hardwareVersion === undefined || 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 === undefined || device.stackVersion === fingerprint.stackVersion) && (fingerprint.zclVersion === undefined || device.zclVersion === fingerprint.zclVersion) && (!fingerprint.ieeeAddr || device.ieeeAddr.match(fingerprint.ieeeAddr) !== null) && (!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 = fingerprintEndpoint.ID !== undefined ? device.getEndpoint(fingerprintEndpoint.ID) : undefined; match = match && (fingerprintEndpoint.deviceID === undefined || (deviceEndpoint !== undefined && deviceEndpoint.deviceID === fingerprintEndpoint.deviceID)) && (fingerprintEndpoint.profileID === undefined || (deviceEndpoint !== undefined && deviceEndpoint.profileID === fingerprintEndpoint.profileID)) && (!fingerprintEndpoint.inputClusters || (deviceEndpoint !== undefined && arrayEquals(deviceEndpoint.inputClusters, fingerprintEndpoint.inputClusters))) && (!fingerprintEndpoint.outputClusters || (deviceEndpoint !== undefined && arrayEquals(deviceEndpoint.outputClusters, fingerprintEndpoint.outputClusters))); } } return match; } // 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, meta) { // 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 = { 61440: { value: 23, type: 35 } }; endpoint.readResponse("genBasic", frame.header.transactionSequenceNumber, payload, options).catch((e) => { logger_1.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_1.logger.warning(`ZNCWWSQ01LM custom time response failed: ${e}`, NS); }); return true; } return false; }; } } //# sourceMappingURL=index.js.map