@willieee802/zigbee-herdsman 0.49.0 → 0.49.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +38 -0
- package/biome.json +1 -1
- package/dist/adapter/adapter.d.ts.map +1 -0
- package/dist/adapter/adapterDiscovery.d.ts.map +1 -0
- package/dist/adapter/const.d.ts.map +1 -0
- package/dist/adapter/deconz/adapter/deconzAdapter.d.ts.map +1 -0
- package/dist/adapter/ember/adapter/emberAdapter.d.ts.map +1 -0
- package/dist/adapter/ember/adapter/endpoints.d.ts.map +1 -0
- package/dist/adapter/ember/adapter/oneWaitress.d.ts.map +1 -0
- package/dist/adapter/ember/adapter/tokensManager.d.ts.map +1 -0
- package/dist/adapter/ember/ezsp/ezsp.d.ts.map +1 -0
- package/dist/adapter/ember/utils/initters.d.ts.map +1 -0
- package/dist/adapter/events.d.ts.map +1 -0
- package/dist/adapter/ezsp/adapter/backup.d.ts.map +1 -0
- package/dist/adapter/ezsp/adapter/ezspAdapter.d.ts.map +1 -0
- package/dist/adapter/ezsp/driver/driver.d.ts.map +1 -0
- package/dist/adapter/ezsp/driver/index.d.ts.map +1 -0
- package/dist/adapter/ezsp/driver/multicast.d.ts.map +1 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/z-stack/adapter/endpoints.d.ts.map +1 -0
- package/dist/adapter/z-stack/adapter/manager.d.ts.map +1 -0
- package/dist/adapter/z-stack/adapter/zStackAdapter.d.ts.map +1 -0
- package/dist/adapter/z-stack/models/startup-options.d.ts.map +1 -0
- package/dist/adapter/zboss/adapter/zbossAdapter.d.ts.map +1 -0
- package/dist/adapter/zboss/driver.d.ts.map +1 -0
- package/dist/adapter/zboss/frame.d.ts.map +1 -0
- package/dist/adapter/zboss/frame.js +200 -0
- package/dist/adapter/zboss/frame.js.map +1 -0
- package/dist/adapter/zboss/uart.d.ts.map +1 -0
- package/dist/adapter/zigate/adapter/zigateAdapter.d.ts.map +1 -0
- package/dist/adapter/zigate/driver/buffaloZiGate.d.ts.map +1 -0
- package/dist/adapter/zigate/driver/buffaloZiGate.js +198 -0
- package/dist/adapter/zigate/driver/buffaloZiGate.js.map +1 -0
- package/dist/adapter/zigate/driver/ziGateObject.d.ts.map +1 -0
- package/dist/adapter/zigate/driver/zigate.d.ts.map +1 -0
- package/dist/adapter/zoh/adapter/zohAdapter.d.ts.map +1 -0
- package/dist/controller/controller.d.ts.map +1 -0
- package/dist/controller/controller.js +874 -0
- package/dist/controller/controller.js.map +1 -0
- package/dist/controller/database.d.ts.map +1 -0
- package/dist/controller/events.d.ts.map +1 -0
- package/dist/controller/events.js +3 -0
- package/dist/controller/events.js.map +1 -0
- package/dist/controller/greenPower.d.ts.map +1 -0
- package/dist/controller/greenPower.js +425 -0
- package/dist/controller/greenPower.js.map +1 -0
- package/dist/controller/helpers/index.d.ts.map +1 -0
- package/dist/controller/helpers/ota.d.ts.map +1 -0
- package/dist/controller/helpers/ota.js +467 -0
- package/dist/controller/helpers/ota.js.map +1 -0
- package/dist/controller/helpers/request.d.ts.map +1 -0
- package/dist/controller/helpers/requestQueue.d.ts.map +1 -0
- package/dist/controller/helpers/zclFrameConverter.d.ts.map +1 -0
- package/dist/controller/helpers/zclFrameConverter.js +84 -0
- package/dist/controller/helpers/zclFrameConverter.js.map +1 -0
- package/dist/controller/index.d.ts.map +1 -0
- package/dist/controller/model/device.d.ts.map +1 -0
- package/dist/controller/model/device.js +1396 -0
- package/dist/controller/model/device.js.map +1 -0
- package/dist/controller/model/endpoint.d.ts.map +1 -0
- package/dist/controller/model/endpoint.js +822 -0
- package/dist/controller/model/endpoint.js.map +1 -0
- package/dist/controller/model/entity.d.ts.map +1 -0
- package/dist/controller/model/group.d.ts.map +1 -0
- package/dist/controller/model/group.js +343 -0
- package/dist/controller/model/group.js.map +1 -0
- package/dist/controller/model/index.d.ts.map +1 -0
- package/dist/controller/model/zigbeeEntity.d.ts.map +1 -0
- package/dist/controller/touchlink.d.ts.map +1 -0
- package/dist/controller/tstype.d.ts.map +1 -0
- package/dist/controller/tstype.js +3 -0
- package/dist/controller/tstype.js.map +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/utils/timeService.d.ts.map +1 -0
- package/dist/utils/timeService.js +127 -0
- package/dist/utils/timeService.js.map +1 -0
- package/dist/zspec/zcl/buffaloZcl.d.ts.map +1 -0
- package/dist/zspec/zcl/buffaloZcl.js +969 -0
- package/dist/zspec/zcl/buffaloZcl.js.map +1 -0
- package/dist/zspec/zcl/definition/cluster.d.ts.map +1 -0
- package/dist/zspec/zcl/definition/cluster.js +7507 -0
- package/dist/zspec/zcl/definition/cluster.js.map +1 -0
- package/dist/zspec/zcl/definition/clusters-types.d.ts +8135 -0
- package/dist/zspec/zcl/definition/clusters-types.d.ts.map +1 -0
- package/dist/zspec/zcl/definition/clusters-types.js +3 -0
- package/dist/zspec/zcl/definition/clusters-types.js.map +1 -0
- package/dist/zspec/zcl/definition/foundation.d.ts.map +1 -0
- package/dist/zspec/zcl/definition/foundation.js +312 -0
- package/dist/zspec/zcl/definition/foundation.js.map +1 -0
- package/dist/zspec/zcl/definition/tstype.d.ts +273 -0
- package/dist/zspec/zcl/definition/tstype.d.ts.map +1 -0
- package/dist/zspec/zcl/definition/tstype.js +3 -0
- package/dist/zspec/zcl/definition/tstype.js.map +1 -0
- package/dist/zspec/zcl/index.d.ts.map +1 -0
- package/dist/zspec/zcl/index.js +57 -0
- package/dist/zspec/zcl/index.js.map +1 -0
- package/dist/zspec/zcl/utils.d.ts.map +1 -0
- package/dist/zspec/zcl/utils.js +419 -0
- package/dist/zspec/zcl/utils.js.map +1 -0
- package/dist/zspec/zcl/zclFrame.d.ts.map +1 -0
- package/dist/zspec/zcl/zclFrame.js +328 -0
- package/dist/zspec/zcl/zclFrame.js.map +1 -0
- package/dist/zspec/zcl/zclHeader.d.ts.map +1 -0
- package/dist/zspec/zcl/zclHeader.js +88 -0
- package/dist/zspec/zcl/zclHeader.js.map +1 -0
- package/package.json +90 -83
- package/src/controller/helpers/ota.ts +5 -2
- package/src/zspec/zcl/definition/cluster.ts +3 -294
- package/src/zspec/zcl/definition/clusters-types.ts +6 -355
- package/src/zspec/zcl/definition/tstype.ts +0 -14
- package/src/zspec/zcl/utils.ts +14 -31
- package/test/controller.test.ts +16 -8
- package/test/zcl.test.ts +36 -7
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.Endpoint = void 0;
|
|
40
|
+
const node_assert_1 = __importDefault(require("node:assert"));
|
|
41
|
+
const logger_1 = require("../../utils/logger");
|
|
42
|
+
const ZSpec = __importStar(require("../../zspec"));
|
|
43
|
+
const enums_1 = require("../../zspec/enums");
|
|
44
|
+
const Zcl = __importStar(require("../../zspec/zcl"));
|
|
45
|
+
const Zdo = __importStar(require("../../zspec/zdo"));
|
|
46
|
+
const request_1 = __importDefault(require("../helpers/request"));
|
|
47
|
+
const requestQueue_1 = __importDefault(require("../helpers/requestQueue"));
|
|
48
|
+
const ZclFrameConverter = __importStar(require("../helpers/zclFrameConverter"));
|
|
49
|
+
const zclTransactionSequenceNumber_1 = __importDefault(require("../helpers/zclTransactionSequenceNumber"));
|
|
50
|
+
const device_1 = __importDefault(require("./device"));
|
|
51
|
+
const entity_1 = __importDefault(require("./entity"));
|
|
52
|
+
const group_1 = __importDefault(require("./group"));
|
|
53
|
+
const zigbeeEntity_1 = require("./zigbeeEntity");
|
|
54
|
+
const NS = "zh:controller:endpoint";
|
|
55
|
+
class Endpoint extends zigbeeEntity_1.ZigbeeEntity {
|
|
56
|
+
databaseID;
|
|
57
|
+
deviceID;
|
|
58
|
+
inputClusters;
|
|
59
|
+
outputClusters;
|
|
60
|
+
profileID;
|
|
61
|
+
// biome-ignore lint/style/useNamingConvention: cross-repo impact
|
|
62
|
+
ID;
|
|
63
|
+
clusters;
|
|
64
|
+
deviceIeeeAddress;
|
|
65
|
+
deviceNetworkAddress;
|
|
66
|
+
_binds;
|
|
67
|
+
_configuredReportings;
|
|
68
|
+
meta;
|
|
69
|
+
pendingRequests;
|
|
70
|
+
// Getters/setters
|
|
71
|
+
get binds() {
|
|
72
|
+
const binds = [];
|
|
73
|
+
for (const bind of this._binds) {
|
|
74
|
+
// XXX: properties assumed valid when associated to `type`
|
|
75
|
+
const target =
|
|
76
|
+
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
|
|
77
|
+
bind.type === "endpoint" ? device_1.default.byIeeeAddr(this.databaseID, bind.deviceIeeeAddress)?.getEndpoint(bind.endpointID) : group_1.default.byGroupID(bind.groupID, this.databaseID);
|
|
78
|
+
if (target) {
|
|
79
|
+
binds.push({ target, cluster: this.getCluster(bind.cluster) });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return binds;
|
|
83
|
+
}
|
|
84
|
+
get configuredReportings() {
|
|
85
|
+
const device = this.getDevice();
|
|
86
|
+
return this._configuredReportings.map((entry, index) => {
|
|
87
|
+
const cluster = Zcl.Utils.getCluster(entry.cluster, entry.manufacturerCode, device.customClusters);
|
|
88
|
+
const attribute = cluster.getAttribute(entry.attrId) ?? {
|
|
89
|
+
ID: entry.attrId,
|
|
90
|
+
name: `attr${index}`,
|
|
91
|
+
type: Zcl.DataType.UNKNOWN,
|
|
92
|
+
manufacturerCode: undefined,
|
|
93
|
+
};
|
|
94
|
+
return {
|
|
95
|
+
cluster,
|
|
96
|
+
attribute,
|
|
97
|
+
minimumReportInterval: entry.minRepIntval,
|
|
98
|
+
maximumReportInterval: entry.maxRepIntval,
|
|
99
|
+
reportableChange: entry.repChange,
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
constructor(databaseID, id, profileID, deviceID, inputClusters, outputClusters, deviceNetworkAddress, deviceIeeeAddress, clusters, binds, configuredReportings, meta) {
|
|
104
|
+
super();
|
|
105
|
+
this.databaseID = databaseID;
|
|
106
|
+
this.ID = id;
|
|
107
|
+
this.profileID = profileID;
|
|
108
|
+
this.deviceID = deviceID;
|
|
109
|
+
this.inputClusters = inputClusters;
|
|
110
|
+
this.outputClusters = outputClusters;
|
|
111
|
+
this.deviceNetworkAddress = deviceNetworkAddress;
|
|
112
|
+
this.deviceIeeeAddress = deviceIeeeAddress;
|
|
113
|
+
this.clusters = clusters;
|
|
114
|
+
this._binds = binds;
|
|
115
|
+
this._configuredReportings = configuredReportings;
|
|
116
|
+
this.meta = meta;
|
|
117
|
+
this.pendingRequests = new requestQueue_1.default(this);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Get device of this endpoint
|
|
121
|
+
*/
|
|
122
|
+
getDevice() {
|
|
123
|
+
const device = device_1.default.byIeeeAddr(this.databaseID, this.deviceIeeeAddress);
|
|
124
|
+
if (!device) {
|
|
125
|
+
logger_1.logger.error(`Tried to get unknown/deleted device ${this.deviceIeeeAddress} from endpoint ${this.ID}.`, NS);
|
|
126
|
+
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
|
|
127
|
+
logger_1.logger.debug(new Error().stack, NS);
|
|
128
|
+
}
|
|
129
|
+
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
|
|
130
|
+
return device;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* @param {number|string} clusterKey
|
|
134
|
+
* @returns {boolean}
|
|
135
|
+
*/
|
|
136
|
+
supportsInputCluster(clusterKey) {
|
|
137
|
+
const cluster = this.getCluster(clusterKey);
|
|
138
|
+
return this.inputClusters.includes(cluster.ID);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* @param {number|string} clusterKey
|
|
142
|
+
* @returns {boolean}
|
|
143
|
+
*/
|
|
144
|
+
supportsOutputCluster(clusterKey) {
|
|
145
|
+
const cluster = this.getCluster(clusterKey);
|
|
146
|
+
return this.outputClusters.includes(cluster.ID);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* @returns {ZclTypes.Cluster[]}
|
|
150
|
+
*/
|
|
151
|
+
getInputClusters() {
|
|
152
|
+
return this.clusterNumbersToClusters(this.inputClusters);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* @returns {ZclTypes.Cluster[]}
|
|
156
|
+
*/
|
|
157
|
+
getOutputClusters() {
|
|
158
|
+
return this.clusterNumbersToClusters(this.outputClusters);
|
|
159
|
+
}
|
|
160
|
+
clusterNumbersToClusters(clusterNumbers) {
|
|
161
|
+
return clusterNumbers.map((c) => this.getCluster(c));
|
|
162
|
+
}
|
|
163
|
+
/*
|
|
164
|
+
* CRUD
|
|
165
|
+
*/
|
|
166
|
+
static fromDatabaseRecord(record, deviceNetworkAddress, deviceIeeeAddress, databaseID) {
|
|
167
|
+
// Migrate attrs to attributes
|
|
168
|
+
for (const entryKey in record.clusters) {
|
|
169
|
+
const entry = record.clusters[entryKey];
|
|
170
|
+
if (entry.attrs != null) {
|
|
171
|
+
entry.attributes = entry.attrs;
|
|
172
|
+
delete entry.attrs;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Migrate cluster renames from https://github.com/Koenkk/zigbee-herdsman/pull/1503 @deprecated 3.0
|
|
176
|
+
/* v8 ignore start */
|
|
177
|
+
if (record.clusters.piRetailTunnel) {
|
|
178
|
+
record.clusters.retailTunnel = record.clusters.piRetailTunnel;
|
|
179
|
+
delete record.clusters.piRetailTunnel;
|
|
180
|
+
}
|
|
181
|
+
if (record.clusters.tunneling) {
|
|
182
|
+
record.clusters.seTunneling = record.clusters.tunneling;
|
|
183
|
+
delete record.clusters.tunneling;
|
|
184
|
+
}
|
|
185
|
+
if (record.clusters.haMeterIdentification) {
|
|
186
|
+
record.clusters.seMeterIdentification = record.clusters.haMeterIdentification;
|
|
187
|
+
delete record.clusters.haMeterIdentification;
|
|
188
|
+
}
|
|
189
|
+
/* v8 ignore stop */
|
|
190
|
+
return new Endpoint(databaseID, record.epId, record.profId, record.devId, record.inClusterList, record.outClusterList, deviceNetworkAddress, deviceIeeeAddress, record.clusters, record.binds || [], record.configuredReportings || [], record.meta || {});
|
|
191
|
+
}
|
|
192
|
+
toDatabaseRecord() {
|
|
193
|
+
return {
|
|
194
|
+
profId: this.profileID,
|
|
195
|
+
epId: this.ID,
|
|
196
|
+
devId: this.deviceID,
|
|
197
|
+
inClusterList: this.inputClusters,
|
|
198
|
+
outClusterList: this.outputClusters,
|
|
199
|
+
clusters: this.clusters,
|
|
200
|
+
binds: this._binds,
|
|
201
|
+
configuredReportings: this._configuredReportings,
|
|
202
|
+
meta: this.meta,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
static create(databaseID, id, profileID, deviceID, inputClusters, outputClusters, deviceNetworkAddress, deviceIeeeAddress) {
|
|
206
|
+
return new Endpoint(databaseID, id, profileID, deviceID, inputClusters, outputClusters, deviceNetworkAddress, deviceIeeeAddress, {}, [], [], {});
|
|
207
|
+
}
|
|
208
|
+
saveClusterAttributeKeyValue(clusterKey, list) {
|
|
209
|
+
const cluster = this.getCluster(clusterKey);
|
|
210
|
+
if (!this.clusters[cluster.name]) {
|
|
211
|
+
this.clusters[cluster.name] = { attributes: {} };
|
|
212
|
+
}
|
|
213
|
+
for (const attribute in list) {
|
|
214
|
+
this.clusters[cluster.name].attributes[attribute] = list[attribute];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
getClusterAttributeValue(clusterKey, attributeKey) {
|
|
218
|
+
const cluster = this.getCluster(clusterKey);
|
|
219
|
+
if (this.clusters[cluster.name] && this.clusters[cluster.name].attributes) {
|
|
220
|
+
// XXX: used to throw (behavior changed in #1455)
|
|
221
|
+
const attribute = cluster.getAttribute(attributeKey);
|
|
222
|
+
if (attribute) {
|
|
223
|
+
return this.clusters[cluster.name].attributes[attribute.name];
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
saveClusterAttributeReportConfig(clusterId, manufacturerCode, reportConfigs) {
|
|
229
|
+
for (const entry of reportConfigs) {
|
|
230
|
+
if (entry.direction === Zcl.Direction.SERVER_TO_CLIENT) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const existingConfigIdx = this._configuredReportings.findIndex((r) => r.cluster === clusterId &&
|
|
234
|
+
r.attrId === entry.attrId &&
|
|
235
|
+
(manufacturerCode === undefined || manufacturerCode === r.manufacturerCode));
|
|
236
|
+
if (entry.status === Zcl.Status.SUCCESS) {
|
|
237
|
+
if (existingConfigIdx > -1) {
|
|
238
|
+
this._configuredReportings[existingConfigIdx].minRepIntval = entry.minRepIntval;
|
|
239
|
+
this._configuredReportings[existingConfigIdx].maxRepIntval = entry.maxRepIntval;
|
|
240
|
+
this._configuredReportings[existingConfigIdx].repChange = entry.repChange;
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
this._configuredReportings.push({
|
|
244
|
+
cluster: clusterId,
|
|
245
|
+
attrId: entry.attrId,
|
|
246
|
+
minRepIntval: entry.minRepIntval,
|
|
247
|
+
maxRepIntval: entry.maxRepIntval,
|
|
248
|
+
repChange: entry.repChange,
|
|
249
|
+
manufacturerCode,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
// UNSUPPORTED_ATTRIBUTE, UNREPORTABLE_ATTRIBUTE, NOT_FOUND
|
|
255
|
+
if (existingConfigIdx > -1) {
|
|
256
|
+
this._configuredReportings.splice(existingConfigIdx, 1);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
this.save();
|
|
261
|
+
}
|
|
262
|
+
saveBindings(binds) {
|
|
263
|
+
this._binds = binds;
|
|
264
|
+
this.save();
|
|
265
|
+
}
|
|
266
|
+
clearBindings() {
|
|
267
|
+
this._binds.length = 0;
|
|
268
|
+
this.save();
|
|
269
|
+
}
|
|
270
|
+
hasPendingRequests() {
|
|
271
|
+
return this.pendingRequests.size > 0;
|
|
272
|
+
}
|
|
273
|
+
async sendPendingRequests(fastPolling) {
|
|
274
|
+
return await this.pendingRequests.send(fastPolling);
|
|
275
|
+
}
|
|
276
|
+
async sendRequest(frame, options, func = () => {
|
|
277
|
+
return entity_1.default.getAdapterByID(this.databaseID)?.sendZclFrameToEndpoint(this.deviceIeeeAddress, this.deviceNetworkAddress, this.ID, frame, options.timeout, options.disableResponse, options.disableRecovery, options.srcEndpoint, options.profileId);
|
|
278
|
+
}) {
|
|
279
|
+
const logPrefix = `Request Queue (${this.deviceIeeeAddress}/${this.ID}): `;
|
|
280
|
+
const device = this.getDevice();
|
|
281
|
+
const request = new request_1.default(func, frame, device.pendingRequestTimeout, options.sendPolicy);
|
|
282
|
+
if (request.sendPolicy !== "bulk") {
|
|
283
|
+
// Check if such a request is already in the queue and remove the old one(s) if necessary
|
|
284
|
+
this.pendingRequests.filter(request);
|
|
285
|
+
}
|
|
286
|
+
// send without queueing if sendPolicy is 'immediate' or if the device has no timeout set
|
|
287
|
+
if (request.sendPolicy === "immediate" || !device.pendingRequestTimeout) {
|
|
288
|
+
if (device.pendingRequestTimeout > 0) {
|
|
289
|
+
logger_1.logger.debug(`${logPrefix}send ${frame.command.name} request immediately (sendPolicy=${options.sendPolicy})`, NS);
|
|
290
|
+
}
|
|
291
|
+
return await request.send();
|
|
292
|
+
}
|
|
293
|
+
// If this is a bulk message, we queue directly.
|
|
294
|
+
if (request.sendPolicy === "bulk") {
|
|
295
|
+
logger_1.logger.debug(`${logPrefix}queue request (${this.pendingRequests.size})`, NS);
|
|
296
|
+
return await this.pendingRequests.queue(request);
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
logger_1.logger.debug(`${logPrefix}send request`, NS);
|
|
300
|
+
return await request.send();
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
// If we got a failed transaction, the device is likely sleeping.
|
|
304
|
+
// Queue for transmission later.
|
|
305
|
+
logger_1.logger.debug(`${logPrefix}queue request (transaction failed) (${error})`, NS);
|
|
306
|
+
return await this.pendingRequests.queue(request);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/*
|
|
310
|
+
* Zigbee functions
|
|
311
|
+
*/
|
|
312
|
+
checkStatus(payload) {
|
|
313
|
+
const codes = Array.isArray(payload) ? payload.map((i) => i.status) : [payload.statusCode];
|
|
314
|
+
const invalid = codes.find((c) => c !== Zcl.Status.SUCCESS);
|
|
315
|
+
if (invalid)
|
|
316
|
+
throw new Zcl.StatusError(invalid);
|
|
317
|
+
}
|
|
318
|
+
async report(clusterKey, attributes, options) {
|
|
319
|
+
const cluster = this.getCluster(clusterKey, undefined, options?.manufacturerCode);
|
|
320
|
+
const payload = [];
|
|
321
|
+
// TODO: handle `attr.report !== true`
|
|
322
|
+
for (const nameOrID in attributes) {
|
|
323
|
+
const attribute = cluster.getAttribute(nameOrID);
|
|
324
|
+
if (attribute) {
|
|
325
|
+
payload.push({ attrId: attribute.ID, attrData: attributes[nameOrID], dataType: attribute.type });
|
|
326
|
+
}
|
|
327
|
+
else if (!Number.isNaN(Number(nameOrID))) {
|
|
328
|
+
const value = attributes[nameOrID];
|
|
329
|
+
payload.push({ attrId: Number(nameOrID), attrData: value.value, dataType: value.type });
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
throw new Error(`Unknown attribute '${nameOrID}', specify either an existing attribute or a number`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
await this.zclCommand(cluster, "report", payload, options, attributes);
|
|
336
|
+
}
|
|
337
|
+
async write(clusterKey, attributes, options) {
|
|
338
|
+
const cluster = this.getCluster(clusterKey, undefined, options?.manufacturerCode);
|
|
339
|
+
const optionsWithDefaults = this.getOptionsWithDefaults(options, true, Zcl.Direction.CLIENT_TO_SERVER, cluster.manufacturerCode);
|
|
340
|
+
optionsWithDefaults.manufacturerCode = this.ensureManufacturerCodeIsUniqueAndGet(cluster, Object.keys(attributes), optionsWithDefaults.manufacturerCode, "write");
|
|
341
|
+
const payload = [];
|
|
342
|
+
for (const nameOrID in attributes) {
|
|
343
|
+
const attribute = cluster.getAttribute(nameOrID);
|
|
344
|
+
if (attribute) {
|
|
345
|
+
// TODO: handle `attr.writeOptional !== true`
|
|
346
|
+
const attrData = Zcl.Utils.processAttributeWrite(attribute, attributes[nameOrID]);
|
|
347
|
+
payload.push({ attrId: attribute.ID, attrData, dataType: attribute.type });
|
|
348
|
+
}
|
|
349
|
+
else if (!Number.isNaN(Number(nameOrID))) {
|
|
350
|
+
const value = attributes[nameOrID];
|
|
351
|
+
payload.push({ attrId: Number(nameOrID), attrData: value.value, dataType: value.type });
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
throw new Error(`Unknown attribute '${nameOrID}', specify either an existing attribute or a number`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
await this.zclCommand(cluster, optionsWithDefaults.writeUndiv ? "writeUndiv" : "write", payload, optionsWithDefaults, attributes, true);
|
|
358
|
+
}
|
|
359
|
+
async writeResponse(clusterKey, transactionSequenceNumber, attributes, options) {
|
|
360
|
+
(0, node_assert_1.default)(options?.transactionSequenceNumber === undefined, "Use parameter");
|
|
361
|
+
const cluster = this.getCluster(clusterKey, undefined, options?.manufacturerCode);
|
|
362
|
+
const payload = [];
|
|
363
|
+
for (const nameOrID in attributes) {
|
|
364
|
+
// biome-ignore lint/style/noNonNullAssertion: from loop
|
|
365
|
+
const value = attributes[nameOrID];
|
|
366
|
+
if (value.status !== undefined) {
|
|
367
|
+
const attribute = cluster.getAttribute(nameOrID);
|
|
368
|
+
if (attribute) {
|
|
369
|
+
payload.push({ attrId: attribute.ID, status: value.status });
|
|
370
|
+
}
|
|
371
|
+
else if (!Number.isNaN(Number(nameOrID))) {
|
|
372
|
+
payload.push({ attrId: Number(nameOrID), status: value.status });
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
throw new Error(`Unknown attribute '${nameOrID}', specify either an existing attribute or a number`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
throw new Error(`Missing attribute 'status'`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
await this.zclCommand(cluster, "writeRsp", payload, { direction: Zcl.Direction.SERVER_TO_CLIENT, ...options, transactionSequenceNumber }, attributes);
|
|
383
|
+
}
|
|
384
|
+
// XXX: ideally, the return type should limit to the contents of the `attributes` param
|
|
385
|
+
async read(clusterKey, attributes, options) {
|
|
386
|
+
const device = this.getDevice();
|
|
387
|
+
const cluster = this.getCluster(clusterKey, device, options?.manufacturerCode);
|
|
388
|
+
const optionsWithDefaults = this.getOptionsWithDefaults(options, true, Zcl.Direction.CLIENT_TO_SERVER, cluster.manufacturerCode);
|
|
389
|
+
optionsWithDefaults.manufacturerCode = this.ensureManufacturerCodeIsUniqueAndGet(cluster, attributes, optionsWithDefaults.manufacturerCode, "read");
|
|
390
|
+
const payload = [];
|
|
391
|
+
// TODO: handle `attr.required !== true` => should not throw
|
|
392
|
+
for (const attribute of attributes) {
|
|
393
|
+
if (typeof attribute === "number") {
|
|
394
|
+
payload.push({ attrId: attribute });
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
const attr = cluster.getAttribute(attribute);
|
|
398
|
+
if (attr) {
|
|
399
|
+
Zcl.Utils.processAttributePreRead(attr);
|
|
400
|
+
payload.push({ attrId: attr.ID });
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
logger_1.logger.warning(`Ignoring unknown attribute ${attribute} in cluster ${cluster.name}`, NS);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// TODO: could be sending empty array payload
|
|
408
|
+
const resultFrame = await this.zclCommand(cluster, "read", payload, optionsWithDefaults, attributes, true);
|
|
409
|
+
return resultFrame
|
|
410
|
+
? ZclFrameConverter.attributeKeyValue(resultFrame, device.manufacturerID, device.customClusters)
|
|
411
|
+
: {};
|
|
412
|
+
}
|
|
413
|
+
async readResponse(clusterKey, transactionSequenceNumber, attributes, options) {
|
|
414
|
+
(0, node_assert_1.default)(options?.transactionSequenceNumber === undefined, "Use parameter");
|
|
415
|
+
const cluster = this.getCluster(clusterKey, undefined, options?.manufacturerCode);
|
|
416
|
+
const payload = [];
|
|
417
|
+
for (const nameOrID in attributes) {
|
|
418
|
+
const attribute = cluster.getAttribute(nameOrID);
|
|
419
|
+
if (attribute) {
|
|
420
|
+
payload.push({ attrId: attribute.ID, attrData: attributes[nameOrID], dataType: attribute.type, status: 0 });
|
|
421
|
+
}
|
|
422
|
+
else if (!Number.isNaN(Number(nameOrID))) {
|
|
423
|
+
const value = attributes[nameOrID];
|
|
424
|
+
payload.push({ attrId: Number(nameOrID), attrData: value.value, dataType: value.type, status: 0 });
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
throw new Error(`Unknown attribute '${nameOrID}', specify either an existing attribute or a number`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
await this.zclCommand(cluster, "readRsp", payload, { direction: Zcl.Direction.SERVER_TO_CLIENT, ...options, transactionSequenceNumber }, attributes);
|
|
431
|
+
}
|
|
432
|
+
async updateSimpleDescriptor() {
|
|
433
|
+
const clusterId = Zdo.ClusterId.SIMPLE_DESCRIPTOR_REQUEST;
|
|
434
|
+
const adapter = entity_1.default.getAdapterByID(this.databaseID);
|
|
435
|
+
if (!adapter) {
|
|
436
|
+
throw new Error(`No adapter found for database ID ${this.databaseID}`);
|
|
437
|
+
}
|
|
438
|
+
const zdoPayload = Zdo.Buffalo.buildRequest(adapter.hasZdoMessageOverhead ?? false, clusterId, this.deviceNetworkAddress, this.ID);
|
|
439
|
+
const response = await adapter.sendZdo(this.deviceIeeeAddress, this.deviceNetworkAddress, clusterId, zdoPayload, false);
|
|
440
|
+
if (!Zdo.Buffalo.checkStatus(response)) {
|
|
441
|
+
throw new Zdo.StatusError(response[0]);
|
|
442
|
+
}
|
|
443
|
+
const simpleDescriptor = response[1];
|
|
444
|
+
this.profileID = simpleDescriptor.profileId;
|
|
445
|
+
this.deviceID = simpleDescriptor.deviceId;
|
|
446
|
+
this.inputClusters = simpleDescriptor.inClusterList;
|
|
447
|
+
this.outputClusters = simpleDescriptor.outClusterList;
|
|
448
|
+
}
|
|
449
|
+
hasBind(clusterId, target) {
|
|
450
|
+
return this.getBindIndex(clusterId, target) !== -1;
|
|
451
|
+
}
|
|
452
|
+
getBindIndex(clusterId, target) {
|
|
453
|
+
return this.binds.findIndex((b) => b.cluster.ID === clusterId && b.target === target);
|
|
454
|
+
}
|
|
455
|
+
addBinding(clusterKey, target) {
|
|
456
|
+
const cluster = this.getCluster(clusterKey);
|
|
457
|
+
if (typeof target === "number") {
|
|
458
|
+
target = group_1.default.byGroupID(target, this.databaseID) || group_1.default.create(target, this.databaseID);
|
|
459
|
+
}
|
|
460
|
+
this.addBindingInternal(cluster, target);
|
|
461
|
+
}
|
|
462
|
+
addBindingInternal(cluster, target) {
|
|
463
|
+
if (!this.hasBind(cluster.ID, target)) {
|
|
464
|
+
if (target instanceof group_1.default) {
|
|
465
|
+
this._binds.push({ cluster: cluster.ID, groupID: target.groupID, type: "group" });
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
this._binds.push({
|
|
469
|
+
cluster: cluster.ID,
|
|
470
|
+
type: "endpoint",
|
|
471
|
+
deviceIeeeAddress: target.deviceIeeeAddress,
|
|
472
|
+
endpointID: target.ID,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
this.save();
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
async bind(clusterKey, target) {
|
|
479
|
+
const cluster = this.getCluster(clusterKey);
|
|
480
|
+
if (typeof target === "number") {
|
|
481
|
+
target = group_1.default.byGroupID(target, this.databaseID) || group_1.default.create(target, this.databaseID);
|
|
482
|
+
}
|
|
483
|
+
const destinationAddress = target instanceof Endpoint ? target.deviceIeeeAddress : target.groupID;
|
|
484
|
+
const log = `Bind ${this.deviceIeeeAddress}/${this.ID} ${cluster.name} from '${target instanceof Endpoint ? `${destinationAddress}/${target.ID}` : destinationAddress}'`;
|
|
485
|
+
logger_1.logger.debug(log, NS);
|
|
486
|
+
try {
|
|
487
|
+
const zdoClusterId = Zdo.ClusterId.BIND_REQUEST;
|
|
488
|
+
const zdoPayload = Zdo.Buffalo.buildRequest(entity_1.default.getAdapterByID(this.databaseID)?.hasZdoMessageOverhead ?? false, 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);
|
|
489
|
+
const adapter = entity_1.default.getAdapterByID(this.databaseID);
|
|
490
|
+
if (!adapter) {
|
|
491
|
+
throw new Error(`No adapter found for database ID ${this.databaseID}`);
|
|
492
|
+
}
|
|
493
|
+
const response = await adapter.sendZdo(this.deviceIeeeAddress, this.deviceNetworkAddress, zdoClusterId, zdoPayload, false);
|
|
494
|
+
if (!Zdo.Buffalo.checkStatus(response)) {
|
|
495
|
+
throw new Zdo.StatusError(response[0]);
|
|
496
|
+
}
|
|
497
|
+
this.addBindingInternal(cluster, target);
|
|
498
|
+
}
|
|
499
|
+
catch (error) {
|
|
500
|
+
const err = error;
|
|
501
|
+
err.message = `${log} failed (${err.message})`;
|
|
502
|
+
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
|
|
503
|
+
logger_1.logger.debug(err.stack, NS);
|
|
504
|
+
throw error;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
save() {
|
|
508
|
+
this.getDevice().save();
|
|
509
|
+
}
|
|
510
|
+
async unbind(clusterKey, target, force = false) {
|
|
511
|
+
// When force is true the unbind is done even when the bind is not in the bind list, additionally when the target is a number
|
|
512
|
+
// it will not check if the group exists.
|
|
513
|
+
const cluster = this.getCluster(clusterKey);
|
|
514
|
+
const action = `Unbind ${this.deviceIeeeAddress}/${this.ID} ${cluster.name}`;
|
|
515
|
+
if (typeof target === "number") {
|
|
516
|
+
const groupTarget = group_1.default.byGroupID(target, this.databaseID);
|
|
517
|
+
if (groupTarget) {
|
|
518
|
+
target = groupTarget;
|
|
519
|
+
}
|
|
520
|
+
else if (!force) {
|
|
521
|
+
throw new Error(`${action} invalid target '${target}' (no group with this ID exists).`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
const destinationAddress = target instanceof Endpoint ? target.deviceIeeeAddress : target instanceof group_1.default ? target.groupID : target;
|
|
525
|
+
const log = `${action} from '${target instanceof Endpoint ? `${destinationAddress}/${target.ID}` : destinationAddress}'`;
|
|
526
|
+
const index = target instanceof Endpoint || target instanceof group_1.default ? this.getBindIndex(cluster.ID, target) : -1;
|
|
527
|
+
if (index === -1 && !force) {
|
|
528
|
+
logger_1.logger.debug(`${log} no bind present, skipping.`, NS);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
logger_1.logger.debug(log, NS);
|
|
532
|
+
try {
|
|
533
|
+
const zdoClusterId = Zdo.ClusterId.UNBIND_REQUEST;
|
|
534
|
+
const adapter = entity_1.default.getAdapterByID(this.databaseID);
|
|
535
|
+
if (!adapter) {
|
|
536
|
+
throw new Error(`No adapter found for database ID ${this.databaseID}`);
|
|
537
|
+
}
|
|
538
|
+
const zdoPayload = Zdo.Buffalo.buildRequest(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 : typeof target === "number" ? target : 0, target instanceof Endpoint ? target.ID : 0xff);
|
|
539
|
+
const response = await adapter.sendZdo(this.deviceIeeeAddress, this.deviceNetworkAddress, zdoClusterId, zdoPayload, false);
|
|
540
|
+
if (!Zdo.Buffalo.checkStatus(response)) {
|
|
541
|
+
if (response[0] === Zdo.Status.NO_ENTRY) {
|
|
542
|
+
logger_1.logger.debug(`${log} no entry on device, removing entry from database.`, NS);
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
throw new Zdo.StatusError(response[0]);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (index !== -1) {
|
|
549
|
+
this._binds.splice(index, 1);
|
|
550
|
+
this.save();
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
catch (error) {
|
|
554
|
+
const err = error;
|
|
555
|
+
err.message = `${log} failed (${err.message})`;
|
|
556
|
+
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
|
|
557
|
+
logger_1.logger.debug(err.stack, NS);
|
|
558
|
+
throw error;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
async defaultResponse(commandID, status, clusterID, transactionSequenceNumber, options) {
|
|
562
|
+
(0, node_assert_1.default)(options?.transactionSequenceNumber === undefined, "Use parameter");
|
|
563
|
+
const payload = { cmdId: commandID, statusCode: status };
|
|
564
|
+
await this.zclCommand(clusterID, "defaultRsp", payload, { direction: Zcl.Direction.SERVER_TO_CLIENT, ...options, transactionSequenceNumber });
|
|
565
|
+
}
|
|
566
|
+
async configureReporting(clusterKey, items, options) {
|
|
567
|
+
const cluster = this.getCluster(clusterKey, undefined, options?.manufacturerCode);
|
|
568
|
+
const optionsWithDefaults = this.getOptionsWithDefaults(options, true, Zcl.Direction.CLIENT_TO_SERVER, cluster.manufacturerCode);
|
|
569
|
+
optionsWithDefaults.manufacturerCode = this.ensureManufacturerCodeIsUniqueAndGet(cluster, items, optionsWithDefaults.manufacturerCode, "configureReporting");
|
|
570
|
+
const payload = items.map((item) => {
|
|
571
|
+
let dataType;
|
|
572
|
+
let attrId;
|
|
573
|
+
if (typeof item.attribute === "object") {
|
|
574
|
+
dataType = item.attribute.type;
|
|
575
|
+
attrId = item.attribute.ID;
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
const attribute = cluster.getAttribute(item.attribute);
|
|
579
|
+
if (attribute) {
|
|
580
|
+
dataType = attribute.type;
|
|
581
|
+
attrId = attribute.ID;
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
throw new Error(`Invalid attribute '${item.attribute}' for cluster '${clusterKey}'`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return {
|
|
588
|
+
direction: Zcl.Direction.CLIENT_TO_SERVER,
|
|
589
|
+
attrId,
|
|
590
|
+
dataType,
|
|
591
|
+
minRepIntval: item.minimumReportInterval,
|
|
592
|
+
maxRepIntval: item.maximumReportInterval,
|
|
593
|
+
repChange: item.reportableChange,
|
|
594
|
+
};
|
|
595
|
+
});
|
|
596
|
+
await this.zclCommand(cluster, "configReport", payload, optionsWithDefaults, items, true);
|
|
597
|
+
for (const e of payload) {
|
|
598
|
+
this._configuredReportings = this._configuredReportings.filter((c) => !(c.attrId === e.attrId &&
|
|
599
|
+
c.cluster === cluster.ID &&
|
|
600
|
+
(!("manufacturerCode" in c) || c.manufacturerCode === optionsWithDefaults.manufacturerCode)));
|
|
601
|
+
}
|
|
602
|
+
for (const entry of payload) {
|
|
603
|
+
if (entry.maxRepIntval !== 0xffff) {
|
|
604
|
+
this._configuredReportings.push({
|
|
605
|
+
cluster: cluster.ID,
|
|
606
|
+
attrId: entry.attrId,
|
|
607
|
+
minRepIntval: entry.minRepIntval,
|
|
608
|
+
maxRepIntval: entry.maxRepIntval,
|
|
609
|
+
// expects items[].attribute to always point to a number DataType
|
|
610
|
+
repChange: entry.repChange,
|
|
611
|
+
manufacturerCode: optionsWithDefaults.manufacturerCode,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
this.save();
|
|
616
|
+
}
|
|
617
|
+
async readReportingConfig(clusterKey, items, options) {
|
|
618
|
+
const cluster = this.getCluster(clusterKey, undefined, options?.manufacturerCode);
|
|
619
|
+
const optionsWithDefaults = this.getOptionsWithDefaults(options, true, Zcl.Direction.CLIENT_TO_SERVER, cluster.manufacturerCode);
|
|
620
|
+
optionsWithDefaults.manufacturerCode = this.ensureManufacturerCodeIsUniqueAndGet(cluster, items, optionsWithDefaults.manufacturerCode, "readReportingConfig");
|
|
621
|
+
const payload = [];
|
|
622
|
+
for (const item of items) {
|
|
623
|
+
if (typeof item.attribute === "object") {
|
|
624
|
+
payload.push({ direction: item.direction ?? Zcl.Direction.CLIENT_TO_SERVER, attrId: item.attribute.ID });
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
const attribute = cluster.getAttribute(item.attribute);
|
|
628
|
+
if (attribute) {
|
|
629
|
+
payload.push({ direction: item.direction ?? Zcl.Direction.CLIENT_TO_SERVER, attrId: attribute.ID });
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
logger_1.logger.warning(`Ignoring unknown attribute ${item.attribute} in cluster ${cluster.name}`, NS);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
// TODO: could be sending empty array payload
|
|
637
|
+
// don't check status otherwise whole command fails (we want to cherry-pick here)
|
|
638
|
+
const response = await this.zclCommand(cluster, "readReportConfig", payload, optionsWithDefaults, items, false);
|
|
639
|
+
if (response) {
|
|
640
|
+
this.saveClusterAttributeReportConfig(response.cluster.ID, optionsWithDefaults.manufacturerCode, response.payload);
|
|
641
|
+
return response.payload;
|
|
642
|
+
}
|
|
643
|
+
throw new Error("No response received");
|
|
644
|
+
}
|
|
645
|
+
async writeStructured(clusterKey, payload, options) {
|
|
646
|
+
await this.zclCommand(clusterKey, "writeStructured", payload, options);
|
|
647
|
+
// TODO: support `writeStructuredResponse`
|
|
648
|
+
}
|
|
649
|
+
async command(clusterKey, commandKey, payload, options) {
|
|
650
|
+
const frame = await this.zclCommand(clusterKey, commandKey, payload, options, undefined, false, Zcl.FrameType.SPECIFIC);
|
|
651
|
+
if (frame) {
|
|
652
|
+
return frame.payload;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
async commandResponse(clusterKey, commandKey, payload, options, transactionSequenceNumber) {
|
|
656
|
+
(0, node_assert_1.default)(options?.transactionSequenceNumber === undefined, "Use parameter");
|
|
657
|
+
const device = this.getDevice();
|
|
658
|
+
const cluster = this.getCluster(clusterKey, device, options?.manufacturerCode);
|
|
659
|
+
const command = cluster.getCommandResponse(commandKey);
|
|
660
|
+
transactionSequenceNumber = transactionSequenceNumber ?? zclTransactionSequenceNumber_1.default.next();
|
|
661
|
+
const optionsWithDefaults = this.getOptionsWithDefaults(options, true, Zcl.Direction.SERVER_TO_CLIENT, cluster.manufacturerCode);
|
|
662
|
+
const frame = Zcl.Frame.create(Zcl.FrameType.SPECIFIC, optionsWithDefaults.direction, optionsWithDefaults.disableDefaultResponse, optionsWithDefaults.manufacturerCode, transactionSequenceNumber, command, cluster, payload, device.customClusters, optionsWithDefaults.reservedBits);
|
|
663
|
+
const createLogMessage = () => `CommandResponse ${this.deviceIeeeAddress}/${this.ID} ` +
|
|
664
|
+
`${cluster.name}.${command.name}(${JSON.stringify(payload)}, ${JSON.stringify(optionsWithDefaults)})`;
|
|
665
|
+
logger_1.logger.debug(createLogMessage, NS);
|
|
666
|
+
try {
|
|
667
|
+
// Broadcast Green Power responses
|
|
668
|
+
if (this.ID === 242) {
|
|
669
|
+
await this.sendRequest(frame, optionsWithDefaults, async () => {
|
|
670
|
+
await entity_1.default.getAdapterByID(this.databaseID)?.sendZclFrameToAll(242, frame, 242, enums_1.BroadcastAddress.RX_ON_WHEN_IDLE);
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
await this.sendRequest(frame, optionsWithDefaults);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
catch (error) {
|
|
678
|
+
const err = error;
|
|
679
|
+
err.message = `${createLogMessage()} failed (${err.message})`;
|
|
680
|
+
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
|
|
681
|
+
logger_1.logger.debug(err.stack, NS);
|
|
682
|
+
throw error;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
getOptionsWithDefaults(options, disableDefaultResponse, direction, manufacturerCode) {
|
|
686
|
+
return {
|
|
687
|
+
timeout: 10000,
|
|
688
|
+
disableResponse: false,
|
|
689
|
+
disableRecovery: false,
|
|
690
|
+
disableDefaultResponse,
|
|
691
|
+
direction,
|
|
692
|
+
srcEndpoint: undefined,
|
|
693
|
+
reservedBits: 0,
|
|
694
|
+
manufacturerCode,
|
|
695
|
+
transactionSequenceNumber: undefined,
|
|
696
|
+
writeUndiv: false,
|
|
697
|
+
...(options || {}),
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
ensureManufacturerCodeIsUniqueAndGet(cluster, attributes, fallbackManufacturerCode, // XXX: problematic undefined for a "fallback"?
|
|
701
|
+
caller) {
|
|
702
|
+
let firstManufacturerCode;
|
|
703
|
+
let codeSet = false;
|
|
704
|
+
for (const nameOrID of attributes) {
|
|
705
|
+
let attributeID;
|
|
706
|
+
if (typeof nameOrID === "object") {
|
|
707
|
+
// ConfigureReportingItem
|
|
708
|
+
if (typeof nameOrID.attribute !== "object") {
|
|
709
|
+
attributeID = nameOrID.attribute;
|
|
710
|
+
}
|
|
711
|
+
else {
|
|
712
|
+
if (!codeSet) {
|
|
713
|
+
firstManufacturerCode = fallbackManufacturerCode;
|
|
714
|
+
codeSet = true;
|
|
715
|
+
}
|
|
716
|
+
else if (firstManufacturerCode !== fallbackManufacturerCode) {
|
|
717
|
+
throw new Error(`Cannot have attributes with different manufacturerCode in single '${caller}' call`);
|
|
718
|
+
}
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
// string || number
|
|
724
|
+
attributeID = nameOrID;
|
|
725
|
+
}
|
|
726
|
+
// we fall back to caller|cluster provided manufacturerCode
|
|
727
|
+
const attribute = cluster.getAttribute(attributeID);
|
|
728
|
+
const manufacturerCode = attribute
|
|
729
|
+
? attribute.manufacturerCode === undefined
|
|
730
|
+
? fallbackManufacturerCode
|
|
731
|
+
: attribute.manufacturerCode
|
|
732
|
+
: fallbackManufacturerCode;
|
|
733
|
+
if (!codeSet) {
|
|
734
|
+
firstManufacturerCode = manufacturerCode;
|
|
735
|
+
codeSet = true;
|
|
736
|
+
}
|
|
737
|
+
else if (firstManufacturerCode !== manufacturerCode) {
|
|
738
|
+
throw new Error(`Cannot have attributes with different manufacturerCode in single '${caller}' call`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return firstManufacturerCode;
|
|
742
|
+
}
|
|
743
|
+
async addToGroup(group) {
|
|
744
|
+
await this.zclCommand("genGroups", "add", { groupid: group.groupID, groupname: "" }, undefined, undefined, true, Zcl.FrameType.SPECIFIC);
|
|
745
|
+
group.addMember(this);
|
|
746
|
+
}
|
|
747
|
+
getCluster(clusterKey, device = undefined, manufacturerCode = undefined) {
|
|
748
|
+
if (!device) {
|
|
749
|
+
device = this.getDevice();
|
|
750
|
+
}
|
|
751
|
+
return Zcl.Utils.getCluster(clusterKey, manufacturerCode ?? device.manufacturerID, device.customClusters);
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Remove endpoint from a group, accepts both a Group and number as parameter.
|
|
755
|
+
* The number parameter type should only be used when removing from a group which is not known
|
|
756
|
+
* to zigbee-herdsman.
|
|
757
|
+
*/
|
|
758
|
+
async removeFromGroup(group) {
|
|
759
|
+
await this.zclCommand("genGroups", "remove", { groupid: group instanceof group_1.default ? group.groupID : group }, undefined, undefined, true, Zcl.FrameType.SPECIFIC);
|
|
760
|
+
if (group instanceof group_1.default) {
|
|
761
|
+
group.removeMember(this);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
async removeFromAllGroups() {
|
|
765
|
+
await this.zclCommand("genGroups", "removeAll", {}, { disableDefaultResponse: true }, undefined, false, Zcl.FrameType.SPECIFIC);
|
|
766
|
+
this.removeFromAllGroupsDatabase();
|
|
767
|
+
}
|
|
768
|
+
removeFromAllGroupsDatabase() {
|
|
769
|
+
for (const group of group_1.default.allByDatabaseID(this.databaseID)) {
|
|
770
|
+
if (group.hasMember(this)) {
|
|
771
|
+
group.removeMember(this);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
async zclCommand(clusterKey, commandKey, payload, options, logPayload, checkStatus = false, frameType = Zcl.FrameType.GLOBAL) {
|
|
776
|
+
const device = this.getDevice();
|
|
777
|
+
const cluster = typeof clusterKey === "object" ? clusterKey : this.getCluster(clusterKey, device, options?.manufacturerCode);
|
|
778
|
+
const command = typeof commandKey === "object"
|
|
779
|
+
? commandKey
|
|
780
|
+
: frameType === Zcl.FrameType.GLOBAL
|
|
781
|
+
? Zcl.Utils.getGlobalCommand(commandKey)
|
|
782
|
+
: cluster.getCommand(commandKey);
|
|
783
|
+
const hasResponse = frameType === Zcl.FrameType.GLOBAL ? true : command.response !== undefined;
|
|
784
|
+
const optionsWithDefaults = this.getOptionsWithDefaults(options, hasResponse, Zcl.Direction.CLIENT_TO_SERVER, cluster.manufacturerCode);
|
|
785
|
+
const frame = Zcl.Frame.create(frameType, optionsWithDefaults.direction, optionsWithDefaults.disableDefaultResponse, optionsWithDefaults.manufacturerCode, optionsWithDefaults.transactionSequenceNumber ?? zclTransactionSequenceNumber_1.default.next(), command, cluster, payload, device.customClusters, optionsWithDefaults.reservedBits);
|
|
786
|
+
const createLogMessage = () => `ZCL command ${this.deviceIeeeAddress}/${this.ID} ` +
|
|
787
|
+
`${cluster.name}.${command.name}(${JSON.stringify(logPayload ? logPayload : payload)}, ${JSON.stringify(optionsWithDefaults)})`;
|
|
788
|
+
logger_1.logger.debug(createLogMessage, NS);
|
|
789
|
+
try {
|
|
790
|
+
const result = await this.sendRequest(frame, optionsWithDefaults);
|
|
791
|
+
if (result) {
|
|
792
|
+
const resultFrame = Zcl.Frame.fromBuffer(result.clusterID, result.header, result.data, device.customClusters);
|
|
793
|
+
if (checkStatus && !optionsWithDefaults.disableResponse) {
|
|
794
|
+
this.checkStatus(resultFrame.payload);
|
|
795
|
+
}
|
|
796
|
+
return resultFrame;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
catch (error) {
|
|
800
|
+
const err = error;
|
|
801
|
+
err.message = `${createLogMessage()} failed (${err.message})`;
|
|
802
|
+
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
|
|
803
|
+
logger_1.logger.debug(err.stack, NS);
|
|
804
|
+
throw error;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
async zclCommandBroadcast(endpoint, destination, clusterKey, commandKey, payload, options) {
|
|
808
|
+
const device = this.getDevice();
|
|
809
|
+
const cluster = this.getCluster(clusterKey, device, options?.manufacturerCode);
|
|
810
|
+
const command = cluster.getCommand(commandKey);
|
|
811
|
+
const optionsWithDefaults = this.getOptionsWithDefaults(options, true, Zcl.Direction.CLIENT_TO_SERVER, cluster.manufacturerCode);
|
|
812
|
+
const sourceEndpoint = optionsWithDefaults.srcEndpoint ?? this.ID;
|
|
813
|
+
const frame = Zcl.Frame.create(Zcl.FrameType.SPECIFIC, optionsWithDefaults.direction, true, optionsWithDefaults.manufacturerCode, optionsWithDefaults.transactionSequenceNumber ?? zclTransactionSequenceNumber_1.default.next(), command, cluster, payload, device.customClusters, optionsWithDefaults.reservedBits);
|
|
814
|
+
logger_1.logger.debug(() => `ZCL command broadcast ${this.deviceIeeeAddress}/${sourceEndpoint} to ${destination}/${endpoint} ` +
|
|
815
|
+
`${cluster.name}.${command.name}(${JSON.stringify({ payload, optionsWithDefaults })})`, NS);
|
|
816
|
+
// if endpoint===0xFF ("broadcast endpoint"), deliver to all endpoints supporting cluster, should be avoided whenever possible
|
|
817
|
+
await entity_1.default.getAdapterByID(this.databaseID)?.sendZclFrameToAll(endpoint, frame, sourceEndpoint, destination);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
exports.Endpoint = Endpoint;
|
|
821
|
+
exports.default = Endpoint;
|
|
822
|
+
//# sourceMappingURL=endpoint.js.map
|