UNPKG

zigbee-herdsman

Version:

An open source ZigBee gateway solution with node.js.

690 lines 35.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 () { 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.Endpoint = void 0; const node_assert_1 = __importDefault(require("node:assert")); const logger_1 = require("../../utils/logger"); const ZSpec = __importStar(require("../../zspec")); const enums_1 = require("../../zspec/enums"); const Zcl = __importStar(require("../../zspec/zcl")); const Zdo = __importStar(require("../../zspec/zdo")); const request_1 = __importDefault(require("../helpers/request")); const requestQueue_1 = __importDefault(require("../helpers/requestQueue")); const ZclFrameConverter = __importStar(require("../helpers/zclFrameConverter")); const zclTransactionSequenceNumber_1 = __importDefault(require("../helpers/zclTransactionSequenceNumber")); const device_1 = __importDefault(require("./device")); const entity_1 = __importDefault(require("./entity")); const group_1 = __importDefault(require("./group")); const NS = "zh:controller:endpoint"; class Endpoint extends entity_1.default { deviceID; inputClusters; outputClusters; profileID; // biome-ignore lint/style/useNamingConvention: cross-repo impact ID; clusters; deviceIeeeAddress; deviceNetworkAddress; _binds; _configuredReportings; meta; pendingRequests; // Getters/setters get binds() { const binds = []; for (const bind of this._binds) { // XXX: properties assumed valid when associated to `type` const target = // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` bind.type === "endpoint" ? device_1.default.byIeeeAddr(bind.deviceIeeeAddress)?.getEndpoint(bind.endpointID) : group_1.default.byGroupID(bind.groupID); if (target) { binds.push({ target, cluster: this.getCluster(bind.cluster) }); } } return binds; } get configuredReportings() { const device = this.getDevice(); return this._configuredReportings.map((entry, index) => { const cluster = Zcl.Utils.getCluster(entry.cluster, entry.manufacturerCode, device.customClusters); const attribute = cluster.hasAttribute(entry.attrId) ? cluster.getAttribute(entry.attrId) : { ID: entry.attrId, name: `attr${index}`, type: Zcl.DataType.UNKNOWN, manufacturerCode: undefined }; return { cluster, attribute, minimumReportInterval: entry.minRepIntval, maximumReportInterval: entry.maxRepIntval, reportableChange: entry.repChange, }; }); } constructor(id, profileID, deviceID, inputClusters, outputClusters, deviceNetworkAddress, deviceIeeeAddress, clusters, binds, configuredReportings, meta) { super(); this.ID = id; this.profileID = profileID; this.deviceID = deviceID; this.inputClusters = inputClusters; this.outputClusters = outputClusters; this.deviceNetworkAddress = deviceNetworkAddress; this.deviceIeeeAddress = deviceIeeeAddress; this.clusters = clusters; this._binds = binds; this._configuredReportings = configuredReportings; this.meta = meta; this.pendingRequests = new requestQueue_1.default(this); } /** * Get device of this endpoint */ getDevice() { const device = device_1.default.byIeeeAddr(this.deviceIeeeAddress); if (!device) { logger_1.logger.error(`Tried to get unknown/deleted device ${this.deviceIeeeAddress} from endpoint ${this.ID}.`, NS); // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` logger_1.logger.debug(new Error().stack, NS); } // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` return device; } /** * @param {number|string} clusterKey * @returns {boolean} */ supportsInputCluster(clusterKey) { const cluster = this.getCluster(clusterKey); return this.inputClusters.includes(cluster.ID); } /** * @param {number|string} clusterKey * @returns {boolean} */ supportsOutputCluster(clusterKey) { const cluster = this.getCluster(clusterKey); return this.outputClusters.includes(cluster.ID); } /** * @returns {ZclTypes.Cluster[]} */ getInputClusters() { return this.clusterNumbersToClusters(this.inputClusters); } /** * @returns {ZclTypes.Cluster[]} */ getOutputClusters() { return this.clusterNumbersToClusters(this.outputClusters); } clusterNumbersToClusters(clusterNumbers) { return clusterNumbers.map((c) => this.getCluster(c)); } /* * CRUD */ static fromDatabaseRecord(record, deviceNetworkAddress, deviceIeeeAddress) { // Migrate attrs to attributes for (const entryKey in record.clusters) { const entry = record.clusters[entryKey]; if (entry.attrs != null) { entry.attributes = entry.attrs; delete entry.attrs; } } return new Endpoint(record.epId, record.profId, record.devId, record.inClusterList, record.outClusterList, deviceNetworkAddress, deviceIeeeAddress, record.clusters, record.binds || [], record.configuredReportings || [], record.meta || {}); } toDatabaseRecord() { return { profId: this.profileID, epId: this.ID, devId: this.deviceID, inClusterList: this.inputClusters, outClusterList: this.outputClusters, clusters: this.clusters, binds: this._binds, configuredReportings: this._configuredReportings, meta: this.meta, }; } static create(id, profileID, deviceID, inputClusters, outputClusters, deviceNetworkAddress, deviceIeeeAddress) { return new Endpoint(id, profileID, deviceID, inputClusters, outputClusters, deviceNetworkAddress, deviceIeeeAddress, {}, [], [], {}); } saveClusterAttributeKeyValue(clusterKey, list) { const cluster = this.getCluster(clusterKey); if (!this.clusters[cluster.name]) this.clusters[cluster.name] = { attributes: {} }; for (const [attribute, value] of Object.entries(list)) { this.clusters[cluster.name].attributes[attribute] = value; } } getClusterAttributeValue(clusterKey, attributeKey) { const cluster = this.getCluster(clusterKey); const attribute = cluster.getAttribute(attributeKey); if (this.clusters[cluster.name] && this.clusters[cluster.name].attributes) { return this.clusters[cluster.name].attributes[attribute.name]; } return undefined; } hasPendingRequests() { return this.pendingRequests.size > 0; } async sendPendingRequests(fastPolling) { return await this.pendingRequests.send(fastPolling); } async sendRequest(frame, options, func = (d) => { // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` return entity_1.default.adapter.sendZclFrameToEndpoint(this.deviceIeeeAddress, this.deviceNetworkAddress, this.ID, d, options.timeout, options.disableResponse, options.disableRecovery, options.srcEndpoint); }) { const logPrefix = `Request Queue (${this.deviceIeeeAddress}/${this.ID}): `; const device = this.getDevice(); const request = new request_1.default(func, frame, device.pendingRequestTimeout, options.sendPolicy); if (request.sendPolicy !== "bulk") { // Check if such a request is already in the queue and remove the old one(s) if necessary this.pendingRequests.filter(request); } // send without queueing if sendPolicy is 'immediate' or if the device has no timeout set if (request.sendPolicy === "immediate" || !device.pendingRequestTimeout) { if (device.pendingRequestTimeout > 0) { logger_1.logger.debug(`${logPrefix}send ${frame.command.name} request immediately (sendPolicy=${options.sendPolicy})`, NS); } return await request.send(); } // If this is a bulk message, we queue directly. if (request.sendPolicy === "bulk") { logger_1.logger.debug(`${logPrefix}queue request (${this.pendingRequests.size})`, NS); return await this.pendingRequests.queue(request); } try { logger_1.logger.debug(`${logPrefix}send request`, NS); return await request.send(); } catch (error) { // If we got a failed transaction, the device is likely sleeping. // Queue for transmission later. logger_1.logger.debug(`${logPrefix}queue request (transaction failed) (${error})`, NS); return await this.pendingRequests.queue(request); } } /* * Zigbee functions */ checkStatus(payload) { const codes = Array.isArray(payload) ? payload.map((i) => i.status) : [payload.statusCode]; const invalid = codes.find((c) => c !== Zcl.Status.SUCCESS); if (invalid) throw new Zcl.StatusError(invalid); } async report(clusterKey, attributes, options) { const cluster = this.getCluster(clusterKey); const payload = []; for (const [nameOrID, value] of Object.entries(attributes)) { if (cluster.hasAttribute(nameOrID)) { const attribute = cluster.getAttribute(nameOrID); payload.push({ attrId: attribute.ID, attrData: value, dataType: attribute.type }); } else if (!Number.isNaN(Number(nameOrID))) { payload.push({ attrId: Number(nameOrID), attrData: value.value, dataType: value.type }); } else { throw new Error(`Unknown attribute '${nameOrID}', specify either an existing attribute or a number`); } } await this.zclCommand(clusterKey, "report", payload, options, attributes); } async write(clusterKey, attributes, options) { const cluster = this.getCluster(clusterKey); const optionsWithDefaults = this.getOptionsWithDefaults(options, true, Zcl.Direction.CLIENT_TO_SERVER, cluster.manufacturerCode); optionsWithDefaults.manufacturerCode = this.ensureManufacturerCodeIsUniqueAndGet(cluster, Object.keys(attributes), optionsWithDefaults.manufacturerCode, "write"); const payload = []; for (const [nameOrID, value] of Object.entries(attributes)) { if (cluster.hasAttribute(nameOrID)) { const attribute = cluster.getAttribute(nameOrID); payload.push({ attrId: attribute.ID, attrData: value, dataType: attribute.type }); } else if (!Number.isNaN(Number(nameOrID))) { payload.push({ attrId: Number(nameOrID), attrData: value.value, dataType: value.type }); } else { throw new Error(`Unknown attribute '${nameOrID}', specify either an existing attribute or a number`); } } await this.zclCommand(clusterKey, optionsWithDefaults.writeUndiv ? "writeUndiv" : "write", payload, optionsWithDefaults, attributes, true); } async writeResponse(clusterKey, transactionSequenceNumber, attributes, options) { (0, node_assert_1.default)(options?.transactionSequenceNumber === undefined, "Use parameter"); const cluster = this.getCluster(clusterKey); const payload = []; for (const [nameOrID, value] of Object.entries(attributes)) { if (value.status !== undefined) { if (cluster.hasAttribute(nameOrID)) { const attribute = cluster.getAttribute(nameOrID); payload.push({ attrId: attribute.ID, status: value.status }); } else if (!Number.isNaN(Number(nameOrID))) { payload.push({ attrId: Number(nameOrID), status: value.status }); } else { throw new Error(`Unknown attribute '${nameOrID}', specify either an existing attribute or a number`); } } else { throw new Error(`Missing attribute 'status'`); } } await this.zclCommand(clusterKey, "writeRsp", payload, { direction: Zcl.Direction.SERVER_TO_CLIENT, ...options, transactionSequenceNumber }, attributes); } async read(clusterKey, attributes, options) { const device = this.getDevice(); const cluster = this.getCluster(clusterKey, device); const optionsWithDefaults = this.getOptionsWithDefaults(options, true, Zcl.Direction.CLIENT_TO_SERVER, cluster.manufacturerCode); optionsWithDefaults.manufacturerCode = this.ensureManufacturerCodeIsUniqueAndGet(cluster, attributes, optionsWithDefaults.manufacturerCode, "read"); const payload = []; for (const attribute of attributes) { payload.push({ attrId: typeof attribute === "number" ? attribute : cluster.getAttribute(attribute).ID }); } const resultFrame = await this.zclCommand(clusterKey, "read", payload, optionsWithDefaults, attributes, true); if (resultFrame) { return ZclFrameConverter.attributeKeyValue(resultFrame, device.manufacturerID, device.customClusters); } return {}; } async readResponse(clusterKey, transactionSequenceNumber, attributes, options) { (0, node_assert_1.default)(options?.transactionSequenceNumber === undefined, "Use parameter"); const cluster = this.getCluster(clusterKey); const payload = []; for (const [nameOrID, value] of Object.entries(attributes)) { if (cluster.hasAttribute(nameOrID)) { const attribute = cluster.getAttribute(nameOrID); payload.push({ attrId: attribute.ID, attrData: value, dataType: attribute.type, status: 0 }); } else if (!Number.isNaN(Number(nameOrID))) { payload.push({ attrId: Number(nameOrID), attrData: value.value, dataType: value.type, status: 0 }); } else { throw new Error(`Unknown attribute '${nameOrID}', specify either an existing attribute or a number`); } } await this.zclCommand(clusterKey, "readRsp", payload, { direction: Zcl.Direction.SERVER_TO_CLIENT, ...options, transactionSequenceNumber }, attributes); } async updateSimpleDescriptor() { const clusterId = Zdo.ClusterId.SIMPLE_DESCRIPTOR_REQUEST; // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` const zdoPayload = Zdo.Buffalo.buildRequest(entity_1.default.adapter.hasZdoMessageOverhead, clusterId, this.deviceNetworkAddress, this.ID); // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` const response = await entity_1.default.adapter.sendZdo(this.deviceIeeeAddress, this.deviceNetworkAddress, clusterId, zdoPayload, false); if (!Zdo.Buffalo.checkStatus(response)) { throw new Zdo.StatusError(response[0]); } const simpleDescriptor = response[1]; this.profileID = simpleDescriptor.profileId; this.deviceID = simpleDescriptor.deviceId; this.inputClusters = simpleDescriptor.inClusterList; this.outputClusters = simpleDescriptor.outClusterList; } hasBind(clusterId, target) { return this.getBindIndex(clusterId, target) !== -1; } getBindIndex(clusterId, target) { return this.binds.findIndex((b) => b.cluster.ID === clusterId && b.target === target); } addBinding(clusterKey, target) { const cluster = this.getCluster(clusterKey); if (typeof target === "number") { target = group_1.default.byGroupID(target) || group_1.default.create(target); } this.addBindingInternal(cluster, target); } addBindingInternal(cluster, target) { if (!this.hasBind(cluster.ID, target)) { if (target instanceof group_1.default) { this._binds.push({ cluster: cluster.ID, groupID: target.groupID, type: "group" }); } else { this._binds.push({ cluster: cluster.ID, type: "endpoint", deviceIeeeAddress: target.deviceIeeeAddress, endpointID: target.ID, }); } this.save(); } } async bind(clusterKey, target) { const cluster = this.getCluster(clusterKey); if (typeof target === "number") { target = group_1.default.byGroupID(target) || group_1.default.create(target); } const destinationAddress = target instanceof Endpoint ? target.deviceIeeeAddress : target.groupID; const log = `Bind ${this.deviceIeeeAddress}/${this.ID} ${cluster.name} from '${target instanceof Endpoint ? `${destinationAddress}/${target.ID}` : destinationAddress}'`; logger_1.logger.debug(log, NS); try { const zdoClusterId = Zdo.ClusterId.BIND_REQUEST; const zdoPayload = Zdo.Buffalo.buildRequest( // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` entity_1.default.adapter.hasZdoMessageOverhead, zdoClusterId, this.deviceIeeeAddress, this.ID, cluster.ID, target instanceof Endpoint ? Zdo.UNICAST_BINDING : Zdo.MULTICAST_BINDING, target instanceof Endpoint ? target.deviceIeeeAddress : ZSpec.BLANK_EUI64, target instanceof group_1.default ? target.groupID : 0, target instanceof Endpoint ? target.ID : 0xff); // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` const response = await entity_1.default.adapter.sendZdo(this.deviceIeeeAddress, this.deviceNetworkAddress, zdoClusterId, zdoPayload, false); if (!Zdo.Buffalo.checkStatus(response)) { throw new Zdo.StatusError(response[0]); } this.addBindingInternal(cluster, target); } catch (error) { const err = error; err.message = `${log} failed (${err.message})`; // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` logger_1.logger.debug(err.stack, NS); throw error; } } save() { this.getDevice().save(); } async unbind(clusterKey, target) { const cluster = this.getCluster(clusterKey); const action = `Unbind ${this.deviceIeeeAddress}/${this.ID} ${cluster.name}`; if (typeof target === "number") { const groupTarget = group_1.default.byGroupID(target); if (!groupTarget) { throw new Error(`${action} invalid target '${target}' (no group with this ID exists).`); } target = groupTarget; } const destinationAddress = target instanceof Endpoint ? target.deviceIeeeAddress : target.groupID; const log = `${action} from '${target instanceof Endpoint ? `${destinationAddress}/${target.ID}` : destinationAddress}'`; const index = this.getBindIndex(cluster.ID, target); if (index === -1) { logger_1.logger.debug(`${log} no bind present, skipping.`, NS); return; } logger_1.logger.debug(log, NS); try { const zdoClusterId = Zdo.ClusterId.UNBIND_REQUEST; const zdoPayload = Zdo.Buffalo.buildRequest( // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` entity_1.default.adapter.hasZdoMessageOverhead, zdoClusterId, this.deviceIeeeAddress, this.ID, cluster.ID, target instanceof Endpoint ? Zdo.UNICAST_BINDING : Zdo.MULTICAST_BINDING, target instanceof Endpoint ? target.deviceIeeeAddress : ZSpec.BLANK_EUI64, target instanceof group_1.default ? target.groupID : 0, target instanceof Endpoint ? target.ID : 0xff); // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` const response = await entity_1.default.adapter.sendZdo(this.deviceIeeeAddress, this.deviceNetworkAddress, zdoClusterId, zdoPayload, false); if (!Zdo.Buffalo.checkStatus(response)) { if (response[0] === Zdo.Status.NO_ENTRY) { logger_1.logger.debug(`${log} no entry on device, removing entry from database.`, NS); } else { throw new Zdo.StatusError(response[0]); } } this._binds.splice(index, 1); this.save(); } catch (error) { const err = error; err.message = `${log} failed (${err.message})`; // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` logger_1.logger.debug(err.stack, NS); throw error; } } async defaultResponse(commandID, status, clusterID, transactionSequenceNumber, options) { (0, node_assert_1.default)(options?.transactionSequenceNumber === undefined, "Use parameter"); const payload = { cmdId: commandID, statusCode: status }; await this.zclCommand(clusterID, "defaultRsp", payload, { direction: Zcl.Direction.SERVER_TO_CLIENT, ...options, transactionSequenceNumber }); } async configureReporting(clusterKey, items, options) { const cluster = this.getCluster(clusterKey); const optionsWithDefaults = this.getOptionsWithDefaults(options, true, Zcl.Direction.CLIENT_TO_SERVER, cluster.manufacturerCode); optionsWithDefaults.manufacturerCode = this.ensureManufacturerCodeIsUniqueAndGet(cluster, items, optionsWithDefaults.manufacturerCode, "configureReporting"); const payload = items.map((item) => { let dataType; let attrId; if (typeof item.attribute === "object") { dataType = item.attribute.type; attrId = item.attribute.ID; } else if (cluster.hasAttribute(item.attribute)) { const attribute = cluster.getAttribute(item.attribute); dataType = attribute.type; attrId = attribute.ID; } return { direction: Zcl.Direction.CLIENT_TO_SERVER, attrId, // TODO: biome migration - can be undefined? dataType, // TODO: biome migration - can be undefined? minRepIntval: item.minimumReportInterval, maxRepIntval: item.maximumReportInterval, repChange: item.reportableChange, }; }); await this.zclCommand(clusterKey, "configReport", payload, optionsWithDefaults, items, true); for (const e of payload) { this._configuredReportings = this._configuredReportings.filter((c) => !(c.attrId === e.attrId && c.cluster === cluster.ID && (!("manufacturerCode" in c) || c.manufacturerCode === optionsWithDefaults.manufacturerCode))); } for (const entry of payload) { if (entry.maxRepIntval !== 0xffff) { this._configuredReportings.push({ cluster: cluster.ID, attrId: entry.attrId, minRepIntval: entry.minRepIntval, maxRepIntval: entry.maxRepIntval, repChange: entry.repChange, manufacturerCode: optionsWithDefaults.manufacturerCode, }); } } this.save(); } async writeStructured(clusterKey, payload, options) { await this.zclCommand(clusterKey, "writeStructured", payload, options); // TODO: support `writeStructuredResponse` } async command(clusterKey, commandKey, payload, options) { const frame = await this.zclCommand(clusterKey, commandKey, payload, options, undefined, false, Zcl.FrameType.SPECIFIC); if (frame) { return frame.payload; } } async commandResponse(clusterKey, commandKey, payload, options, transactionSequenceNumber) { (0, node_assert_1.default)(options?.transactionSequenceNumber === undefined, "Use parameter"); const device = this.getDevice(); const cluster = this.getCluster(clusterKey, device); const command = cluster.getCommandResponse(commandKey); transactionSequenceNumber = transactionSequenceNumber || zclTransactionSequenceNumber_1.default.next(); const optionsWithDefaults = this.getOptionsWithDefaults(options, true, Zcl.Direction.SERVER_TO_CLIENT, cluster.manufacturerCode); const frame = Zcl.Frame.create(Zcl.FrameType.SPECIFIC, optionsWithDefaults.direction, optionsWithDefaults.disableDefaultResponse, optionsWithDefaults.manufacturerCode, transactionSequenceNumber, command.name, cluster.name, payload, device.customClusters, optionsWithDefaults.reservedBits); const createLogMessage = () => `CommandResponse ${this.deviceIeeeAddress}/${this.ID} ` + `${cluster.name}.${command.name}(${JSON.stringify(payload)}, ${JSON.stringify(optionsWithDefaults)})`; logger_1.logger.debug(createLogMessage, NS); try { await this.sendRequest(frame, optionsWithDefaults, async (f) => { // Broadcast Green Power responses if (this.ID === 242) { // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` await entity_1.default.adapter.sendZclFrameToAll(242, f, 242, enums_1.BroadcastAddress.RX_ON_WHEN_IDLE); } else { // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` await entity_1.default.adapter.sendZclFrameToEndpoint(this.deviceIeeeAddress, this.deviceNetworkAddress, this.ID, f, optionsWithDefaults.timeout, optionsWithDefaults.disableResponse, optionsWithDefaults.disableRecovery, optionsWithDefaults.srcEndpoint); } }); } catch (error) { const err = error; err.message = `${createLogMessage()} failed (${err.message})`; // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` logger_1.logger.debug(err.stack, NS); throw error; } } waitForCommand(clusterKey, commandKey, transactionSequenceNumber, timeout) { const device = this.getDevice(); const cluster = this.getCluster(clusterKey, device); const command = cluster.getCommand(commandKey); // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` const waiter = entity_1.default.adapter.waitFor(this.deviceNetworkAddress, this.ID, Zcl.FrameType.SPECIFIC, Zcl.Direction.CLIENT_TO_SERVER, transactionSequenceNumber, cluster.ID, command.ID, timeout); const promise = new Promise((resolve, reject) => { waiter.promise.then((payload) => { const frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, device.customClusters); resolve({ header: frame.header, payload: frame.payload }); }, (error) => reject(error)); }); return { promise, cancel: waiter.cancel }; } getOptionsWithDefaults(options, disableDefaultResponse, direction, manufacturerCode) { return { timeout: 10000, disableResponse: false, disableRecovery: false, disableDefaultResponse, direction, srcEndpoint: undefined, reservedBits: 0, manufacturerCode, transactionSequenceNumber: undefined, writeUndiv: false, ...(options || {}), }; } ensureManufacturerCodeIsUniqueAndGet(cluster, attributes, fallbackManufacturerCode, // XXX: problematic undefined for a "fallback"? caller) { const manufacturerCodes = new Set(attributes.map((nameOrID) => { let attributeID; if (typeof nameOrID === "object") { // ConfigureReportingItem if (typeof nameOrID.attribute !== "object") { attributeID = nameOrID.attribute; } else { return fallbackManufacturerCode; } } else { // string || number attributeID = nameOrID; } // we fall back to caller|cluster provided manufacturerCode if (cluster.hasAttribute(attributeID)) { const attribute = cluster.getAttribute(attributeID); return attribute.manufacturerCode === undefined ? fallbackManufacturerCode : attribute.manufacturerCode; } // unknown attribute, we should not fail on this here return fallbackManufacturerCode; })); if (manufacturerCodes.size === 1) { return manufacturerCodes.values().next().value; } throw new Error(`Cannot have attributes with different manufacturerCode in single '${caller}' call`); } async addToGroup(group) { await this.command("genGroups", "add", { groupid: group.groupID, groupname: "" }); group.addMember(this); } getCluster(clusterKey, device = undefined) { if (!device) { device = this.getDevice(); } return Zcl.Utils.getCluster(clusterKey, device.manufacturerID, device.customClusters); } /** * Remove endpoint from a group, accepts both a Group and number as parameter. * The number parameter type should only be used when removing from a group which is not known * to zigbee-herdsman. */ async removeFromGroup(group) { await this.command("genGroups", "remove", { groupid: group instanceof group_1.default ? group.groupID : group }); if (group instanceof group_1.default) { group.removeMember(this); } } async removeFromAllGroups() { await this.command("genGroups", "removeAll", {}, { disableDefaultResponse: true }); this.removeFromAllGroupsDatabase(); } removeFromAllGroupsDatabase() { for (const group of group_1.default.allIterator()) { if (group.hasMember(this)) { group.removeMember(this); } } } async zclCommand(clusterKey, commandKey, payload, options, logPayload, checkStatus = false, frameType = Zcl.FrameType.GLOBAL) { const device = this.getDevice(); const cluster = this.getCluster(clusterKey, device); const command = frameType === Zcl.FrameType.GLOBAL ? Zcl.Utils.getGlobalCommand(commandKey) : cluster.getCommand(commandKey); const hasResponse = frameType === Zcl.FrameType.GLOBAL ? true : command.response !== undefined; const optionsWithDefaults = this.getOptionsWithDefaults(options, hasResponse, Zcl.Direction.CLIENT_TO_SERVER, cluster.manufacturerCode); const frame = Zcl.Frame.create(frameType, optionsWithDefaults.direction, optionsWithDefaults.disableDefaultResponse, optionsWithDefaults.manufacturerCode, optionsWithDefaults.transactionSequenceNumber ?? zclTransactionSequenceNumber_1.default.next(), command.name, cluster.name, payload, device.customClusters, optionsWithDefaults.reservedBits); const createLogMessage = () => `ZCL command ${this.deviceIeeeAddress}/${this.ID} ` + `${cluster.name}.${command.name}(${JSON.stringify(logPayload ? logPayload : payload)}, ${JSON.stringify(optionsWithDefaults)})`; logger_1.logger.debug(createLogMessage, NS); try { const result = await this.sendRequest(frame, optionsWithDefaults); if (result) { const resultFrame = Zcl.Frame.fromBuffer(result.clusterID, result.header, result.data, device.customClusters); if (result && checkStatus && !optionsWithDefaults.disableResponse) { this.checkStatus(resultFrame.payload); } return resultFrame; } } catch (error) { const err = error; err.message = `${createLogMessage()} failed (${err.message})`; // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` logger_1.logger.debug(err.stack, NS); throw error; } } async zclCommandBroadcast(endpoint, destination, clusterKey, commandKey, payload, options) { const device = this.getDevice(); const cluster = this.getCluster(clusterKey, device); const command = cluster.getCommand(commandKey); const optionsWithDefaults = this.getOptionsWithDefaults(options, true, Zcl.Direction.CLIENT_TO_SERVER, cluster.manufacturerCode); const sourceEndpoint = optionsWithDefaults.srcEndpoint ?? this.ID; const frame = Zcl.Frame.create(Zcl.FrameType.SPECIFIC, optionsWithDefaults.direction, true, optionsWithDefaults.manufacturerCode, optionsWithDefaults.transactionSequenceNumber ?? zclTransactionSequenceNumber_1.default.next(), command.name, cluster.name, payload, device.customClusters, optionsWithDefaults.reservedBits); logger_1.logger.debug(() => `ZCL command broadcast ${this.deviceIeeeAddress}/${sourceEndpoint} to ${destination}/${endpoint} ` + `${cluster.name}.${command.name}(${JSON.stringify({ payload, optionsWithDefaults })})`, NS); // if endpoint===0xFF ("broadcast endpoint"), deliver to all endpoints supporting cluster, should be avoided whenever possible // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` await entity_1.default.adapter.sendZclFrameToAll(endpoint, frame, sourceEndpoint, destination); } } exports.Endpoint = Endpoint; exports.default = Endpoint; //# sourceMappingURL=endpoint.js.map