zigbee-herdsman 6.0.2 → 6.0.3

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 (252) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/package.json +12 -3
  3. package/.github/ISSUE_TEMPLATE/config.yml +0 -5
  4. package/.github/dependabot.yml +0 -22
  5. package/.github/workflows/ci.yml +0 -69
  6. package/.github/workflows/release-please.yml +0 -18
  7. package/.github/workflows/stale.yml +0 -20
  8. package/.github/workflows/typedoc.yaml +0 -47
  9. package/.release-please-manifest.json +0 -3
  10. package/.vscode/extensions.json +0 -3
  11. package/.vscode/settings.json +0 -11
  12. package/biome.json +0 -98
  13. package/examples/join-and-log.js +0 -24
  14. package/release-please-config.json +0 -9
  15. package/src/adapter/adapter.ts +0 -189
  16. package/src/adapter/adapterDiscovery.ts +0 -666
  17. package/src/adapter/const.ts +0 -12
  18. package/src/adapter/deconz/adapter/deconzAdapter.ts +0 -877
  19. package/src/adapter/deconz/driver/constants.ts +0 -246
  20. package/src/adapter/deconz/driver/driver.ts +0 -1540
  21. package/src/adapter/deconz/driver/frame.ts +0 -11
  22. package/src/adapter/deconz/driver/frameParser.ts +0 -753
  23. package/src/adapter/deconz/driver/parser.ts +0 -45
  24. package/src/adapter/deconz/driver/writer.ts +0 -22
  25. package/src/adapter/deconz/types.d.ts +0 -13
  26. package/src/adapter/ember/adapter/emberAdapter.ts +0 -2265
  27. package/src/adapter/ember/adapter/endpoints.ts +0 -86
  28. package/src/adapter/ember/adapter/oneWaitress.ts +0 -324
  29. package/src/adapter/ember/adapter/tokensManager.ts +0 -782
  30. package/src/adapter/ember/consts.ts +0 -178
  31. package/src/adapter/ember/enums.ts +0 -1746
  32. package/src/adapter/ember/ezsp/buffalo.ts +0 -1392
  33. package/src/adapter/ember/ezsp/consts.ts +0 -148
  34. package/src/adapter/ember/ezsp/enums.ts +0 -1114
  35. package/src/adapter/ember/ezsp/ezsp.ts +0 -9061
  36. package/src/adapter/ember/ezspError.ts +0 -10
  37. package/src/adapter/ember/types.ts +0 -866
  38. package/src/adapter/ember/uart/ash.ts +0 -1960
  39. package/src/adapter/ember/uart/consts.ts +0 -109
  40. package/src/adapter/ember/uart/enums.ts +0 -192
  41. package/src/adapter/ember/uart/parser.ts +0 -48
  42. package/src/adapter/ember/uart/queues.ts +0 -247
  43. package/src/adapter/ember/uart/writer.ts +0 -53
  44. package/src/adapter/ember/utils/initters.ts +0 -58
  45. package/src/adapter/ember/utils/math.ts +0 -73
  46. package/src/adapter/events.ts +0 -21
  47. package/src/adapter/ezsp/adapter/backup.ts +0 -109
  48. package/src/adapter/ezsp/adapter/ezspAdapter.ts +0 -614
  49. package/src/adapter/ezsp/driver/commands.ts +0 -2497
  50. package/src/adapter/ezsp/driver/consts.ts +0 -11
  51. package/src/adapter/ezsp/driver/driver.ts +0 -1002
  52. package/src/adapter/ezsp/driver/ezsp.ts +0 -802
  53. package/src/adapter/ezsp/driver/frame.ts +0 -101
  54. package/src/adapter/ezsp/driver/index.ts +0 -4
  55. package/src/adapter/ezsp/driver/multicast.ts +0 -78
  56. package/src/adapter/ezsp/driver/parser.ts +0 -81
  57. package/src/adapter/ezsp/driver/types/basic.ts +0 -201
  58. package/src/adapter/ezsp/driver/types/index.ts +0 -239
  59. package/src/adapter/ezsp/driver/types/named.ts +0 -2330
  60. package/src/adapter/ezsp/driver/types/struct.ts +0 -844
  61. package/src/adapter/ezsp/driver/uart.ts +0 -460
  62. package/src/adapter/ezsp/driver/utils/crc16ccitt.ts +0 -44
  63. package/src/adapter/ezsp/driver/utils/index.ts +0 -32
  64. package/src/adapter/ezsp/driver/writer.ts +0 -64
  65. package/src/adapter/index.ts +0 -3
  66. package/src/adapter/serialPort.ts +0 -58
  67. package/src/adapter/socketPortUtils.ts +0 -16
  68. package/src/adapter/tstype.ts +0 -78
  69. package/src/adapter/z-stack/adapter/adapter-backup.ts +0 -519
  70. package/src/adapter/z-stack/adapter/adapter-nv-memory.ts +0 -457
  71. package/src/adapter/z-stack/adapter/endpoints.ts +0 -57
  72. package/src/adapter/z-stack/adapter/manager.ts +0 -543
  73. package/src/adapter/z-stack/adapter/tstype.ts +0 -6
  74. package/src/adapter/z-stack/adapter/zStackAdapter.ts +0 -1190
  75. package/src/adapter/z-stack/constants/af.ts +0 -27
  76. package/src/adapter/z-stack/constants/common.ts +0 -285
  77. package/src/adapter/z-stack/constants/dbg.ts +0 -23
  78. package/src/adapter/z-stack/constants/index.ts +0 -11
  79. package/src/adapter/z-stack/constants/mac.ts +0 -128
  80. package/src/adapter/z-stack/constants/sapi.ts +0 -25
  81. package/src/adapter/z-stack/constants/sys.ts +0 -72
  82. package/src/adapter/z-stack/constants/util.ts +0 -82
  83. package/src/adapter/z-stack/constants/utils.ts +0 -14
  84. package/src/adapter/z-stack/constants/zdo.ts +0 -103
  85. package/src/adapter/z-stack/models/startup-options.ts +0 -13
  86. package/src/adapter/z-stack/structs/entries/address-manager-entry.ts +0 -44
  87. package/src/adapter/z-stack/structs/entries/address-manager-table.ts +0 -19
  88. package/src/adapter/z-stack/structs/entries/aps-link-key-data-entry.ts +0 -12
  89. package/src/adapter/z-stack/structs/entries/aps-link-key-data-table.ts +0 -21
  90. package/src/adapter/z-stack/structs/entries/aps-tc-link-key-entry.ts +0 -19
  91. package/src/adapter/z-stack/structs/entries/aps-tc-link-key-table.ts +0 -21
  92. package/src/adapter/z-stack/structs/entries/channel-list.ts +0 -8
  93. package/src/adapter/z-stack/structs/entries/has-configured.ts +0 -16
  94. package/src/adapter/z-stack/structs/entries/index.ts +0 -16
  95. package/src/adapter/z-stack/structs/entries/nib.ts +0 -66
  96. package/src/adapter/z-stack/structs/entries/nwk-key-descriptor.ts +0 -15
  97. package/src/adapter/z-stack/structs/entries/nwk-key.ts +0 -13
  98. package/src/adapter/z-stack/structs/entries/nwk-pan-id.ts +0 -8
  99. package/src/adapter/z-stack/structs/entries/nwk-sec-material-descriptor-entry.ts +0 -20
  100. package/src/adapter/z-stack/structs/entries/nwk-sec-material-descriptor-table.ts +0 -19
  101. package/src/adapter/z-stack/structs/entries/security-manager-entry.ts +0 -33
  102. package/src/adapter/z-stack/structs/entries/security-manager-table.ts +0 -22
  103. package/src/adapter/z-stack/structs/index.ts +0 -4
  104. package/src/adapter/z-stack/structs/serializable-memory-object.ts +0 -14
  105. package/src/adapter/z-stack/structs/struct.ts +0 -367
  106. package/src/adapter/z-stack/structs/table.ts +0 -198
  107. package/src/adapter/z-stack/unpi/constants.ts +0 -33
  108. package/src/adapter/z-stack/unpi/frame.ts +0 -62
  109. package/src/adapter/z-stack/unpi/index.ts +0 -4
  110. package/src/adapter/z-stack/unpi/parser.ts +0 -56
  111. package/src/adapter/z-stack/unpi/writer.ts +0 -21
  112. package/src/adapter/z-stack/utils/channel-list.ts +0 -40
  113. package/src/adapter/z-stack/utils/index.ts +0 -2
  114. package/src/adapter/z-stack/utils/network-options.ts +0 -26
  115. package/src/adapter/z-stack/znp/buffaloZnp.ts +0 -175
  116. package/src/adapter/z-stack/znp/definition.ts +0 -2713
  117. package/src/adapter/z-stack/znp/index.ts +0 -2
  118. package/src/adapter/z-stack/znp/parameterType.ts +0 -22
  119. package/src/adapter/z-stack/znp/tstype.ts +0 -44
  120. package/src/adapter/z-stack/znp/utils.ts +0 -10
  121. package/src/adapter/z-stack/znp/znp.ts +0 -342
  122. package/src/adapter/z-stack/znp/zpiObject.ts +0 -148
  123. package/src/adapter/zboss/adapter/zbossAdapter.ts +0 -526
  124. package/src/adapter/zboss/commands.ts +0 -1184
  125. package/src/adapter/zboss/consts.ts +0 -9
  126. package/src/adapter/zboss/driver.ts +0 -422
  127. package/src/adapter/zboss/enums.ts +0 -360
  128. package/src/adapter/zboss/frame.ts +0 -227
  129. package/src/adapter/zboss/reader.ts +0 -65
  130. package/src/adapter/zboss/types.ts +0 -0
  131. package/src/adapter/zboss/uart.ts +0 -428
  132. package/src/adapter/zboss/utils.ts +0 -58
  133. package/src/adapter/zboss/writer.ts +0 -49
  134. package/src/adapter/zigate/adapter/patchZdoBuffaloBE.ts +0 -27
  135. package/src/adapter/zigate/adapter/zigateAdapter.ts +0 -618
  136. package/src/adapter/zigate/driver/LICENSE +0 -17
  137. package/src/adapter/zigate/driver/buffaloZiGate.ts +0 -212
  138. package/src/adapter/zigate/driver/commandType.ts +0 -418
  139. package/src/adapter/zigate/driver/constants.ts +0 -150
  140. package/src/adapter/zigate/driver/frame.ts +0 -197
  141. package/src/adapter/zigate/driver/messageType.ts +0 -287
  142. package/src/adapter/zigate/driver/parameterType.ts +0 -32
  143. package/src/adapter/zigate/driver/ziGateObject.ts +0 -146
  144. package/src/adapter/zigate/driver/zigate.ts +0 -423
  145. package/src/adapter/zoh/adapter/utils.ts +0 -27
  146. package/src/adapter/zoh/adapter/zohAdapter.ts +0 -838
  147. package/src/buffalo/buffalo.ts +0 -342
  148. package/src/buffalo/index.ts +0 -1
  149. package/src/controller/controller.ts +0 -1022
  150. package/src/controller/database.ts +0 -124
  151. package/src/controller/events.ts +0 -52
  152. package/src/controller/greenPower.ts +0 -603
  153. package/src/controller/helpers/index.ts +0 -1
  154. package/src/controller/helpers/installCodes.ts +0 -107
  155. package/src/controller/helpers/request.ts +0 -96
  156. package/src/controller/helpers/requestQueue.ts +0 -125
  157. package/src/controller/helpers/zclFrameConverter.ts +0 -47
  158. package/src/controller/helpers/zclTransactionSequenceNumber.ts +0 -19
  159. package/src/controller/index.ts +0 -6
  160. package/src/controller/model/device.ts +0 -1249
  161. package/src/controller/model/endpoint.ts +0 -1105
  162. package/src/controller/model/entity.ts +0 -23
  163. package/src/controller/model/group.ts +0 -424
  164. package/src/controller/model/index.ts +0 -5
  165. package/src/controller/model/zigbeeEntity.ts +0 -30
  166. package/src/controller/touchlink.ts +0 -189
  167. package/src/controller/tstype.ts +0 -274
  168. package/src/index.ts +0 -12
  169. package/src/models/backup-storage-legacy.ts +0 -48
  170. package/src/models/backup-storage-unified.ts +0 -47
  171. package/src/models/backup.ts +0 -37
  172. package/src/models/index.ts +0 -5
  173. package/src/models/network-options.ts +0 -11
  174. package/src/utils/backup.ts +0 -152
  175. package/src/utils/index.ts +0 -5
  176. package/src/utils/logger.ts +0 -20
  177. package/src/utils/patchBigIntSerialization.ts +0 -8
  178. package/src/utils/queue.ts +0 -76
  179. package/src/utils/types.d.ts +0 -3
  180. package/src/utils/utils.ts +0 -19
  181. package/src/utils/wait.ts +0 -5
  182. package/src/utils/waitress.ts +0 -96
  183. package/src/zspec/consts.ts +0 -84
  184. package/src/zspec/enums.ts +0 -22
  185. package/src/zspec/index.ts +0 -3
  186. package/src/zspec/tstypes.ts +0 -18
  187. package/src/zspec/utils.ts +0 -247
  188. package/src/zspec/zcl/buffaloZcl.ts +0 -1220
  189. package/src/zspec/zcl/definition/cluster.ts +0 -5915
  190. package/src/zspec/zcl/definition/clusters-typegen.ts +0 -588
  191. package/src/zspec/zcl/definition/clusters-types.ts +0 -7331
  192. package/src/zspec/zcl/definition/consts.ts +0 -24
  193. package/src/zspec/zcl/definition/enums.ts +0 -203
  194. package/src/zspec/zcl/definition/foundation.ts +0 -329
  195. package/src/zspec/zcl/definition/manufacturerCode.ts +0 -729
  196. package/src/zspec/zcl/definition/status.ts +0 -69
  197. package/src/zspec/zcl/definition/tstype.ts +0 -377
  198. package/src/zspec/zcl/index.ts +0 -11
  199. package/src/zspec/zcl/utils.ts +0 -321
  200. package/src/zspec/zcl/zclFrame.ts +0 -356
  201. package/src/zspec/zcl/zclHeader.ts +0 -102
  202. package/src/zspec/zcl/zclStatusError.ts +0 -10
  203. package/src/zspec/zdo/buffaloZdo.ts +0 -2336
  204. package/src/zspec/zdo/definition/clusters.ts +0 -722
  205. package/src/zspec/zdo/definition/consts.ts +0 -16
  206. package/src/zspec/zdo/definition/enums.ts +0 -99
  207. package/src/zspec/zdo/definition/status.ts +0 -105
  208. package/src/zspec/zdo/definition/tstypes.ts +0 -1062
  209. package/src/zspec/zdo/index.ts +0 -7
  210. package/src/zspec/zdo/utils.ts +0 -76
  211. package/src/zspec/zdo/zdoStatusError.ts +0 -10
  212. package/test/adapter/adapter.test.ts +0 -1062
  213. package/test/adapter/ember/ash.test.ts +0 -337
  214. package/test/adapter/ember/consts.ts +0 -131
  215. package/test/adapter/ember/emberAdapter.test.ts +0 -3449
  216. package/test/adapter/ember/ezsp.test.ts +0 -385
  217. package/test/adapter/ember/ezspBuffalo.test.ts +0 -93
  218. package/test/adapter/ember/ezspError.test.ts +0 -12
  219. package/test/adapter/ember/math.test.ts +0 -206
  220. package/test/adapter/ezsp/frame.test.ts +0 -30
  221. package/test/adapter/ezsp/uart.test.ts +0 -181
  222. package/test/adapter/z-stack/adapter.test.ts +0 -3984
  223. package/test/adapter/z-stack/constants.test.ts +0 -33
  224. package/test/adapter/z-stack/structs.test.ts +0 -115
  225. package/test/adapter/z-stack/unpi.test.ts +0 -213
  226. package/test/adapter/z-stack/znp.test.ts +0 -1284
  227. package/test/adapter/zboss/fixZdoResponse.test.ts +0 -179
  228. package/test/adapter/zigate/patchZdoBuffaloBE.test.ts +0 -81
  229. package/test/adapter/zigate/zdo.test.ts +0 -187
  230. package/test/adapter/zoh/utils.test.ts +0 -36
  231. package/test/adapter/zoh/zohAdapter.test.ts +0 -1307
  232. package/test/benchOptions.ts +0 -14
  233. package/test/buffalo.test.ts +0 -431
  234. package/test/controller.bench.ts +0 -214
  235. package/test/controller.test.ts +0 -8702
  236. package/test/greenpower.test.ts +0 -1408
  237. package/test/mockAdapters.ts +0 -65
  238. package/test/mockDevices.ts +0 -598
  239. package/test/requests.bench.ts +0 -229
  240. package/test/testUtils.ts +0 -20
  241. package/test/tsconfig.json +0 -9
  242. package/test/utils/math.ts +0 -19
  243. package/test/utils.test.ts +0 -279
  244. package/test/vitest.config.mts +0 -26
  245. package/test/zcl.test.ts +0 -2831
  246. package/test/zspec/utils.test.ts +0 -68
  247. package/test/zspec/zcl/buffalo.test.ts +0 -1374
  248. package/test/zspec/zcl/frame.test.ts +0 -960
  249. package/test/zspec/zcl/utils.test.ts +0 -273
  250. package/test/zspec/zdo/buffalo.test.ts +0 -1850
  251. package/test/zspec/zdo/utils.test.ts +0 -241
  252. package/tsconfig.json +0 -24
@@ -1,1022 +0,0 @@
1
- import assert from "node:assert";
2
- import events from "node:events";
3
- import fs from "node:fs";
4
- import mixinDeep from "mixin-deep";
5
- import {Adapter, type Events as AdapterEvents, type TsType as AdapterTsType} from "../adapter";
6
- import {BackupUtils, wait} from "../utils";
7
- import {logger} from "../utils/logger";
8
- import {isNumberArrayOfLength} from "../utils/utils";
9
- import * as ZSpec from "../zspec";
10
- import type {Eui64} from "../zspec/tstypes";
11
- import * as Zcl from "../zspec/zcl";
12
- import type {TPartialClusterAttributes} from "../zspec/zcl/definition/clusters-types";
13
- import type {FrameControl} from "../zspec/zcl/definition/tstype";
14
- import * as Zdo from "../zspec/zdo";
15
- import type * as ZdoTypes from "../zspec/zdo/definition/tstypes";
16
- import Database from "./database";
17
- import type * as Events from "./events";
18
- import GreenPower from "./greenPower";
19
- import {ZclFrameConverter} from "./helpers";
20
- import {checkInstallCode, parseInstallCode} from "./helpers/installCodes";
21
- import {Device, Entity} from "./model";
22
- import {InterviewState} from "./model/device";
23
- import Group from "./model/group";
24
- import Touchlink from "./touchlink";
25
- import type {DeviceType, GreenPowerDeviceJoinedPayload} from "./tstype";
26
-
27
- const NS = "zh:controller";
28
-
29
- interface Options {
30
- network: AdapterTsType.NetworkOptions;
31
- serialPort: AdapterTsType.SerialPortOptions;
32
- databasePath: string;
33
- databaseBackupPath: string;
34
- backupPath: string;
35
- adapter: AdapterTsType.AdapterOptions;
36
- /**
37
- * This lambda can be used by an application to explictly reject or accept an incoming device.
38
- * When false is returned zigbee-herdsman will not start the interview process and immidiately
39
- * try to remove the device from the network.
40
- */
41
- acceptJoiningDeviceHandler: (ieeeAddr: string) => Promise<boolean>;
42
- }
43
-
44
- const DefaultOptions: Pick<Options, "network" | "serialPort" | "adapter"> = {
45
- network: {
46
- networkKeyDistribute: false,
47
- networkKey: [0x01, 0x03, 0x05, 0x07, 0x09, 0x0b, 0x0d, 0x0f, 0x00, 0x02, 0x04, 0x06, 0x08, 0x0a, 0x0c, 0x0d],
48
- panID: 0x1a62,
49
- extendedPanID: [0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd],
50
- channelList: [11],
51
- },
52
- serialPort: {},
53
- adapter: {disableLED: false},
54
- };
55
-
56
- export interface ControllerEventMap {
57
- message: [data: Events.MessagePayload];
58
- adapterDisconnected: [];
59
- deviceJoined: [data: Events.DeviceJoinedPayload];
60
- deviceInterview: [data: Events.DeviceInterviewPayload];
61
- deviceAnnounce: [data: Events.DeviceAnnouncePayload];
62
- deviceNetworkAddressChanged: [data: Events.DeviceNetworkAddressChangedPayload];
63
- deviceLeave: [data: Events.DeviceLeavePayload];
64
- permitJoinChanged: [data: Events.PermitJoinChangedPayload];
65
- lastSeenChanged: [data: Events.LastSeenChangedPayload];
66
- }
67
-
68
- /**
69
- * @noInheritDoc
70
- */
71
- export class Controller extends events.EventEmitter<ControllerEventMap> {
72
- private options: Options;
73
- private database!: Database;
74
- private adapter!: Adapter;
75
- private greenPower!: GreenPower;
76
- private touchlink!: Touchlink;
77
-
78
- private permitJoinTimer: NodeJS.Timeout | undefined;
79
- private permitJoinEnd?: number;
80
- private backupTimer: NodeJS.Timeout | undefined;
81
- private databaseSaveTimer: NodeJS.Timeout | undefined;
82
- private stopping: boolean;
83
- private adapterDisconnected: boolean;
84
- private networkParametersCached: AdapterTsType.NetworkParameters | undefined;
85
- /** List of unknown devices detected during a single runtime session. Serves as de-dupe and anti-spam. */
86
- private unknownDevices: Set<number>;
87
-
88
- /**
89
- * Create a controller
90
- *
91
- * To auto detect the port provide `null` for `options.serialPort.path`
92
- */
93
- public constructor(options: Options) {
94
- super();
95
- this.stopping = false;
96
- this.adapterDisconnected = true; // set false after adapter.start() is successfully called
97
- this.options = mixinDeep(JSON.parse(JSON.stringify(DefaultOptions)), options);
98
- this.unknownDevices = new Set();
99
-
100
- // Validate options
101
- for (const channel of this.options.network.channelList) {
102
- if (channel < 11 || channel > 26) {
103
- throw new Error(`'${channel}' is an invalid channel, use a channel between 11 - 26.`);
104
- }
105
- }
106
-
107
- if (!isNumberArrayOfLength(this.options.network.networkKey, 16)) {
108
- throw new Error(`Network key must be a 16 digits long array, got ${this.options.network.networkKey}.`);
109
- }
110
-
111
- if (!isNumberArrayOfLength(this.options.network.extendedPanID, 8)) {
112
- throw new Error(`ExtendedPanID must be an 8 digits long array, got ${this.options.network.extendedPanID}.`);
113
- }
114
-
115
- if (this.options.network.panID < 1 || this.options.network.panID >= 0xffff) {
116
- throw new Error(`PanID must have a value of 0x0001 (1) - 0xFFFE (65534), got ${this.options.network.panID}.`);
117
- }
118
- }
119
-
120
- /**
121
- * Start the Herdsman controller
122
- */
123
- public async start(): Promise<AdapterTsType.StartResult> {
124
- // Database (create end inject)
125
- this.database = Database.open(this.options.databasePath);
126
- Entity.injectDatabase(this.database);
127
-
128
- // Adapter (create and inject)
129
- this.adapter = await Adapter.create(this.options.network, this.options.serialPort, this.options.backupPath, this.options.adapter);
130
-
131
- const stringifiedOptions = JSON.stringify(this.options).replaceAll(JSON.stringify(this.options.network.networkKey), '"HIDDEN"');
132
- logger.debug(`Starting with options '${stringifiedOptions}'`, NS);
133
- const startResult = await this.adapter.start();
134
- logger.debug(`Started with result '${startResult}'`, NS);
135
- this.adapterDisconnected = false;
136
-
137
- // Check if we have to change the channel, only do this when adapter `resumed` because:
138
- // - `getNetworkParameters` might be return wrong info because it needs to propogate after backup restore
139
- // - If result is not `resumed` (`reset` or `restored`), the adapter should comission with the channel from `this.options.network`
140
- if (startResult === "resumed") {
141
- const netParams = await this.getNetworkParameters();
142
- const configuredChannel = this.options.network.channelList[0];
143
- const adapterChannel = netParams.channel;
144
- const nwkUpdateID = netParams.nwkUpdateID;
145
-
146
- if (configuredChannel !== adapterChannel) {
147
- logger.info(`Configured channel '${configuredChannel}' does not match adapter channel '${adapterChannel}', changing channel`, NS);
148
- await this.changeChannel(adapterChannel, configuredChannel, nwkUpdateID);
149
- }
150
- }
151
-
152
- Entity.injectAdapter(this.adapter);
153
-
154
- // log injection
155
- logger.debug(`Injected database: ${this.database !== undefined}, adapter: ${this.adapter !== undefined}`, NS);
156
-
157
- this.greenPower = new GreenPower(this.adapter);
158
- this.greenPower.on("deviceJoined", this.onDeviceJoinedGreenPower.bind(this));
159
- this.greenPower.on("deviceLeave", this.onDeviceLeaveGreenPower.bind(this));
160
-
161
- // Register adapter events
162
- this.adapter.on("deviceJoined", this.onDeviceJoined.bind(this));
163
- this.adapter.on("zclPayload", this.onZclPayload.bind(this));
164
- this.adapter.on("zdoResponse", this.onZdoResponse.bind(this));
165
- this.adapter.on("disconnected", this.onAdapterDisconnected.bind(this));
166
- this.adapter.on("deviceLeave", this.onDeviceLeave.bind(this));
167
-
168
- if (startResult === "reset") {
169
- if (this.options.databaseBackupPath && fs.existsSync(this.options.databasePath)) {
170
- fs.copyFileSync(this.options.databasePath, this.options.databaseBackupPath);
171
- }
172
-
173
- logger.debug("Clearing database...", NS);
174
- for (const group of Group.allIterator()) {
175
- group.removeFromDatabase();
176
- }
177
-
178
- for (const device of Device.allIterator()) {
179
- device.removeFromDatabase();
180
- }
181
- Device.resetCache();
182
- }
183
-
184
- if (startResult === "reset" || (this.options.backupPath && !fs.existsSync(this.options.backupPath))) {
185
- await this.backup();
186
- }
187
-
188
- // Add coordinator to the database if it is not there yet.
189
- const coordinatorIEEE = await this.adapter.getCoordinatorIEEE();
190
-
191
- if (Device.byType("Coordinator").length === 0) {
192
- logger.debug("No coordinator in database, querying...", NS);
193
- const coordinator = Device.create(
194
- "Coordinator",
195
- coordinatorIEEE,
196
- ZSpec.COORDINATOR_ADDRESS,
197
- this.adapter.manufacturerID,
198
- undefined,
199
- undefined,
200
- undefined,
201
- InterviewState.Successful,
202
- undefined,
203
- );
204
-
205
- await coordinator.updateActiveEndpoints();
206
-
207
- for (const endpoint of coordinator.endpoints) {
208
- await endpoint.updateSimpleDescriptor();
209
- }
210
-
211
- coordinator.save();
212
- }
213
-
214
- // Update coordinator ieeeAddr if changed, can happen due to e.g. reflashing
215
- const databaseCoordinator = Device.byType("Coordinator")[0];
216
- if (databaseCoordinator.ieeeAddr !== coordinatorIEEE) {
217
- logger.info(`Coordinator address changed, updating to '${coordinatorIEEE}'`, NS);
218
- databaseCoordinator.changeIeeeAddress(coordinatorIEEE);
219
- }
220
-
221
- // Set backup timer to 1 day.
222
- this.backupTimer = setInterval(() => this.backup(), 86400000);
223
-
224
- // Set database save timer to 1 hour.
225
- this.databaseSaveTimer = setInterval(() => this.databaseSave(), 3600000);
226
-
227
- this.touchlink = new Touchlink(this.adapter);
228
-
229
- return startResult;
230
- }
231
-
232
- public async touchlinkIdentify(ieeeAddr: string, channel: number): Promise<void> {
233
- await this.touchlink.identify(ieeeAddr, channel);
234
- }
235
-
236
- public async touchlinkScan(): Promise<{ieeeAddr: string; channel: number}[]> {
237
- return await this.touchlink.scan();
238
- }
239
-
240
- public async touchlinkFactoryReset(ieeeAddr: string, channel: number): Promise<boolean> {
241
- return await this.touchlink.factoryReset(ieeeAddr, channel);
242
- }
243
-
244
- public async touchlinkFactoryResetFirst(): Promise<boolean> {
245
- return await this.touchlink.factoryResetFirst();
246
- }
247
-
248
- public async addInstallCode(installCode: string): Promise<void> {
249
- // will throw if code cannot be parsed
250
- const [ieeeAddr, keyStr] = parseInstallCode(installCode);
251
- // biome-ignore lint/style/noNonNullAssertion: valid from above parsing
252
- const key = Buffer.from(keyStr.match(/.{1,2}/g)!.map((d) => Number.parseInt(d, 16)));
253
- // will throw if code cannot be fixed and is invalid
254
- const [adjustedKey, adjusted] = checkInstallCode(key, true);
255
-
256
- if (adjusted) {
257
- logger.info(`Install code was adjusted for reason '${adjusted}'.`, NS);
258
- }
259
-
260
- logger.info(`Adding install code for ${ieeeAddr}.`, NS);
261
-
262
- await this.adapter.addInstallCode(ieeeAddr, adjustedKey, false);
263
-
264
- if (adjusted === "missing CRC") {
265
- // in case the CRC was missing, could also be a "already-hashed" key, send both
266
- // XXX: seems to be the case for old HA1.2 devices
267
- await this.adapter.addInstallCode(ieeeAddr, key, true);
268
- }
269
- }
270
-
271
- public async permitJoin(time: number, device?: Device): Promise<void> {
272
- clearTimeout(this.permitJoinTimer);
273
- this.permitJoinTimer = undefined;
274
- this.permitJoinEnd = undefined;
275
-
276
- if (time > 0) {
277
- // never permit more than uint8, and never permit 255 that is often equal to "forever"
278
- assert(time <= 254, "Cannot permit join for more than 254 seconds.");
279
-
280
- await this.adapter.permitJoin(time, device?.networkAddress);
281
- await this.greenPower.permitJoin(time, device?.networkAddress);
282
-
283
- const timeMs = time * 1000;
284
- this.permitJoinEnd = Date.now() + timeMs;
285
- this.permitJoinTimer = setTimeout((): void => {
286
- this.emit("permitJoinChanged", {permitted: false});
287
-
288
- this.permitJoinTimer = undefined;
289
- this.permitJoinEnd = undefined;
290
- }, timeMs);
291
-
292
- this.emit("permitJoinChanged", {permitted: true, time});
293
- } else {
294
- logger.debug("Disable joining", NS);
295
-
296
- await this.greenPower.permitJoin(0);
297
- await this.adapter.permitJoin(0);
298
-
299
- this.emit("permitJoinChanged", {permitted: false});
300
- }
301
- }
302
-
303
- public getPermitJoin(): boolean {
304
- return this.permitJoinTimer !== undefined;
305
- }
306
-
307
- public getPermitJoinEnd(): number | undefined {
308
- return this.permitJoinEnd;
309
- }
310
-
311
- public isStopping(): boolean {
312
- return this.stopping;
313
- }
314
-
315
- public isAdapterDisconnected(): boolean {
316
- return this.adapterDisconnected;
317
- }
318
-
319
- public async stop(): Promise<void> {
320
- this.stopping = true;
321
-
322
- // Unregister adapter events
323
- this.adapter.removeAllListeners();
324
-
325
- clearInterval(this.backupTimer);
326
- clearInterval(this.databaseSaveTimer);
327
-
328
- if (this.adapterDisconnected) {
329
- this.databaseSave();
330
- } else {
331
- try {
332
- await this.permitJoin(0);
333
- } catch (error) {
334
- logger.error(`Failed to disable join on stop: ${error}`, NS);
335
- }
336
-
337
- await this.backup(); // always calls databaseSave()
338
- await this.adapter.stop();
339
-
340
- this.adapterDisconnected = true;
341
- }
342
-
343
- Device.resetCache();
344
- Group.resetCache();
345
- }
346
-
347
- private databaseSave(): void {
348
- for (const device of Device.allIterator()) {
349
- device.save(false);
350
- }
351
-
352
- for (const group of Group.allIterator()) {
353
- group.save(false);
354
- }
355
-
356
- this.database.write();
357
- }
358
-
359
- public async backup(): Promise<void> {
360
- this.databaseSave();
361
- if (this.options.backupPath && (await this.adapter.supportsBackup())) {
362
- logger.debug("Creating coordinator backup", NS);
363
- const backup = await this.adapter.backup(this.getDeviceIeeeAddresses());
364
- const unifiedBackup = BackupUtils.toUnifiedBackup(backup);
365
- const tmpBackupPath = `${this.options.backupPath}.tmp`;
366
- fs.writeFileSync(tmpBackupPath, JSON.stringify(unifiedBackup, null, 2));
367
- fs.renameSync(tmpBackupPath, this.options.backupPath);
368
- logger.info(`Wrote coordinator backup to '${this.options.backupPath}'`, NS);
369
- }
370
- }
371
-
372
- public async coordinatorCheck(): Promise<{missingRouters: Device[]}> {
373
- if (await this.adapter.supportsBackup()) {
374
- const backup = await this.adapter.backup(this.getDeviceIeeeAddresses());
375
- const devicesInBackup = backup.devices.map((d) => ZSpec.Utils.eui64BEBufferToHex(d.ieeeAddress));
376
- const missingRouters = [];
377
-
378
- for (const device of this.getDevicesIterator((d) => d.type === "Router" && !devicesInBackup.includes(d.ieeeAddr as Eui64))) {
379
- missingRouters.push(device);
380
- }
381
-
382
- return {missingRouters};
383
- }
384
-
385
- throw new Error("Coordinator does not coordinator check because it doesn't support backups");
386
- }
387
-
388
- public async reset(type: "soft" | "hard"): Promise<void> {
389
- await this.adapter.reset(type);
390
- }
391
-
392
- public async getCoordinatorVersion(): Promise<AdapterTsType.CoordinatorVersion> {
393
- return await this.adapter.getCoordinatorVersion();
394
- }
395
-
396
- public async getNetworkParameters(): Promise<AdapterTsType.NetworkParameters> {
397
- // Cache network parameters as they don't change anymore after start.
398
- if (!this.networkParametersCached) {
399
- this.networkParametersCached = await this.adapter.getNetworkParameters();
400
- }
401
-
402
- return this.networkParametersCached;
403
- }
404
-
405
- /**
406
- * Get all devices
407
- * @deprecated use getDevicesIterator()
408
- */
409
- public getDevices(): Device[] {
410
- return Device.all();
411
- }
412
-
413
- /**
414
- * Get iterator for all devices
415
- */
416
- public getDevicesIterator(predicate?: (value: Device) => boolean): Generator<Device> {
417
- return Device.allIterator(predicate);
418
- }
419
-
420
- /**
421
- * Get all devices with a specific type
422
- */
423
- public getDevicesByType(type: DeviceType): Device[] {
424
- return Device.byType(type);
425
- }
426
-
427
- /**
428
- * Get device by ieeeAddr
429
- */
430
- public getDeviceByIeeeAddr(ieeeAddr: string): Device | undefined {
431
- return Device.byIeeeAddr(ieeeAddr);
432
- }
433
-
434
- /**
435
- * Get device by networkAddress
436
- */
437
- public getDeviceByNetworkAddress(networkAddress: number): Device | undefined {
438
- return Device.byNetworkAddress(networkAddress);
439
- }
440
-
441
- /**
442
- * Get IEEE address for all devices
443
- */
444
- public getDeviceIeeeAddresses(): string[] {
445
- const deviceIeeeAddresses = [];
446
-
447
- for (const device of Device.allIterator()) {
448
- deviceIeeeAddresses.push(device.ieeeAddr);
449
- }
450
-
451
- return deviceIeeeAddresses;
452
- }
453
-
454
- /**
455
- * Get group by ID
456
- */
457
- public getGroupByID(groupID: number): Group | undefined {
458
- return Group.byGroupID(groupID);
459
- }
460
-
461
- /**
462
- * Get all groups
463
- * @deprecated use getGroupsIterator()
464
- */
465
- public getGroups(): Group[] {
466
- return Group.all();
467
- }
468
-
469
- /**
470
- * Get iterator for all groups
471
- */
472
- public getGroupsIterator(predicate?: (value: Group) => boolean): Generator<Group> {
473
- return Group.allIterator(predicate);
474
- }
475
-
476
- /**
477
- * Create a Group
478
- */
479
- public createGroup(groupID: number): Group {
480
- return Group.create(groupID);
481
- }
482
-
483
- /**
484
- * Broadcast a network-wide channel change.
485
- */
486
- private async changeChannel(oldChannel: number, newChannel: number, nwkUpdateID: number): Promise<void> {
487
- logger.warning(`Changing channel from '${oldChannel}' to '${newChannel}'`, NS);
488
-
489
- // According to the Zigbee specification:
490
- // 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.
491
- // The valid range of nwkUpdateId is 0x00 to 0xFF, and it should wrap back to 0 if necessary.
492
- if (++nwkUpdateID > 0xff) {
493
- nwkUpdateID = 0x00;
494
- }
495
-
496
- const clusterId = Zdo.ClusterId.NWK_UPDATE_REQUEST;
497
- const zdoPayload = Zdo.Buffalo.buildRequest(
498
- this.adapter.hasZdoMessageOverhead,
499
- clusterId,
500
- [newChannel],
501
- 0xfe,
502
- undefined,
503
- nwkUpdateID,
504
- undefined,
505
- );
506
-
507
- await this.adapter.sendZdo(ZSpec.BLANK_EUI64, ZSpec.BroadcastAddress.SLEEPY, clusterId, zdoPayload, true);
508
- logger.info(`Channel changed to '${newChannel}'`, NS);
509
-
510
- this.networkParametersCached = undefined; // invalidate cache
511
- // wait for the broadcast to propagate and the adapter to actually change
512
- // NOTE: observed to ~9sec on `ember` with actual stack event
513
- await wait(12000);
514
- }
515
-
516
- public async identifyUnknownDevice(nwkAddress: number): Promise<Device | undefined> {
517
- if (this.unknownDevices.has(nwkAddress)) {
518
- // prevent duplicate triggering
519
- return;
520
- }
521
-
522
- logger.debug(`Trying to identify unknown device with address '${nwkAddress}'`, NS);
523
- this.unknownDevices.add(nwkAddress);
524
- const clusterId = Zdo.ClusterId.IEEE_ADDRESS_REQUEST;
525
- const zdoPayload = Zdo.Buffalo.buildRequest(this.adapter.hasZdoMessageOverhead, clusterId, nwkAddress, false, 0);
526
-
527
- try {
528
- const response = await this.adapter.sendZdo(ZSpec.BLANK_EUI64, nwkAddress, clusterId, zdoPayload, false);
529
-
530
- if (Zdo.Buffalo.checkStatus<Zdo.ClusterId.IEEE_ADDRESS_RESPONSE>(response)) {
531
- const payload = response[1];
532
- const device = Device.byIeeeAddr(payload.eui64);
533
-
534
- if (device) {
535
- this.checkDeviceNetworkAddress(device, payload.eui64, payload.nwkAddress);
536
- this.unknownDevices.delete(payload.nwkAddress);
537
- }
538
-
539
- return device;
540
- }
541
-
542
- throw new Zdo.StatusError(response[0]);
543
- } catch (error) {
544
- // Catches 2 types of exception: Zdo.StatusError and no response from `adapter.sendZdo()`.
545
- logger.debug(`Failed to retrieve IEEE address for device '${nwkAddress}': ${error}`, NS);
546
- }
547
-
548
- // NOTE: by keeping nwkAddress in `this.unknownDevices` on fail, it prevents a non-responding device from potentially spamming identify.
549
- // This only lasts until next reboot (runtime Set), allowing to 'force' another trigger if necessary.
550
- }
551
-
552
- private checkDeviceNetworkAddress(device: Device, ieeeAddress: string, nwkAddress: number): void {
553
- if (device.networkAddress !== nwkAddress) {
554
- logger.debug(`Device '${ieeeAddress}' got new networkAddress '${nwkAddress}'`, NS);
555
- device.networkAddress = nwkAddress;
556
- device.save();
557
-
558
- this.selfAndDeviceEmit(device, "deviceNetworkAddressChanged", {device});
559
- }
560
- }
561
-
562
- private onNetworkAddress(payload: ZdoTypes.NetworkAddressResponse): void {
563
- logger.debug(`Network address from '${payload.eui64}:${payload.nwkAddress}'`, NS);
564
- const device = Device.byIeeeAddr(payload.eui64);
565
-
566
- if (!device) {
567
- logger.debug(`Network address is from unknown device '${payload.eui64}:${payload.nwkAddress}'`, NS);
568
- return;
569
- }
570
-
571
- device.updateLastSeen();
572
- this.selfAndDeviceEmit(device, "lastSeenChanged", {device, reason: "networkAddress"});
573
- this.checkDeviceNetworkAddress(device, payload.eui64, payload.nwkAddress);
574
- }
575
-
576
- private onIEEEAddress(payload: ZdoTypes.IEEEAddressResponse): void {
577
- logger.debug(`IEEE address from '${payload.eui64}:${payload.nwkAddress}'`, NS);
578
- const device = Device.byIeeeAddr(payload.eui64);
579
-
580
- if (!device) {
581
- logger.debug(`IEEE address is from unknown device '${payload.eui64}:${payload.nwkAddress}'`, NS);
582
- return;
583
- }
584
-
585
- device.updateLastSeen();
586
- this.selfAndDeviceEmit(device, "lastSeenChanged", {device, reason: "networkAddress"});
587
- this.checkDeviceNetworkAddress(device, payload.eui64, payload.nwkAddress);
588
- }
589
-
590
- private onDeviceAnnounce(payload: ZdoTypes.EndDeviceAnnounce): void {
591
- logger.debug(`Device announce from '${payload.eui64}:${payload.nwkAddress}'`, NS);
592
- const device = Device.byIeeeAddr(payload.eui64);
593
-
594
- if (!device) {
595
- logger.debug(`Device announce is from unknown device '${payload.eui64}:${payload.nwkAddress}'`, NS);
596
- return;
597
- }
598
-
599
- device.updateLastSeen();
600
- this.selfAndDeviceEmit(device, "lastSeenChanged", {device, reason: "deviceAnnounce"});
601
- device.implicitCheckin();
602
- this.checkDeviceNetworkAddress(device, payload.eui64, payload.nwkAddress);
603
- this.selfAndDeviceEmit(device, "deviceAnnounce", {device});
604
- }
605
-
606
- private onDeviceLeave(payload: AdapterEvents.DeviceLeavePayload): void {
607
- logger.debug(`Device leave '${payload.ieeeAddr}'`, NS);
608
-
609
- // XXX: seems type is not properly detected?
610
- // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
611
- const device = payload.ieeeAddr ? Device.byIeeeAddr(payload.ieeeAddr) : Device.byNetworkAddress(payload.networkAddress!);
612
-
613
- if (!device) {
614
- logger.debug(`Device leave is from unknown or already deleted device '${payload.ieeeAddr ?? payload.networkAddress}'`, NS);
615
- return;
616
- }
617
-
618
- logger.debug(`Removing device from database '${device.ieeeAddr}'`, NS);
619
- device.removeFromDatabase();
620
-
621
- this.selfAndDeviceEmit(device, "deviceLeave", {ieeeAddr: device.ieeeAddr});
622
- }
623
-
624
- private async onAdapterDisconnected(): Promise<void> {
625
- logger.debug("Adapter disconnected", NS);
626
-
627
- this.adapterDisconnected = true;
628
-
629
- try {
630
- await this.adapter.stop();
631
- } catch (error) {
632
- logger.error(`Failed to stop adapter on disconnect: ${error}`, NS);
633
- }
634
-
635
- this.emit("adapterDisconnected");
636
- }
637
-
638
- private onDeviceJoinedGreenPower(payload: GreenPowerDeviceJoinedPayload): void {
639
- logger.debug(() => `Green power device '${JSON.stringify(payload).replaceAll(/\[[\d,]+\]/g, "HIDDEN")}' joined`, NS);
640
-
641
- // Green power devices don't have an ieeeAddr, the sourceID is unique and static so use this.
642
- const ieeeAddr = GreenPower.sourceIdToIeeeAddress(payload.sourceID);
643
- // Green power devices dont' have a modelID, create a modelID based on the deviceID (=type)
644
- const modelID = `GreenPower_${payload.deviceID}`;
645
- let device = Device.byIeeeAddr(ieeeAddr, true);
646
-
647
- if (!device) {
648
- logger.debug(`New green power device '${ieeeAddr}' joined`, NS);
649
- logger.debug(`Creating device '${ieeeAddr}'`, NS);
650
- device = Device.create(
651
- "GreenPower",
652
- ieeeAddr,
653
- payload.networkAddress,
654
- undefined,
655
- undefined,
656
- undefined,
657
- modelID,
658
- InterviewState.Successful,
659
- payload.securityKey ? Array.from(payload.securityKey) : /* v8 ignore next */ undefined,
660
- );
661
-
662
- device.save();
663
-
664
- this.selfAndDeviceEmit(device, "deviceJoined", {device});
665
- this.selfAndDeviceEmit(device, "deviceInterview", {status: "successful", device});
666
- } else if (device.isDeleted) {
667
- logger.debug(`Deleted green power device '${ieeeAddr}' joined, undeleting`, NS);
668
-
669
- device.undelete();
670
-
671
- this.selfAndDeviceEmit(device, "deviceJoined", {device});
672
- this.selfAndDeviceEmit(device, "deviceInterview", {status: "successful", device});
673
- }
674
- }
675
-
676
- private onDeviceLeaveGreenPower(sourceID: number): void {
677
- logger.debug(`Green power device '${sourceID}' left`, NS);
678
-
679
- // Green power devices don't have an ieeeAddr, the sourceID is unique and static so use this.
680
- const ieeeAddr = GreenPower.sourceIdToIeeeAddress(sourceID);
681
- const device = Device.byIeeeAddr(ieeeAddr);
682
-
683
- if (!device) {
684
- logger.debug(`Green power device leave is from unknown or already deleted device '${ieeeAddr}'`, NS);
685
- return;
686
- }
687
-
688
- logger.debug(`Removing green power device from database '${device.ieeeAddr}'`, NS);
689
- device.removeFromDatabase();
690
-
691
- this.selfAndDeviceEmit(device, "deviceLeave", {ieeeAddr: device.ieeeAddr});
692
- }
693
-
694
- private selfAndDeviceEmit<K extends keyof ControllerEventMap>(
695
- device: Device,
696
- event: K,
697
- ...args: K extends keyof ControllerEventMap ? ControllerEventMap[K] : never
698
- ): void {
699
- device.emit(event, ...args);
700
- this.emit(event, ...args);
701
- }
702
-
703
- private async onDeviceJoined(payload: AdapterEvents.DeviceJoinedPayload): Promise<void> {
704
- logger.debug(`Device '${payload.ieeeAddr}' joined`, NS);
705
-
706
- if (this.options.acceptJoiningDeviceHandler) {
707
- if (!(await this.options.acceptJoiningDeviceHandler(payload.ieeeAddr))) {
708
- logger.debug(`Device '${payload.ieeeAddr}' rejected by handler, removing it`, NS);
709
-
710
- // XXX: GP devices? see Device.removeFromNetwork
711
- try {
712
- const clusterId = Zdo.ClusterId.LEAVE_REQUEST;
713
- const zdoPayload = Zdo.Buffalo.buildRequest(
714
- this.adapter.hasZdoMessageOverhead,
715
- clusterId,
716
- payload.ieeeAddr as Eui64,
717
- Zdo.LeaveRequestFlags.WITHOUT_REJOIN,
718
- );
719
- const response = await this.adapter.sendZdo(payload.ieeeAddr, payload.networkAddress, clusterId, zdoPayload, false);
720
-
721
- if (!Zdo.Buffalo.checkStatus<Zdo.ClusterId.LEAVE_RESPONSE>(response)) {
722
- throw new Zdo.StatusError(response[0]);
723
- }
724
- } catch (error) {
725
- logger.error(`Failed to remove rejected device: ${(error as Error).message}`, NS);
726
- }
727
-
728
- return;
729
- }
730
-
731
- logger.debug(`Device '${payload.ieeeAddr}' accepted by handler`, NS);
732
- }
733
-
734
- let device = Device.byIeeeAddr(payload.ieeeAddr, true);
735
- if (!device) {
736
- logger.debug(`New device '${payload.ieeeAddr}' joined`, NS);
737
- logger.debug(`Creating device '${payload.ieeeAddr}'`, NS);
738
- device = Device.create(
739
- "Unknown",
740
- payload.ieeeAddr,
741
- payload.networkAddress,
742
- undefined,
743
- undefined,
744
- undefined,
745
- undefined,
746
- InterviewState.Pending,
747
- undefined,
748
- );
749
- this.selfAndDeviceEmit(device, "deviceJoined", {device});
750
- } else if (device.isDeleted) {
751
- logger.debug(`Deleted device '${payload.ieeeAddr}' joined, undeleting`, NS);
752
- device.undelete();
753
- this.selfAndDeviceEmit(device, "deviceJoined", {device});
754
- }
755
-
756
- if (device.networkAddress !== payload.networkAddress) {
757
- logger.debug(`Device '${payload.ieeeAddr}' is already in database with different network address, updating network address`, NS);
758
- device.networkAddress = payload.networkAddress;
759
- device.save();
760
- }
761
-
762
- device.updateLastSeen();
763
- this.selfAndDeviceEmit(device, "lastSeenChanged", {device, reason: "deviceJoined"});
764
- device.implicitCheckin();
765
-
766
- if (device.interviewState === InterviewState.Pending || device.interviewState === InterviewState.Failed) {
767
- logger.info(`Interview for '${device.ieeeAddr}' started`, NS);
768
- this.selfAndDeviceEmit(device, "deviceInterview", {status: "started", device});
769
-
770
- try {
771
- await device.interview();
772
- logger.info(`Succesfully interviewed '${device.ieeeAddr}'`, NS);
773
- this.selfAndDeviceEmit(device, "deviceInterview", {status: "successful", device});
774
- } catch (error) {
775
- logger.error(`Interview failed for '${device.ieeeAddr} with error '${error}'`, NS);
776
- this.selfAndDeviceEmit(device, "deviceInterview", {status: "failed", device});
777
- }
778
- } else {
779
- logger.debug(`Not interviewing '${payload.ieeeAddr}', interviewState=${device.interviewState}'`, NS);
780
- }
781
- }
782
-
783
- private onZdoResponse(clusterId: Zdo.ClusterId, response: ZdoTypes.GenericZdoResponse): void {
784
- logger.debug(
785
- `Received ZDO response: clusterId=${Zdo.ClusterId[clusterId]}, status=${Zdo.Status[response[0]]}, payload=${JSON.stringify(response[1])}`,
786
- NS,
787
- );
788
-
789
- switch (clusterId) {
790
- case Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE: {
791
- if (Zdo.Buffalo.checkStatus<typeof clusterId>(response)) {
792
- this.onNetworkAddress(response[1]);
793
- }
794
- break;
795
- }
796
-
797
- case Zdo.ClusterId.IEEE_ADDRESS_RESPONSE: {
798
- if (Zdo.Buffalo.checkStatus<typeof clusterId>(response)) {
799
- this.onIEEEAddress(response[1]);
800
- }
801
- break;
802
- }
803
-
804
- case Zdo.ClusterId.END_DEVICE_ANNOUNCE: {
805
- if (Zdo.Buffalo.checkStatus<typeof clusterId>(response)) {
806
- this.onDeviceAnnounce(response[1]);
807
- }
808
- break;
809
- }
810
- }
811
- }
812
-
813
- private async onZclPayload(payload: AdapterEvents.ZclPayload): Promise<void> {
814
- let frame: Zcl.Frame | undefined;
815
- let device: Device | undefined;
816
-
817
- if (payload.clusterID === Zcl.Clusters.touchlink.ID) {
818
- // This is handled by touchlink
819
- return;
820
- }
821
-
822
- if (payload.clusterID === Zcl.Clusters.greenPower.ID) {
823
- try {
824
- // Custom clusters are not supported for Green Power since we need to parse the frame to get the device.
825
- frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
826
- } catch (error) {
827
- logger.debug(`Failed to parse frame green power frame, ignoring it: ${error}`, NS);
828
- return;
829
- }
830
-
831
- if (frame.payload.commandID === undefined) {
832
- // can be from gpd or gpp
833
- // can be:
834
- // - greenPower.commandsResponse.commissioningMode
835
- // - Foundation.defaultRsp for greenPower.commandsResponse.pairing with status INVALID_FIELD or INSUFFICIENT_SPACE
836
- // - ...
837
- device = Device.find(payload.address);
838
- } else {
839
- if (frame.payload.srcID === undefined) {
840
- logger.debug("Data is from unsupported green power device with IEEE addressing, skipping...", NS);
841
- return;
842
- }
843
-
844
- const ieeeAddr = GreenPower.sourceIdToIeeeAddress(frame.payload.srcID);
845
- device = Device.byIeeeAddr(ieeeAddr);
846
- frame = await this.greenPower.processCommand(payload, frame, device?.gpSecurityKey ? Buffer.from(device.gpSecurityKey) : undefined);
847
-
848
- // lookup encapsulated gpDevice for further processing (re-fetch, may have been created by above call)
849
- device = Device.byIeeeAddr(ieeeAddr);
850
-
851
- if (!device) {
852
- logger.debug(`Data is from unknown green power device with address '${ieeeAddr}' (${frame.payload.srcID}), skipping...`, NS);
853
- return;
854
- }
855
- }
856
- } else {
857
- /**
858
- * Handling of re-transmitted Xiaomi messages.
859
- * https://github.com/Koenkk/zigbee2mqtt/issues/1238
860
- * https://github.com/Koenkk/zigbee2mqtt/issues/3592
861
- *
862
- * Some Xiaomi router devices re-transmit messages from Xiaomi end devices.
863
- * The network address of these message is set to the one of the Xiaomi router.
864
- * Therefore it looks like if the message came from the Xiaomi router, while in
865
- * fact it came from the end device.
866
- * Handling these message would result in false state updates.
867
- * The group ID attribute of these message defines the network address of the end device.
868
- */
869
- device = Device.find(payload.address);
870
-
871
- if (device?.manufacturerName === "LUMI" && device?.type === "Router" && payload.groupID) {
872
- logger.debug(`Handling re-transmitted Xiaomi message ${device.networkAddress} -> ${payload.groupID}`, NS);
873
- device = Device.byNetworkAddress(payload.groupID);
874
- }
875
-
876
- try {
877
- frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, device ? device.customClusters : {});
878
- } catch (error) {
879
- logger.debug(`Failed to parse frame: ${error}`, NS);
880
- }
881
- }
882
-
883
- if (!device) {
884
- if (typeof payload.address === "number") {
885
- device = await this.identifyUnknownDevice(payload.address);
886
- }
887
-
888
- if (!device) {
889
- logger.debug(`Data is from unknown device with address '${payload.address}', skipping...`, NS);
890
- return;
891
- }
892
- }
893
-
894
- logger.debug(
895
- `Received payload: clusterID=${payload.clusterID}, address=${payload.address}, groupID=${payload.groupID}, ` +
896
- `endpoint=${payload.endpoint}, destinationEndpoint=${payload.destinationEndpoint}, wasBroadcast=${payload.wasBroadcast}, ` +
897
- `linkQuality=${payload.linkquality}, frame=${frame?.toString()}`,
898
- NS,
899
- );
900
-
901
- device.updateLastSeen();
902
-
903
- //no implicit checkin for genPollCtrl data because it might interfere with the explicit checkin
904
- if (!frame?.isCluster("genPollCtrl")) {
905
- device.implicitCheckin();
906
- }
907
-
908
- device.linkquality = payload.linkquality;
909
- let endpoint = device.getEndpoint(payload.endpoint);
910
-
911
- if (!endpoint) {
912
- logger.debug(
913
- `Data is from unknown endpoint '${payload.endpoint}' from device with network address '${payload.address}', creating it...`,
914
- NS,
915
- );
916
-
917
- endpoint = device.createEndpoint(payload.endpoint);
918
- }
919
-
920
- // Parse command for event
921
- let type: Events.MessagePayload["type"] | undefined;
922
- let data: Events.MessagePayload["data"] = {};
923
- let clusterName: Events.MessagePayload["cluster"];
924
- const meta: {
925
- zclTransactionSequenceNumber?: number;
926
- manufacturerCode?: number;
927
- frameControl?: FrameControl;
928
- rawData: Buffer;
929
- } = {rawData: payload.data};
930
-
931
- if (frame) {
932
- const command = frame.command;
933
- clusterName = frame.cluster.name;
934
- meta.zclTransactionSequenceNumber = frame.header.transactionSequenceNumber;
935
- meta.manufacturerCode = frame.header.manufacturerCode;
936
- meta.frameControl = frame.header.frameControl;
937
-
938
- if (frame.header.isGlobal) {
939
- switch (frame.command.name) {
940
- case "report": {
941
- type = "attributeReport";
942
- data = ZclFrameConverter.attributeKeyValue(frame, device.manufacturerID, device.customClusters);
943
- break;
944
- }
945
-
946
- case "read": {
947
- type = "read";
948
- data = ZclFrameConverter.attributeList(frame, device.manufacturerID, device.customClusters);
949
- break;
950
- }
951
-
952
- case "write": {
953
- type = "write";
954
- data = ZclFrameConverter.attributeKeyValue(frame, device.manufacturerID, device.customClusters);
955
- break;
956
- }
957
-
958
- case "readRsp": {
959
- type = "readResponse";
960
- data = ZclFrameConverter.attributeKeyValue(frame, device.manufacturerID, device.customClusters);
961
- break;
962
- }
963
-
964
- case "defaultRsp": {
965
- if (frame.payload.statusCode !== Zcl.Status.SUCCESS) {
966
- logger.debug(
967
- `Failure default response from '${payload.address}': clusterID=${payload.clusterID} cmdId=${frame.payload.cmdId} status=${Zcl.Status[frame.payload.statusCode]}`,
968
- NS,
969
- );
970
- }
971
-
972
- break;
973
- }
974
- }
975
-
976
- if (type === "readResponse" || type === "attributeReport") {
977
- // devices report attributes through readRsp or attributeReport
978
- if (frame.isCluster("genBasic")) {
979
- device.updateGenBasic(data as TPartialClusterAttributes<"genBasic">);
980
- }
981
-
982
- endpoint.saveClusterAttributeKeyValue(frame.cluster.ID, data);
983
- }
984
- } else {
985
- if (frame.header.isSpecific) {
986
- type = `command${command.name.charAt(0).toUpperCase()}${command.name.slice(1)}`;
987
- data = frame.payload;
988
- }
989
- }
990
- } else {
991
- type = "raw";
992
- data = payload.data;
993
- const name = Zcl.Utils.getCluster(payload.clusterID, device.manufacturerID, device.customClusters).name;
994
- clusterName = Number.isNaN(Number(name)) ? name : Number(name);
995
- }
996
-
997
- if (type && data) {
998
- const linkquality = payload.linkquality;
999
- const groupID = payload.groupID;
1000
-
1001
- this.selfAndDeviceEmit(device, "message", {
1002
- type,
1003
- device,
1004
- endpoint,
1005
- data,
1006
- linkquality,
1007
- groupID,
1008
- cluster: clusterName,
1009
- meta,
1010
- });
1011
- this.selfAndDeviceEmit(device, "lastSeenChanged", {device, reason: "messageEmitted"});
1012
- } else {
1013
- this.selfAndDeviceEmit(device, "lastSeenChanged", {device, reason: "messageNonEmitted"});
1014
- }
1015
-
1016
- if (frame) {
1017
- await device.onZclData(payload, frame, endpoint);
1018
- }
1019
- }
1020
- }
1021
-
1022
- export default Controller;