zigbee-herdsman
Version:
An open source ZigBee gateway solution with node.js.
690 lines • 35.7 kB
JavaScript
"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