@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.
Files changed (114) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/CHANGELOG.md +38 -0
  3. package/biome.json +1 -1
  4. package/dist/adapter/adapter.d.ts.map +1 -0
  5. package/dist/adapter/adapterDiscovery.d.ts.map +1 -0
  6. package/dist/adapter/const.d.ts.map +1 -0
  7. package/dist/adapter/deconz/adapter/deconzAdapter.d.ts.map +1 -0
  8. package/dist/adapter/ember/adapter/emberAdapter.d.ts.map +1 -0
  9. package/dist/adapter/ember/adapter/endpoints.d.ts.map +1 -0
  10. package/dist/adapter/ember/adapter/oneWaitress.d.ts.map +1 -0
  11. package/dist/adapter/ember/adapter/tokensManager.d.ts.map +1 -0
  12. package/dist/adapter/ember/ezsp/ezsp.d.ts.map +1 -0
  13. package/dist/adapter/ember/utils/initters.d.ts.map +1 -0
  14. package/dist/adapter/events.d.ts.map +1 -0
  15. package/dist/adapter/ezsp/adapter/backup.d.ts.map +1 -0
  16. package/dist/adapter/ezsp/adapter/ezspAdapter.d.ts.map +1 -0
  17. package/dist/adapter/ezsp/driver/driver.d.ts.map +1 -0
  18. package/dist/adapter/ezsp/driver/index.d.ts.map +1 -0
  19. package/dist/adapter/ezsp/driver/multicast.d.ts.map +1 -0
  20. package/dist/adapter/index.d.ts.map +1 -0
  21. package/dist/adapter/z-stack/adapter/endpoints.d.ts.map +1 -0
  22. package/dist/adapter/z-stack/adapter/manager.d.ts.map +1 -0
  23. package/dist/adapter/z-stack/adapter/zStackAdapter.d.ts.map +1 -0
  24. package/dist/adapter/z-stack/models/startup-options.d.ts.map +1 -0
  25. package/dist/adapter/zboss/adapter/zbossAdapter.d.ts.map +1 -0
  26. package/dist/adapter/zboss/driver.d.ts.map +1 -0
  27. package/dist/adapter/zboss/frame.d.ts.map +1 -0
  28. package/dist/adapter/zboss/frame.js +200 -0
  29. package/dist/adapter/zboss/frame.js.map +1 -0
  30. package/dist/adapter/zboss/uart.d.ts.map +1 -0
  31. package/dist/adapter/zigate/adapter/zigateAdapter.d.ts.map +1 -0
  32. package/dist/adapter/zigate/driver/buffaloZiGate.d.ts.map +1 -0
  33. package/dist/adapter/zigate/driver/buffaloZiGate.js +198 -0
  34. package/dist/adapter/zigate/driver/buffaloZiGate.js.map +1 -0
  35. package/dist/adapter/zigate/driver/ziGateObject.d.ts.map +1 -0
  36. package/dist/adapter/zigate/driver/zigate.d.ts.map +1 -0
  37. package/dist/adapter/zoh/adapter/zohAdapter.d.ts.map +1 -0
  38. package/dist/controller/controller.d.ts.map +1 -0
  39. package/dist/controller/controller.js +874 -0
  40. package/dist/controller/controller.js.map +1 -0
  41. package/dist/controller/database.d.ts.map +1 -0
  42. package/dist/controller/events.d.ts.map +1 -0
  43. package/dist/controller/events.js +3 -0
  44. package/dist/controller/events.js.map +1 -0
  45. package/dist/controller/greenPower.d.ts.map +1 -0
  46. package/dist/controller/greenPower.js +425 -0
  47. package/dist/controller/greenPower.js.map +1 -0
  48. package/dist/controller/helpers/index.d.ts.map +1 -0
  49. package/dist/controller/helpers/ota.d.ts.map +1 -0
  50. package/dist/controller/helpers/ota.js +467 -0
  51. package/dist/controller/helpers/ota.js.map +1 -0
  52. package/dist/controller/helpers/request.d.ts.map +1 -0
  53. package/dist/controller/helpers/requestQueue.d.ts.map +1 -0
  54. package/dist/controller/helpers/zclFrameConverter.d.ts.map +1 -0
  55. package/dist/controller/helpers/zclFrameConverter.js +84 -0
  56. package/dist/controller/helpers/zclFrameConverter.js.map +1 -0
  57. package/dist/controller/index.d.ts.map +1 -0
  58. package/dist/controller/model/device.d.ts.map +1 -0
  59. package/dist/controller/model/device.js +1396 -0
  60. package/dist/controller/model/device.js.map +1 -0
  61. package/dist/controller/model/endpoint.d.ts.map +1 -0
  62. package/dist/controller/model/endpoint.js +822 -0
  63. package/dist/controller/model/endpoint.js.map +1 -0
  64. package/dist/controller/model/entity.d.ts.map +1 -0
  65. package/dist/controller/model/group.d.ts.map +1 -0
  66. package/dist/controller/model/group.js +343 -0
  67. package/dist/controller/model/group.js.map +1 -0
  68. package/dist/controller/model/index.d.ts.map +1 -0
  69. package/dist/controller/model/zigbeeEntity.d.ts.map +1 -0
  70. package/dist/controller/touchlink.d.ts.map +1 -0
  71. package/dist/controller/tstype.d.ts.map +1 -0
  72. package/dist/controller/tstype.js +3 -0
  73. package/dist/controller/tstype.js.map +1 -0
  74. package/dist/index.d.ts.map +1 -0
  75. package/dist/utils/timeService.d.ts.map +1 -0
  76. package/dist/utils/timeService.js +127 -0
  77. package/dist/utils/timeService.js.map +1 -0
  78. package/dist/zspec/zcl/buffaloZcl.d.ts.map +1 -0
  79. package/dist/zspec/zcl/buffaloZcl.js +969 -0
  80. package/dist/zspec/zcl/buffaloZcl.js.map +1 -0
  81. package/dist/zspec/zcl/definition/cluster.d.ts.map +1 -0
  82. package/dist/zspec/zcl/definition/cluster.js +7507 -0
  83. package/dist/zspec/zcl/definition/cluster.js.map +1 -0
  84. package/dist/zspec/zcl/definition/clusters-types.d.ts +8135 -0
  85. package/dist/zspec/zcl/definition/clusters-types.d.ts.map +1 -0
  86. package/dist/zspec/zcl/definition/clusters-types.js +3 -0
  87. package/dist/zspec/zcl/definition/clusters-types.js.map +1 -0
  88. package/dist/zspec/zcl/definition/foundation.d.ts.map +1 -0
  89. package/dist/zspec/zcl/definition/foundation.js +312 -0
  90. package/dist/zspec/zcl/definition/foundation.js.map +1 -0
  91. package/dist/zspec/zcl/definition/tstype.d.ts +273 -0
  92. package/dist/zspec/zcl/definition/tstype.d.ts.map +1 -0
  93. package/dist/zspec/zcl/definition/tstype.js +3 -0
  94. package/dist/zspec/zcl/definition/tstype.js.map +1 -0
  95. package/dist/zspec/zcl/index.d.ts.map +1 -0
  96. package/dist/zspec/zcl/index.js +57 -0
  97. package/dist/zspec/zcl/index.js.map +1 -0
  98. package/dist/zspec/zcl/utils.d.ts.map +1 -0
  99. package/dist/zspec/zcl/utils.js +419 -0
  100. package/dist/zspec/zcl/utils.js.map +1 -0
  101. package/dist/zspec/zcl/zclFrame.d.ts.map +1 -0
  102. package/dist/zspec/zcl/zclFrame.js +328 -0
  103. package/dist/zspec/zcl/zclFrame.js.map +1 -0
  104. package/dist/zspec/zcl/zclHeader.d.ts.map +1 -0
  105. package/dist/zspec/zcl/zclHeader.js +88 -0
  106. package/dist/zspec/zcl/zclHeader.js.map +1 -0
  107. package/package.json +90 -83
  108. package/src/controller/helpers/ota.ts +5 -2
  109. package/src/zspec/zcl/definition/cluster.ts +3 -294
  110. package/src/zspec/zcl/definition/clusters-types.ts +6 -355
  111. package/src/zspec/zcl/definition/tstype.ts +0 -14
  112. package/src/zspec/zcl/utils.ts +14 -31
  113. package/test/controller.test.ts +16 -8
  114. package/test/zcl.test.ts +36 -7
@@ -0,0 +1,874 @@
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.Controller = void 0;
40
+ const node_assert_1 = __importDefault(require("node:assert"));
41
+ const node_events_1 = __importDefault(require("node:events"));
42
+ const node_fs_1 = __importDefault(require("node:fs"));
43
+ const adapter_1 = require("../adapter");
44
+ const utils_1 = require("../utils");
45
+ const logger_1 = require("../utils/logger");
46
+ const utils_2 = require("../utils/utils");
47
+ const ZSpec = __importStar(require("../zspec"));
48
+ const Zcl = __importStar(require("../zspec/zcl"));
49
+ const Zdo = __importStar(require("../zspec/zdo"));
50
+ const database_1 = __importDefault(require("./database"));
51
+ const greenPower_1 = __importDefault(require("./greenPower"));
52
+ const helpers_1 = require("./helpers");
53
+ const installCodes_1 = require("./helpers/installCodes");
54
+ const zclTransactionSequenceNumber_1 = __importDefault(require("./helpers/zclTransactionSequenceNumber"));
55
+ const model_1 = require("./model");
56
+ const device_1 = require("./model/device");
57
+ const group_1 = __importDefault(require("./model/group"));
58
+ const touchlink_1 = __importDefault(require("./touchlink"));
59
+ const NS = "zh:controller";
60
+ /**
61
+ * @noInheritDoc
62
+ */
63
+ class Controller extends node_events_1.default.EventEmitter {
64
+ options;
65
+ database;
66
+ adapter;
67
+ #greenPower;
68
+ #touchlink;
69
+ permitJoinTimer;
70
+ permitJoinEnd;
71
+ backupTimer;
72
+ databaseSaveTimer;
73
+ stopping;
74
+ adapterDisconnected;
75
+ networkParametersCached;
76
+ /** List of unknown devices detected during a single runtime session. Serves as de-dupe and anti-spam. */
77
+ unknownDevices;
78
+ /**
79
+ * Create a controller
80
+ *
81
+ * To auto detect the port provide `null` for `options.serialPort.path`
82
+ */
83
+ constructor(options) {
84
+ super();
85
+ this.stopping = false;
86
+ this.adapterDisconnected = true; // set false after adapter.start() is successfully called
87
+ const { network: networkOpts, serialPort: serialPortOpts, adapter: adapterOpts, konnextConfig: konnextConfigOpts, ...restOpts } = options;
88
+ this.options = {
89
+ network: {
90
+ networkKeyDistribute: false,
91
+ networkKey: [0x01, 0x03, 0x05, 0x07, 0x09, 0x0b, 0x0d, 0x0f, 0x00, 0x02, 0x04, 0x06, 0x08, 0x0a, 0x0c, 0x0d],
92
+ extendedPanID: [0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd],
93
+ ...networkOpts,
94
+ },
95
+ serialPort: { ...serialPortOpts },
96
+ adapter: {
97
+ ...adapterOpts,
98
+ },
99
+ konnextConfig: {
100
+ // @ts-ignore-next-line we need to set a default value for isEncrypted
101
+ isEncrypted: 0,
102
+ ...konnextConfigOpts,
103
+ },
104
+ ...restOpts,
105
+ };
106
+ this.unknownDevices = new Set();
107
+ // Validate options
108
+ for (const channel of this.options.network.channelList) {
109
+ if (channel < 11 || channel > 26) {
110
+ throw new Error(`'${channel}' is an invalid channel, use a channel between 11 - 26.`);
111
+ }
112
+ }
113
+ if (!(0, utils_2.isNumberArrayOfLength)(this.options.network.networkKey, 16)) {
114
+ throw new Error(`Network key must be a 16 digits long array, got ${this.options.network.networkKey}.`);
115
+ }
116
+ if (!(0, utils_2.isNumberArrayOfLength)(this.options.network.extendedPanID, 8)) {
117
+ throw new Error(`ExtendedPanID must be an 8 digits long array, got ${this.options.network.extendedPanID}.`);
118
+ }
119
+ if (this.options.network.panID < 1 || this.options.network.panID >= 0xffff) {
120
+ throw new Error(`PanID must have a value of 0x0001 (1) - 0xFFFE (65534), got ${this.options.network.panID}.`);
121
+ }
122
+ }
123
+ get greenPower() {
124
+ return this.#greenPower;
125
+ }
126
+ get touchlink() {
127
+ return this.#touchlink;
128
+ }
129
+ /**
130
+ * Start the Herdsman controller
131
+ */
132
+ async start(abortSignal) {
133
+ // Database (create end inject)
134
+ this.database = database_1.default.open(this.options.databasePath);
135
+ model_1.Entity.injectDatabase(this.database);
136
+ // Adapter (create and inject)
137
+ this.adapter = await adapter_1.Adapter.create(this.options.network, this.options.serialPort, this.options.backupPath, this.options.adapter, this.options.konnextConfig);
138
+ abortSignal?.throwIfAborted();
139
+ const stringifiedOptions = JSON.stringify(this.options).replaceAll(JSON.stringify(this.options.network.networkKey), '"HIDDEN"');
140
+ logger_1.logger.debug(`Starting with options '${stringifiedOptions}'`, NS);
141
+ const startResult = await this.adapter.start();
142
+ logger_1.logger.debug(`Started with result '${startResult}'`, NS);
143
+ this.adapterDisconnected = false;
144
+ abortSignal?.throwIfAborted();
145
+ // Check if we have to change the channel, only do this when adapter `resumed` because:
146
+ // - `getNetworkParameters` might be return wrong info because it needs to propogate after backup restore
147
+ // - If result is not `resumed` (`reset` or `restored`), the adapter should comission with the channel from `this.options.network`
148
+ if (startResult === "resumed") {
149
+ const netParams = await this.getNetworkParameters();
150
+ const configuredChannel = this.options.network.channelList[0];
151
+ const adapterChannel = netParams.channel;
152
+ const nwkUpdateID = netParams.nwkUpdateID;
153
+ if (configuredChannel !== adapterChannel) {
154
+ logger_1.logger.info(`Configured channel '${configuredChannel}' does not match adapter channel '${adapterChannel}', changing channel`, NS);
155
+ await this.changeChannel(adapterChannel, configuredChannel, nwkUpdateID);
156
+ abortSignal?.throwIfAborted();
157
+ }
158
+ }
159
+ model_1.Entity.injectAdapter(this.database.id, this.adapter);
160
+ // log injection
161
+ logger_1.logger.debug(`Injected database: ${this.database !== undefined}, adapter: ${this.adapter !== undefined}`, NS);
162
+ this.#greenPower = new greenPower_1.default(this.adapter, this.database.id);
163
+ this.#greenPower.on("deviceJoined", this.onDeviceJoinedGreenPower.bind(this));
164
+ this.#greenPower.on("deviceLeave", this.onDeviceLeaveGreenPower.bind(this));
165
+ // Register adapter events
166
+ this.adapter.on("deviceJoined", this.onDeviceJoined.bind(this));
167
+ this.adapter.on("zclPayload", this.onZclPayload.bind(this));
168
+ this.adapter.on("zdoResponse", this.onZdoResponse.bind(this));
169
+ this.adapter.on("disconnected", this.onAdapterDisconnected.bind(this));
170
+ this.adapter.on("deviceLeave", this.onDeviceLeave.bind(this));
171
+ if (startResult === "reset") {
172
+ if (this.options.databaseBackupPath && node_fs_1.default.existsSync(this.options.databasePath)) {
173
+ node_fs_1.default.copyFileSync(this.options.databasePath, this.options.databaseBackupPath);
174
+ }
175
+ this.database.clear();
176
+ group_1.default.resetCache();
177
+ model_1.Device.resetCache();
178
+ abortSignal?.throwIfAborted();
179
+ }
180
+ if (startResult === "reset" || (this.options.backupPath && !node_fs_1.default.existsSync(this.options.backupPath))) {
181
+ await this.backup();
182
+ abortSignal?.throwIfAborted();
183
+ }
184
+ // Add coordinator to the database if it is not there yet.
185
+ const coordinatorIEEE = await this.adapter.getCoordinatorIEEE();
186
+ if (model_1.Device.byType(this.database.id, "Coordinator").length === 0) {
187
+ logger_1.logger.debug("No coordinator in database, querying...", NS);
188
+ const coordinator = model_1.Device.create("Coordinator", coordinatorIEEE, ZSpec.COORDINATOR_ADDRESS, this.adapter.manufacturerID, undefined, undefined, undefined, device_1.InterviewState.Successful, undefined, this.database.id);
189
+ await coordinator.updateActiveEndpoints();
190
+ abortSignal?.throwIfAborted();
191
+ for (const endpoint of coordinator.endpoints) {
192
+ await endpoint.updateSimpleDescriptor();
193
+ abortSignal?.throwIfAborted();
194
+ }
195
+ coordinator.save();
196
+ }
197
+ // Update coordinator ieeeAddr if changed, can happen due to e.g. reflashing
198
+ const databaseCoordinator = model_1.Device.byType(this.database.id, "Coordinator")[0];
199
+ if (databaseCoordinator.ieeeAddr !== coordinatorIEEE) {
200
+ logger_1.logger.info(`Coordinator address changed, updating to '${coordinatorIEEE}'`, NS);
201
+ databaseCoordinator.changeIeeeAddress(coordinatorIEEE);
202
+ }
203
+ // Set backup timer to 1 day.
204
+ this.backupTimer = setInterval(() => this.backup(), 86400000);
205
+ // Set database save timer to 5 minutes.
206
+ this.databaseSaveTimer = setInterval(() => this.databaseSave(), 300000);
207
+ this.#touchlink = new touchlink_1.default(this.adapter);
208
+ return startResult;
209
+ }
210
+ /**
211
+ * Send a request according to given payload.
212
+ * @param rawPayload Payload used to determine and build the request
213
+ * @param customClusters Manually passed custom clusters used in ZCL serialization (if any, if matching)
214
+ * @returns A response may or may not be returned depending on given payload (up to caller to verify)
215
+ */
216
+ async sendRaw(rawPayload, customClusters = {}) {
217
+ const { ieeeAddress, networkAddress, groupId, dstEndpoint, srcEndpoint = ZSpec.HA_ENDPOINT, interPan = false, profileId = ZSpec.HA_PROFILE_ID, clusterKey, zdoParams, zcl, disableResponse = false, timeout = 10000, } = rawPayload;
218
+ if (profileId === Zdo.ZDO_PROFILE_ID) {
219
+ (0, node_assert_1.default)(ieeeAddress);
220
+ (0, node_assert_1.default)(networkAddress !== undefined);
221
+ (0, node_assert_1.default)(clusterKey !== undefined && typeof clusterKey === "number");
222
+ // no ZDO request takes 0 params
223
+ (0, node_assert_1.default)(Array.isArray(zdoParams) && zdoParams.length > 0);
224
+ // will fail if args are incorrect for request
225
+ const buf = Zdo.Buffalo.buildRequest(this.adapter.hasZdoMessageOverhead, clusterKey, ...zdoParams);
226
+ return (await this.adapter.sendZdo(ieeeAddress, networkAddress, clusterKey, buf, disableResponse));
227
+ }
228
+ (0, node_assert_1.default)(zcl);
229
+ if (interPan) {
230
+ (0, node_assert_1.default)(zcl.commandKey);
231
+ (0, node_assert_1.default)(zcl.payload);
232
+ (0, node_assert_1.default)(zcl.frameType === undefined);
233
+ (0, node_assert_1.default)(zcl.direction === undefined);
234
+ const zclFrame = Zcl.Frame.create(zcl.frameType ?? Zcl.FrameType.SPECIFIC, zcl.direction ?? Zcl.Direction.CLIENT_TO_SERVER, true, zcl.manufacturerCode, 0, zcl.commandKey, clusterKey ?? Zcl.Clusters.touchlink.ID, zcl.payload, customClusters);
235
+ if (ieeeAddress) {
236
+ await this.adapter.sendZclFrameInterPANToIeeeAddr(zclFrame, ieeeAddress);
237
+ return;
238
+ }
239
+ return await this.adapter.sendZclFrameInterPANBroadcast(zclFrame, timeout, disableResponse);
240
+ }
241
+ (0, node_assert_1.default)(clusterKey !== undefined);
242
+ (0, node_assert_1.default)(zcl.frameType !== undefined);
243
+ (0, node_assert_1.default)(zcl.direction !== undefined);
244
+ const zclFrame = Zcl.Frame.create(zcl.frameType, zcl.direction, zcl.disableDefaultResponse ?? false, zcl.manufacturerCode, zcl.tsn ?? zclTransactionSequenceNumber_1.default.next(), zcl.commandKey, clusterKey, zcl.payload, customClusters);
245
+ if (groupId !== undefined) {
246
+ (0, node_assert_1.default)(groupId >= 0x0000 && groupId <= 0xffff);
247
+ await this.adapter.sendZclFrameToGroup(groupId, zclFrame, srcEndpoint, profileId);
248
+ return;
249
+ }
250
+ (0, node_assert_1.default)(dstEndpoint !== undefined && dstEndpoint >= 0x01 && dstEndpoint <= 0xff);
251
+ (0, node_assert_1.default)(srcEndpoint >= 0x01 && srcEndpoint <= 0xff);
252
+ (0, node_assert_1.default)(networkAddress !== undefined && networkAddress >= 0x0000 && networkAddress <= 0xffff);
253
+ if (networkAddress >= ZSpec.BROADCAST_MIN) {
254
+ await this.adapter.sendZclFrameToAll(dstEndpoint, zclFrame, srcEndpoint, networkAddress, profileId);
255
+ return;
256
+ }
257
+ (0, node_assert_1.default)(ieeeAddress);
258
+ return await this.adapter.sendZclFrameToEndpoint(ieeeAddress, networkAddress, dstEndpoint, zclFrame, timeout, disableResponse, false, srcEndpoint, profileId);
259
+ }
260
+ async addInstallCode(installCode) {
261
+ // will throw if code cannot be parsed
262
+ const [ieeeAddr, keyStr] = (0, installCodes_1.parseInstallCode)(installCode);
263
+ // biome-ignore lint/style/noNonNullAssertion: valid from above parsing
264
+ const key = Buffer.from(keyStr.match(/.{1,2}/g).map((d) => Number.parseInt(d, 16)));
265
+ // will throw if code cannot be fixed and is invalid
266
+ const [adjustedKey, adjusted] = (0, installCodes_1.checkInstallCode)(key, true);
267
+ if (adjusted) {
268
+ logger_1.logger.info(`Install code was adjusted for reason '${adjusted}'.`, NS);
269
+ }
270
+ logger_1.logger.info(`Adding install code for ${ieeeAddr}.`, NS);
271
+ await this.adapter.addInstallCode(ieeeAddr, adjustedKey, false);
272
+ if (adjusted === "missing CRC") {
273
+ // in case the CRC was missing, could also be a "already-hashed" key, send both
274
+ // XXX: seems to be the case for old HA1.2 devices
275
+ await this.adapter.addInstallCode(ieeeAddr, key, true);
276
+ }
277
+ }
278
+ async permitJoin(time, device) {
279
+ clearTimeout(this.permitJoinTimer);
280
+ this.permitJoinTimer = undefined;
281
+ this.permitJoinEnd = undefined;
282
+ if (time > 0) {
283
+ // never permit more than uint8, and never permit 255 that is often equal to "forever"
284
+ (0, node_assert_1.default)(time <= 254, "Cannot permit join for more than 254 seconds.");
285
+ await this.adapter.permitJoin(time, device?.networkAddress);
286
+ await this.#greenPower.permitJoin(time, device?.networkAddress);
287
+ const timeMs = time * 1000;
288
+ this.permitJoinEnd = Date.now() + timeMs;
289
+ this.permitJoinTimer = setTimeout(() => {
290
+ this.emit("permitJoinChanged", { permitted: false });
291
+ this.permitJoinTimer = undefined;
292
+ this.permitJoinEnd = undefined;
293
+ }, timeMs);
294
+ this.emit("permitJoinChanged", { permitted: true, time });
295
+ }
296
+ else {
297
+ logger_1.logger.debug("Disable joining", NS);
298
+ await this.#greenPower.permitJoin(0);
299
+ await this.adapter.permitJoin(0);
300
+ this.emit("permitJoinChanged", { permitted: false });
301
+ }
302
+ }
303
+ getPermitJoin() {
304
+ return this.permitJoinTimer !== undefined;
305
+ }
306
+ getPermitJoinEnd() {
307
+ return this.permitJoinEnd;
308
+ }
309
+ isStopping() {
310
+ return this.stopping;
311
+ }
312
+ isAdapterDisconnected() {
313
+ return this.adapterDisconnected;
314
+ }
315
+ async stop() {
316
+ this.stopping = true;
317
+ // Unregister adapter events
318
+ this.adapter.removeAllListeners();
319
+ clearInterval(this.backupTimer);
320
+ clearInterval(this.databaseSaveTimer);
321
+ if (this.adapterDisconnected) {
322
+ this.databaseSave();
323
+ }
324
+ else {
325
+ try {
326
+ await this.#touchlink.stop();
327
+ }
328
+ catch (error) {
329
+ logger_1.logger.error(`Failed to stop Touchlink: ${error}`, NS);
330
+ }
331
+ try {
332
+ await this.permitJoin(0);
333
+ }
334
+ catch (error) {
335
+ logger_1.logger.error(`Failed to disable join on stop: ${error}`, NS);
336
+ }
337
+ await this.backup(); // always calls databaseSave()
338
+ await this.adapter.stop();
339
+ this.adapterDisconnected = true;
340
+ }
341
+ model_1.Device.resetCache();
342
+ group_1.default.resetCache();
343
+ model_1.Entity.removeAdapter(this.database.id);
344
+ model_1.Entity.removeDatabase(this.database.id);
345
+ }
346
+ databaseSave() {
347
+ for (const device of model_1.Device.allIterator(this.database.id)) {
348
+ device.save(false);
349
+ }
350
+ for (const group of group_1.default.allIterator(this.database.id)) {
351
+ group.save(false);
352
+ }
353
+ this.database.write();
354
+ }
355
+ async backup() {
356
+ this.databaseSave();
357
+ if (this.options.backupPath && (await this.adapter.supportsBackup())) {
358
+ logger_1.logger.debug("Creating coordinator backup", NS);
359
+ const backup = await this.adapter.backup(this.getDeviceIeeeAddresses());
360
+ const unifiedBackup = utils_1.BackupUtils.toUnifiedBackup(backup);
361
+ const tmpBackupPath = `${this.options.backupPath}.tmp`;
362
+ node_fs_1.default.writeFileSync(tmpBackupPath, JSON.stringify(unifiedBackup, null, 2));
363
+ node_fs_1.default.renameSync(tmpBackupPath, this.options.backupPath);
364
+ logger_1.logger.info(`Wrote coordinator backup to '${this.options.backupPath}'`, NS);
365
+ }
366
+ }
367
+ async coordinatorCheck() {
368
+ if (await this.adapter.supportsBackup()) {
369
+ const backup = await this.adapter.backup(this.getDeviceIeeeAddresses());
370
+ const devicesInBackup = backup.devices.map((d) => ZSpec.Utils.eui64BEBufferToHex(d.ieeeAddress));
371
+ const missingRouters = [];
372
+ for (const device of this.getDevicesIterator((d) => d.type === "Router" && !devicesInBackup.includes(d.ieeeAddr))) {
373
+ missingRouters.push(device);
374
+ }
375
+ return { missingRouters };
376
+ }
377
+ throw new Error("Coordinator does not coordinator check because it doesn't support backups");
378
+ }
379
+ async reset(type) {
380
+ await this.adapter.reset(type);
381
+ }
382
+ async getCoordinatorVersion() {
383
+ return await this.adapter.getCoordinatorVersion();
384
+ }
385
+ async getNetworkParameters() {
386
+ // Cache network parameters as they don't change anymore after start.
387
+ if (!this.networkParametersCached) {
388
+ this.networkParametersCached = await this.adapter.getNetworkParameters();
389
+ }
390
+ return this.networkParametersCached;
391
+ }
392
+ /**
393
+ * Get the database ID for this controller's database.
394
+ */
395
+ getDatabaseId() {
396
+ return this.database.id;
397
+ }
398
+ /**
399
+ * Get all devices
400
+ * @deprecated use getDevicesIterator()
401
+ */
402
+ getDevices() {
403
+ return model_1.Device.allByDatabaseID(this.database.id);
404
+ }
405
+ /**
406
+ * Get iterator for all devices
407
+ */
408
+ getDevicesIterator(predicate) {
409
+ return model_1.Device.allIterator(this.database.id, predicate);
410
+ }
411
+ /**
412
+ * Get all devices with a specific type
413
+ */
414
+ getDevicesByType(type) {
415
+ return model_1.Device.byType(this.database.id, type);
416
+ }
417
+ /**
418
+ * Get device by ieeeAddr
419
+ */
420
+ getDeviceByIeeeAddr(ieeeAddr) {
421
+ return model_1.Device.byIeeeAddr(this.database.id, ieeeAddr);
422
+ }
423
+ /**
424
+ * Get device by networkAddress
425
+ */
426
+ getDeviceByNetworkAddress(networkAddress) {
427
+ return model_1.Device.byNetworkAddress(this.database.id, networkAddress);
428
+ }
429
+ /**
430
+ * Get IEEE address for all devices
431
+ */
432
+ getDeviceIeeeAddresses() {
433
+ const deviceIeeeAddresses = [];
434
+ for (const device of model_1.Device.allIterator(this.database.id)) {
435
+ deviceIeeeAddresses.push(device.ieeeAddr);
436
+ }
437
+ return deviceIeeeAddresses;
438
+ }
439
+ /**
440
+ * Get group by ID
441
+ */
442
+ getGroupByID(groupID) {
443
+ return group_1.default.byGroupID(groupID, this.database.id);
444
+ }
445
+ /**
446
+ * Get all groups
447
+ * @deprecated use getGroupsIterator()
448
+ */
449
+ getGroups() {
450
+ return group_1.default.allByDatabaseID(this.database.id);
451
+ }
452
+ /**
453
+ * Get iterator for all groups
454
+ */
455
+ getGroupsIterator(predicate) {
456
+ return group_1.default.allIterator(this.database.id, predicate);
457
+ }
458
+ /**
459
+ * Create a Group
460
+ */
461
+ createGroup(groupID) {
462
+ return group_1.default.create(groupID, this.database.id);
463
+ }
464
+ /**
465
+ * Broadcast a network-wide channel change.
466
+ */
467
+ async changeChannel(oldChannel, newChannel, nwkUpdateID) {
468
+ logger_1.logger.warning(`Changing channel from '${oldChannel}' to '${newChannel}'`, NS);
469
+ // According to the Zigbee specification:
470
+ // When broadcasting a Mgmt_NWK_Update_req to notify devices of a new channel, the nwkUpdateId parameter should be incremented in the NIB and included in the Mgmt_NWK_Update_req.
471
+ // The valid range of nwkUpdateId is 0x00 to 0xFF, and it should wrap back to 0 if necessary.
472
+ if (++nwkUpdateID > 0xff) {
473
+ nwkUpdateID = 0x00;
474
+ }
475
+ const clusterId = Zdo.ClusterId.NWK_UPDATE_REQUEST;
476
+ const zdoPayload = Zdo.Buffalo.buildRequest(this.adapter.hasZdoMessageOverhead, clusterId, [newChannel], 0xfe, undefined, nwkUpdateID, undefined);
477
+ await this.adapter.sendZdo(ZSpec.BLANK_EUI64, ZSpec.BroadcastAddress.SLEEPY, clusterId, zdoPayload, true);
478
+ logger_1.logger.info(`Channel changed to '${newChannel}'`, NS);
479
+ this.networkParametersCached = undefined; // invalidate cache
480
+ // wait for the broadcast to propagate and the adapter to actually change
481
+ // NOTE: observed to ~9sec on `ember` with actual stack event
482
+ await (0, utils_1.wait)(12000);
483
+ }
484
+ async identifyUnknownDevice(nwkAddress) {
485
+ if (this.unknownDevices.has(nwkAddress)) {
486
+ // prevent duplicate triggering
487
+ return;
488
+ }
489
+ logger_1.logger.debug(`Trying to identify unknown device with address '${nwkAddress}'`, NS);
490
+ this.unknownDevices.add(nwkAddress);
491
+ const clusterId = Zdo.ClusterId.IEEE_ADDRESS_REQUEST;
492
+ const zdoPayload = Zdo.Buffalo.buildRequest(this.adapter.hasZdoMessageOverhead, clusterId, nwkAddress, false, 0);
493
+ try {
494
+ const response = await this.adapter.sendZdo(ZSpec.BLANK_EUI64, nwkAddress, clusterId, zdoPayload, false);
495
+ if (Zdo.Buffalo.checkStatus(response)) {
496
+ const payload = response[1];
497
+ const device = model_1.Device.byIeeeAddr(this.database.id, payload.eui64);
498
+ if (device) {
499
+ this.checkDeviceNetworkAddress(device, payload.eui64, payload.nwkAddress);
500
+ this.unknownDevices.delete(payload.nwkAddress);
501
+ }
502
+ return device;
503
+ }
504
+ throw new Zdo.StatusError(response[0]);
505
+ }
506
+ catch (error) {
507
+ // Catches 2 types of exception: Zdo.StatusError and no response from `adapter.sendZdo()`.
508
+ logger_1.logger.debug(`Failed to retrieve IEEE address for device '${nwkAddress}': ${error}`, NS);
509
+ }
510
+ // NOTE: by keeping nwkAddress in `this.unknownDevices` on fail, it prevents a non-responding device from potentially spamming identify.
511
+ // This only lasts until next reboot (runtime Set), allowing to 'force' another trigger if necessary.
512
+ }
513
+ checkDeviceNetworkAddress(device, ieeeAddress, nwkAddress) {
514
+ if (device.networkAddress !== nwkAddress) {
515
+ logger_1.logger.debug(`Device '${ieeeAddress}' got new networkAddress '${nwkAddress}'`, NS);
516
+ device.networkAddress = nwkAddress;
517
+ device.save();
518
+ this.selfAndDeviceEmit(device, "deviceNetworkAddressChanged", { device });
519
+ }
520
+ }
521
+ onNetworkAddress(payload) {
522
+ logger_1.logger.debug(`Network address from '${payload.eui64}:${payload.nwkAddress}'`, NS);
523
+ const device = model_1.Device.byIeeeAddr(this.database.id, payload.eui64);
524
+ if (!device) {
525
+ logger_1.logger.debug(`Network address is from unknown device '${payload.eui64}:${payload.nwkAddress}'`, NS);
526
+ return;
527
+ }
528
+ device.updateLastSeen();
529
+ this.selfAndDeviceEmit(device, "lastSeenChanged", { device, reason: "networkAddress" });
530
+ this.checkDeviceNetworkAddress(device, payload.eui64, payload.nwkAddress);
531
+ }
532
+ onIEEEAddress(payload) {
533
+ logger_1.logger.debug(`IEEE address from '${payload.eui64}:${payload.nwkAddress}'`, NS);
534
+ const device = model_1.Device.byIeeeAddr(this.database.id, payload.eui64);
535
+ if (!device) {
536
+ logger_1.logger.debug(`IEEE address is from unknown device '${payload.eui64}:${payload.nwkAddress}'`, NS);
537
+ return;
538
+ }
539
+ device.updateLastSeen();
540
+ this.selfAndDeviceEmit(device, "lastSeenChanged", { device, reason: "networkAddress" });
541
+ this.checkDeviceNetworkAddress(device, payload.eui64, payload.nwkAddress);
542
+ }
543
+ onDeviceAnnounce(payload) {
544
+ logger_1.logger.debug(`Device announce from '${payload.eui64}:${payload.nwkAddress}'`, NS);
545
+ const device = model_1.Device.byIeeeAddr(this.database.id, payload.eui64);
546
+ if (!device) {
547
+ logger_1.logger.debug(`Device announce is from unknown device '${payload.eui64}:${payload.nwkAddress}'`, NS);
548
+ return;
549
+ }
550
+ device.updateLastSeen();
551
+ this.selfAndDeviceEmit(device, "lastSeenChanged", { device, reason: "deviceAnnounce" });
552
+ device.implicitCheckin();
553
+ this.checkDeviceNetworkAddress(device, payload.eui64, payload.nwkAddress);
554
+ this.selfAndDeviceEmit(device, "deviceAnnounce", { device });
555
+ }
556
+ onDeviceLeave(payload) {
557
+ logger_1.logger.debug(`Device leave '${payload.ieeeAddr}'`, NS);
558
+ // XXX: seems type is not properly detected?
559
+ // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
560
+ const device = payload.ieeeAddr ? model_1.Device.byIeeeAddr(this.database.id, payload.ieeeAddr) : model_1.Device.byNetworkAddress(this.database.id, payload.networkAddress);
561
+ if (!device) {
562
+ logger_1.logger.debug(`Device leave is from unknown or already deleted device '${payload.ieeeAddr ?? payload.networkAddress}'`, NS);
563
+ return;
564
+ }
565
+ logger_1.logger.debug(`Removing device from database '${device.ieeeAddr}'`, NS);
566
+ device.removeFromDatabase();
567
+ this.selfAndDeviceEmit(device, "deviceLeave", { ieeeAddr: device.ieeeAddr });
568
+ }
569
+ async onAdapterDisconnected() {
570
+ logger_1.logger.debug("Adapter disconnected", NS);
571
+ this.adapterDisconnected = true;
572
+ try {
573
+ await this.adapter.stop();
574
+ }
575
+ catch (error) {
576
+ logger_1.logger.error(`Failed to stop adapter on disconnect: ${error}`, NS);
577
+ }
578
+ this.emit("adapterDisconnected");
579
+ }
580
+ onDeviceJoinedGreenPower(payload) {
581
+ logger_1.logger.debug(() => `Green power device '${JSON.stringify(payload).replaceAll(/\[[\d,]+\]/g, "HIDDEN")}' joined`, NS);
582
+ // Green power devices don't have an ieeeAddr, the sourceID is unique and static so use this.
583
+ const ieeeAddr = greenPower_1.default.sourceIdToIeeeAddress(payload.sourceID);
584
+ // Green power devices dont' have a modelID, create a modelID based on the deviceID (=type)
585
+ const modelID = `GreenPower_${payload.deviceID}`;
586
+ let device = model_1.Device.byIeeeAddr(this.database.id, ieeeAddr, true);
587
+ if (!device) {
588
+ logger_1.logger.debug(`New green power device '${ieeeAddr}' joined`, NS);
589
+ logger_1.logger.debug(`Creating device '${ieeeAddr}'`, NS);
590
+ device = model_1.Device.create("GreenPower", ieeeAddr, payload.networkAddress, undefined, undefined, undefined, modelID, device_1.InterviewState.Successful, payload.securityKey ? Array.from(payload.securityKey) : /* v8 ignore next */ undefined, this.database.id);
591
+ device.save();
592
+ this.selfAndDeviceEmit(device, "deviceJoined", { device });
593
+ this.selfAndDeviceEmit(device, "deviceInterview", { status: "successful", device });
594
+ }
595
+ else if (device.isDeleted) {
596
+ logger_1.logger.debug(`Deleted green power device '${ieeeAddr}' joined, undeleting`, NS);
597
+ device.undelete();
598
+ this.selfAndDeviceEmit(device, "deviceJoined", { device });
599
+ this.selfAndDeviceEmit(device, "deviceInterview", { status: "successful", device });
600
+ }
601
+ }
602
+ onDeviceLeaveGreenPower(sourceID) {
603
+ logger_1.logger.debug(`Green power device '${sourceID}' left`, NS);
604
+ // Green power devices don't have an ieeeAddr, the sourceID is unique and static so use this.
605
+ const ieeeAddr = greenPower_1.default.sourceIdToIeeeAddress(sourceID);
606
+ const device = model_1.Device.byIeeeAddr(this.database.id, ieeeAddr);
607
+ if (!device) {
608
+ logger_1.logger.debug(`Green power device leave is from unknown or already deleted device '${ieeeAddr}'`, NS);
609
+ return;
610
+ }
611
+ logger_1.logger.debug(`Removing green power device from database '${device.ieeeAddr}'`, NS);
612
+ device.removeFromDatabase();
613
+ this.selfAndDeviceEmit(device, "deviceLeave", { ieeeAddr: device.ieeeAddr });
614
+ }
615
+ selfAndDeviceEmit(device, event, ...args) {
616
+ device.emit(event, ...args);
617
+ this.emit(event, ...args);
618
+ }
619
+ async onDeviceJoined(payload) {
620
+ logger_1.logger.debug(`Device '${payload.ieeeAddr}' joined`, NS);
621
+ if (this.options.acceptJoiningDeviceHandler) {
622
+ if (!(await this.options.acceptJoiningDeviceHandler(payload.ieeeAddr))) {
623
+ logger_1.logger.debug(`Device '${payload.ieeeAddr}' rejected by handler, removing it`, NS);
624
+ // XXX: GP devices? see Device.removeFromNetwork
625
+ try {
626
+ const clusterId = Zdo.ClusterId.LEAVE_REQUEST;
627
+ const zdoPayload = Zdo.Buffalo.buildRequest(this.adapter.hasZdoMessageOverhead, clusterId, payload.ieeeAddr, Zdo.LeaveRequestFlags.WITHOUT_REJOIN);
628
+ const response = await this.adapter.sendZdo(payload.ieeeAddr, payload.networkAddress, clusterId, zdoPayload, false);
629
+ if (!Zdo.Buffalo.checkStatus(response)) {
630
+ throw new Zdo.StatusError(response[0]);
631
+ }
632
+ }
633
+ catch (error) {
634
+ logger_1.logger.error(`Failed to remove rejected device: ${error.message}`, NS);
635
+ }
636
+ return;
637
+ }
638
+ logger_1.logger.debug(`Device '${payload.ieeeAddr}' accepted by handler`, NS);
639
+ }
640
+ let device = model_1.Device.byIeeeAddr(this.database.id, payload.ieeeAddr, true);
641
+ if (!device) {
642
+ logger_1.logger.debug(`New device '${payload.ieeeAddr}' joined`, NS);
643
+ logger_1.logger.debug(`Creating device '${payload.ieeeAddr}'`, NS);
644
+ device = model_1.Device.create("Unknown", payload.ieeeAddr, payload.networkAddress, undefined, undefined, undefined, undefined, device_1.InterviewState.Pending, undefined, this.database.id);
645
+ this.selfAndDeviceEmit(device, "deviceJoined", { device });
646
+ }
647
+ else if (device.isDeleted) {
648
+ logger_1.logger.debug(`Deleted device '${payload.ieeeAddr}' joined, undeleting`, NS);
649
+ device.undelete();
650
+ this.selfAndDeviceEmit(device, "deviceJoined", { device });
651
+ }
652
+ if (device.networkAddress !== payload.networkAddress) {
653
+ logger_1.logger.debug(`Device '${payload.ieeeAddr}' is already in database with different network address, updating network address`, NS);
654
+ device.networkAddress = payload.networkAddress;
655
+ device.save();
656
+ }
657
+ device.updateLastSeen();
658
+ this.selfAndDeviceEmit(device, "lastSeenChanged", { device, reason: "deviceJoined" });
659
+ device.implicitCheckin();
660
+ if (device.interviewState === device_1.InterviewState.Pending || device.interviewState === device_1.InterviewState.Failed) {
661
+ logger_1.logger.info(`Interview for '${device.ieeeAddr}' started`, NS);
662
+ this.selfAndDeviceEmit(device, "deviceInterview", { status: "started", device });
663
+ try {
664
+ await device.interview();
665
+ logger_1.logger.info(`Succesfully interviewed '${device.ieeeAddr}'`, NS);
666
+ this.selfAndDeviceEmit(device, "deviceInterview", { status: "successful", device });
667
+ }
668
+ catch (error) {
669
+ logger_1.logger.error(`Interview failed for '${device.ieeeAddr} with error '${error}'`, NS);
670
+ this.selfAndDeviceEmit(device, "deviceInterview", { status: "failed", device });
671
+ }
672
+ }
673
+ else {
674
+ logger_1.logger.debug(`Not interviewing '${payload.ieeeAddr}', interviewState=${device.interviewState}'`, NS);
675
+ }
676
+ }
677
+ onZdoResponse(clusterId, response) {
678
+ logger_1.logger.debug(`Received ZDO response: clusterId=${Zdo.ClusterId[clusterId]}, status=${Zdo.Status[response[0]]}, payload=${JSON.stringify(response[1])}`, NS);
679
+ switch (clusterId) {
680
+ case Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE: {
681
+ if (Zdo.Buffalo.checkStatus(response)) {
682
+ this.onNetworkAddress(response[1]);
683
+ }
684
+ break;
685
+ }
686
+ case Zdo.ClusterId.IEEE_ADDRESS_RESPONSE: {
687
+ if (Zdo.Buffalo.checkStatus(response)) {
688
+ this.onIEEEAddress(response[1]);
689
+ }
690
+ break;
691
+ }
692
+ case Zdo.ClusterId.END_DEVICE_ANNOUNCE: {
693
+ if (Zdo.Buffalo.checkStatus(response)) {
694
+ this.onDeviceAnnounce(response[1]);
695
+ }
696
+ break;
697
+ }
698
+ }
699
+ }
700
+ async onZclPayload(payload) {
701
+ let frame;
702
+ let device;
703
+ if (payload.clusterID === Zcl.Clusters.touchlink.ID) {
704
+ // This is handled by touchlink
705
+ return;
706
+ }
707
+ if (payload.clusterID === Zcl.Clusters.greenPower.ID) {
708
+ try {
709
+ // Custom clusters are not supported for Green Power since we need to parse the frame to get the device.
710
+ frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
711
+ }
712
+ catch (error) {
713
+ logger_1.logger.debug(`Failed to parse frame green power frame, ignoring it: ${error}`, NS);
714
+ return;
715
+ }
716
+ if (frame.payload.commandID === undefined) {
717
+ // can be from gpd or gpp
718
+ // can be:
719
+ // - greenPower.commandsResponse.commissioningMode
720
+ // - Foundation.defaultRsp for greenPower.commandsResponse.pairing with status INVALID_FIELD or INSUFFICIENT_SPACE
721
+ // - ...
722
+ device = model_1.Device.find(this.database.id, payload.address);
723
+ }
724
+ else {
725
+ if (frame.payload.srcID === undefined) {
726
+ logger_1.logger.debug("Data is from unsupported green power device with IEEE addressing, skipping...", NS);
727
+ return;
728
+ }
729
+ const ieeeAddr = greenPower_1.default.sourceIdToIeeeAddress(frame.payload.srcID);
730
+ device = model_1.Device.byIeeeAddr(this.database.id, ieeeAddr);
731
+ frame = await this.#greenPower.processCommand(payload, frame, device?.gpSecurityKey ? Buffer.from(device.gpSecurityKey) : undefined);
732
+ // lookup encapsulated gpDevice for further processing (re-fetch, may have been created by above call)
733
+ device = model_1.Device.byIeeeAddr(this.database.id, ieeeAddr);
734
+ if (!device) {
735
+ logger_1.logger.debug(`Data is from unknown green power device with address '${ieeeAddr}' (${frame.payload.srcID}), skipping...`, NS);
736
+ return;
737
+ }
738
+ }
739
+ }
740
+ else {
741
+ /**
742
+ * Handling of re-transmitted Xiaomi messages.
743
+ * https://github.com/Koenkk/zigbee2mqtt/issues/1238
744
+ * https://github.com/Koenkk/zigbee2mqtt/issues/3592
745
+ *
746
+ * Some Xiaomi router devices re-transmit messages from Xiaomi end devices.
747
+ * The network address of these message is set to the one of the Xiaomi router.
748
+ * Therefore it looks like if the message came from the Xiaomi router, while in
749
+ * fact it came from the end device.
750
+ * Handling these message would result in false state updates.
751
+ * The group ID attribute of these message defines the network address of the end device.
752
+ */
753
+ device = model_1.Device.find(this.database.id, payload.address);
754
+ if (device?.manufacturerName === "LUMI" && device?.type === "Router" && payload.groupID) {
755
+ logger_1.logger.debug(`Handling re-transmitted Xiaomi message ${device.networkAddress} -> ${payload.groupID}`, NS);
756
+ device = model_1.Device.byNetworkAddress(this.database.id, payload.groupID);
757
+ }
758
+ try {
759
+ frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, device ? device.customClusters : {});
760
+ }
761
+ catch (error) {
762
+ logger_1.logger.debug(`Failed to parse frame: ${error}`, NS);
763
+ }
764
+ }
765
+ if (!device) {
766
+ if (typeof payload.address === "number" && !model_1.Device.isDeletedByNetworkAddress(this.database.id, payload.address)) {
767
+ device = await this.identifyUnknownDevice(payload.address);
768
+ }
769
+ if (!device) {
770
+ logger_1.logger.debug(`Data is from unknown device with address '${payload.address}', skipping...`, NS);
771
+ return;
772
+ }
773
+ }
774
+ logger_1.logger.debug(`Received payload: clusterID=${payload.clusterID}, address=${payload.address}, groupID=${payload.groupID}, ` +
775
+ `endpoint=${payload.endpoint}, destinationEndpoint=${payload.destinationEndpoint}, wasBroadcast=${payload.wasBroadcast}, ` +
776
+ `linkQuality=${payload.linkquality}, frame=${frame?.toString()}`, NS);
777
+ device.updateLastSeen();
778
+ //no implicit checkin for genPollCtrl data because it might interfere with the explicit checkin
779
+ if (frame?.cluster.name !== "genPollCtrl") {
780
+ device.implicitCheckin();
781
+ }
782
+ device.linkquality = payload.linkquality;
783
+ let endpoint = device.getEndpoint(payload.endpoint);
784
+ if (!endpoint) {
785
+ logger_1.logger.debug(`Data is from unknown endpoint '${payload.endpoint}' from device with network address '${payload.address}', creating it...`, NS);
786
+ endpoint = device.createEndpoint(payload.endpoint);
787
+ }
788
+ // Parse command for event
789
+ let type;
790
+ let data = {};
791
+ let clusterName;
792
+ const meta = { rawData: payload.data };
793
+ if (frame) {
794
+ const command = frame.command;
795
+ clusterName = frame.cluster.name;
796
+ meta.zclTransactionSequenceNumber = frame.header.transactionSequenceNumber;
797
+ meta.manufacturerCode = frame.header.manufacturerCode;
798
+ meta.frameControl = frame.header.frameControl;
799
+ if (frame.header.isGlobal) {
800
+ switch (frame.command.name) {
801
+ case "report": {
802
+ type = "attributeReport";
803
+ data = helpers_1.ZclFrameConverter.attributeKeyValue(frame, device.manufacturerID, device.customClusters);
804
+ break;
805
+ }
806
+ case "read": {
807
+ type = "read";
808
+ data = helpers_1.ZclFrameConverter.attributeList(frame, device.manufacturerID, device.customClusters);
809
+ break;
810
+ }
811
+ case "write": {
812
+ type = "write";
813
+ data = helpers_1.ZclFrameConverter.attributeKeyValue(frame, device.manufacturerID, device.customClusters);
814
+ break;
815
+ }
816
+ case "readRsp": {
817
+ type = "readResponse";
818
+ data = helpers_1.ZclFrameConverter.attributeKeyValue(frame, device.manufacturerID, device.customClusters);
819
+ break;
820
+ }
821
+ case "defaultRsp": {
822
+ if (frame.payload.statusCode !== Zcl.Status.SUCCESS) {
823
+ logger_1.logger.debug(`Failure default response from '${payload.address}': clusterID=${payload.clusterID} cmdId=${frame.payload.cmdId} status=${Zcl.Status[frame.payload.statusCode]}`, NS);
824
+ }
825
+ break;
826
+ }
827
+ }
828
+ if (type === "readResponse" || type === "attributeReport") {
829
+ // devices report attributes through readRsp or attributeReport
830
+ if (frame.cluster.name === "genBasic") {
831
+ device.updateGenBasic(data);
832
+ }
833
+ endpoint.saveClusterAttributeKeyValue(frame.cluster.ID, data);
834
+ }
835
+ }
836
+ else {
837
+ if (frame.header.isSpecific) {
838
+ type = `command${command.name.charAt(0).toUpperCase()}${command.name.slice(1)}`;
839
+ data = frame.payload;
840
+ }
841
+ }
842
+ }
843
+ else {
844
+ type = "raw";
845
+ data = payload.data;
846
+ const name = Zcl.Utils.getCluster(payload.clusterID, device.manufacturerID, device.customClusters).name;
847
+ clusterName = Number.isNaN(Number(name)) ? name : Number(name);
848
+ }
849
+ if (type && data) {
850
+ const linkquality = payload.linkquality;
851
+ const groupID = payload.groupID;
852
+ this.selfAndDeviceEmit(device, "message", {
853
+ type,
854
+ device,
855
+ endpoint,
856
+ data,
857
+ linkquality,
858
+ groupID,
859
+ cluster: clusterName,
860
+ meta,
861
+ });
862
+ this.selfAndDeviceEmit(device, "lastSeenChanged", { device, reason: "messageEmitted" });
863
+ }
864
+ else {
865
+ this.selfAndDeviceEmit(device, "lastSeenChanged", { device, reason: "messageNonEmitted" });
866
+ }
867
+ if (frame) {
868
+ await device.onZclData(payload, frame, endpoint);
869
+ }
870
+ }
871
+ }
872
+ exports.Controller = Controller;
873
+ exports.default = Controller;
874
+ //# sourceMappingURL=controller.js.map