@matterbridge/core 3.5.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 (295) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +22 -0
  3. package/dist/cli.d.ts +29 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +268 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/cliEmitter.d.ts +50 -0
  8. package/dist/cliEmitter.d.ts.map +1 -0
  9. package/dist/cliEmitter.js +49 -0
  10. package/dist/cliEmitter.js.map +1 -0
  11. package/dist/cliHistory.d.ts +48 -0
  12. package/dist/cliHistory.d.ts.map +1 -0
  13. package/dist/cliHistory.js +826 -0
  14. package/dist/cliHistory.js.map +1 -0
  15. package/dist/clusters/export.d.ts +2 -0
  16. package/dist/clusters/export.d.ts.map +1 -0
  17. package/dist/clusters/export.js +3 -0
  18. package/dist/clusters/export.js.map +1 -0
  19. package/dist/crypto/attestationDecoder.d.ts +180 -0
  20. package/dist/crypto/attestationDecoder.d.ts.map +1 -0
  21. package/dist/crypto/attestationDecoder.js +176 -0
  22. package/dist/crypto/attestationDecoder.js.map +1 -0
  23. package/dist/crypto/declarationDecoder.d.ts +72 -0
  24. package/dist/crypto/declarationDecoder.d.ts.map +1 -0
  25. package/dist/crypto/declarationDecoder.js +241 -0
  26. package/dist/crypto/declarationDecoder.js.map +1 -0
  27. package/dist/crypto/extract/342/200/220cert/342/200/220extensions.d.ts +9 -0
  28. package/dist/crypto/extract/342/200/220cert/342/200/220extensions.d.ts.map +1 -0
  29. package/dist/crypto/extract/342/200/220cert/342/200/220extensions.js +120 -0
  30. package/dist/crypto/extract/342/200/220cert/342/200/220extensions.js.map +1 -0
  31. package/dist/crypto/read-extensions.d.ts +2 -0
  32. package/dist/crypto/read-extensions.d.ts.map +1 -0
  33. package/dist/crypto/read-extensions.js +81 -0
  34. package/dist/crypto/read-extensions.js.map +1 -0
  35. package/dist/crypto/testData.d.ts +31 -0
  36. package/dist/crypto/testData.d.ts.map +1 -0
  37. package/dist/crypto/testData.js +131 -0
  38. package/dist/crypto/testData.js.map +1 -0
  39. package/dist/crypto/walk-der.d.ts +2 -0
  40. package/dist/crypto/walk-der.d.ts.map +1 -0
  41. package/dist/crypto/walk-der.js +165 -0
  42. package/dist/crypto/walk-der.js.map +1 -0
  43. package/dist/deviceManager.d.ts +135 -0
  44. package/dist/deviceManager.d.ts.map +1 -0
  45. package/dist/deviceManager.js +270 -0
  46. package/dist/deviceManager.js.map +1 -0
  47. package/dist/devices/airConditioner.d.ts +98 -0
  48. package/dist/devices/airConditioner.d.ts.map +1 -0
  49. package/dist/devices/airConditioner.js +74 -0
  50. package/dist/devices/airConditioner.js.map +1 -0
  51. package/dist/devices/basicVideoPlayer.d.ts +88 -0
  52. package/dist/devices/basicVideoPlayer.d.ts.map +1 -0
  53. package/dist/devices/basicVideoPlayer.js +155 -0
  54. package/dist/devices/basicVideoPlayer.js.map +1 -0
  55. package/dist/devices/batteryStorage.d.ts +48 -0
  56. package/dist/devices/batteryStorage.d.ts.map +1 -0
  57. package/dist/devices/batteryStorage.js +75 -0
  58. package/dist/devices/batteryStorage.js.map +1 -0
  59. package/dist/devices/castingVideoPlayer.d.ts +79 -0
  60. package/dist/devices/castingVideoPlayer.d.ts.map +1 -0
  61. package/dist/devices/castingVideoPlayer.js +101 -0
  62. package/dist/devices/castingVideoPlayer.js.map +1 -0
  63. package/dist/devices/cooktop.d.ts +61 -0
  64. package/dist/devices/cooktop.d.ts.map +1 -0
  65. package/dist/devices/cooktop.js +77 -0
  66. package/dist/devices/cooktop.js.map +1 -0
  67. package/dist/devices/dishwasher.d.ts +71 -0
  68. package/dist/devices/dishwasher.d.ts.map +1 -0
  69. package/dist/devices/dishwasher.js +130 -0
  70. package/dist/devices/dishwasher.js.map +1 -0
  71. package/dist/devices/evse.d.ts +76 -0
  72. package/dist/devices/evse.d.ts.map +1 -0
  73. package/dist/devices/evse.js +156 -0
  74. package/dist/devices/evse.js.map +1 -0
  75. package/dist/devices/export.d.ts +19 -0
  76. package/dist/devices/export.d.ts.map +1 -0
  77. package/dist/devices/export.js +23 -0
  78. package/dist/devices/export.js.map +1 -0
  79. package/dist/devices/extractorHood.d.ts +46 -0
  80. package/dist/devices/extractorHood.d.ts.map +1 -0
  81. package/dist/devices/extractorHood.js +78 -0
  82. package/dist/devices/extractorHood.js.map +1 -0
  83. package/dist/devices/heatPump.d.ts +47 -0
  84. package/dist/devices/heatPump.d.ts.map +1 -0
  85. package/dist/devices/heatPump.js +84 -0
  86. package/dist/devices/heatPump.js.map +1 -0
  87. package/dist/devices/laundryDryer.d.ts +67 -0
  88. package/dist/devices/laundryDryer.d.ts.map +1 -0
  89. package/dist/devices/laundryDryer.js +106 -0
  90. package/dist/devices/laundryDryer.js.map +1 -0
  91. package/dist/devices/laundryWasher.d.ts +81 -0
  92. package/dist/devices/laundryWasher.d.ts.map +1 -0
  93. package/dist/devices/laundryWasher.js +147 -0
  94. package/dist/devices/laundryWasher.js.map +1 -0
  95. package/dist/devices/microwaveOven.d.ts +168 -0
  96. package/dist/devices/microwaveOven.d.ts.map +1 -0
  97. package/dist/devices/microwaveOven.js +179 -0
  98. package/dist/devices/microwaveOven.js.map +1 -0
  99. package/dist/devices/oven.d.ts +105 -0
  100. package/dist/devices/oven.d.ts.map +1 -0
  101. package/dist/devices/oven.js +190 -0
  102. package/dist/devices/oven.js.map +1 -0
  103. package/dist/devices/refrigerator.d.ts +118 -0
  104. package/dist/devices/refrigerator.d.ts.map +1 -0
  105. package/dist/devices/refrigerator.js +186 -0
  106. package/dist/devices/refrigerator.js.map +1 -0
  107. package/dist/devices/roboticVacuumCleaner.d.ts +112 -0
  108. package/dist/devices/roboticVacuumCleaner.d.ts.map +1 -0
  109. package/dist/devices/roboticVacuumCleaner.js +268 -0
  110. package/dist/devices/roboticVacuumCleaner.js.map +1 -0
  111. package/dist/devices/solarPower.d.ts +40 -0
  112. package/dist/devices/solarPower.d.ts.map +1 -0
  113. package/dist/devices/solarPower.js +59 -0
  114. package/dist/devices/solarPower.js.map +1 -0
  115. package/dist/devices/speaker.d.ts +87 -0
  116. package/dist/devices/speaker.d.ts.map +1 -0
  117. package/dist/devices/speaker.js +120 -0
  118. package/dist/devices/speaker.js.map +1 -0
  119. package/dist/devices/temperatureControl.d.ts +166 -0
  120. package/dist/devices/temperatureControl.d.ts.map +1 -0
  121. package/dist/devices/temperatureControl.js +78 -0
  122. package/dist/devices/temperatureControl.js.map +1 -0
  123. package/dist/devices/waterHeater.d.ts +111 -0
  124. package/dist/devices/waterHeater.d.ts.map +1 -0
  125. package/dist/devices/waterHeater.js +166 -0
  126. package/dist/devices/waterHeater.js.map +1 -0
  127. package/dist/dgram/export.d.ts +2 -0
  128. package/dist/dgram/export.d.ts.map +1 -0
  129. package/dist/dgram/export.js +2 -0
  130. package/dist/dgram/export.js.map +1 -0
  131. package/dist/export.d.ts +32 -0
  132. package/dist/export.d.ts.map +1 -0
  133. package/dist/export.js +39 -0
  134. package/dist/export.js.map +1 -0
  135. package/dist/frontend.d.ts +248 -0
  136. package/dist/frontend.d.ts.map +1 -0
  137. package/dist/frontend.js +2605 -0
  138. package/dist/frontend.js.map +1 -0
  139. package/dist/helpers.d.ts +48 -0
  140. package/dist/helpers.d.ts.map +1 -0
  141. package/dist/helpers.js +161 -0
  142. package/dist/helpers.js.map +1 -0
  143. package/dist/jestutils/export.d.ts +2 -0
  144. package/dist/jestutils/export.d.ts.map +1 -0
  145. package/dist/jestutils/export.js +2 -0
  146. package/dist/jestutils/export.js.map +1 -0
  147. package/dist/jestutils/jestHelpers.d.ts +349 -0
  148. package/dist/jestutils/jestHelpers.d.ts.map +1 -0
  149. package/dist/jestutils/jestHelpers.js +980 -0
  150. package/dist/jestutils/jestHelpers.js.map +1 -0
  151. package/dist/matter/behaviors.d.ts +2 -0
  152. package/dist/matter/behaviors.d.ts.map +1 -0
  153. package/dist/matter/behaviors.js +3 -0
  154. package/dist/matter/behaviors.js.map +1 -0
  155. package/dist/matter/clusters.d.ts +2 -0
  156. package/dist/matter/clusters.d.ts.map +1 -0
  157. package/dist/matter/clusters.js +3 -0
  158. package/dist/matter/clusters.js.map +1 -0
  159. package/dist/matter/devices.d.ts +2 -0
  160. package/dist/matter/devices.d.ts.map +1 -0
  161. package/dist/matter/devices.js +3 -0
  162. package/dist/matter/devices.js.map +1 -0
  163. package/dist/matter/endpoints.d.ts +2 -0
  164. package/dist/matter/endpoints.d.ts.map +1 -0
  165. package/dist/matter/endpoints.js +3 -0
  166. package/dist/matter/endpoints.js.map +1 -0
  167. package/dist/matter/export.d.ts +4 -0
  168. package/dist/matter/export.d.ts.map +1 -0
  169. package/dist/matter/export.js +5 -0
  170. package/dist/matter/export.js.map +1 -0
  171. package/dist/matter/types.d.ts +2 -0
  172. package/dist/matter/types.d.ts.map +1 -0
  173. package/dist/matter/types.js +3 -0
  174. package/dist/matter/types.js.map +1 -0
  175. package/dist/matterNode.d.ts +341 -0
  176. package/dist/matterNode.d.ts.map +1 -0
  177. package/dist/matterNode.js +1329 -0
  178. package/dist/matterNode.js.map +1 -0
  179. package/dist/matterbridge.d.ts +544 -0
  180. package/dist/matterbridge.d.ts.map +1 -0
  181. package/dist/matterbridge.js +2880 -0
  182. package/dist/matterbridge.js.map +1 -0
  183. package/dist/matterbridgeAccessoryPlatform.d.ts +49 -0
  184. package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -0
  185. package/dist/matterbridgeAccessoryPlatform.js +80 -0
  186. package/dist/matterbridgeAccessoryPlatform.js.map +1 -0
  187. package/dist/matterbridgeBehaviors.d.ts +2428 -0
  188. package/dist/matterbridgeBehaviors.d.ts.map +1 -0
  189. package/dist/matterbridgeBehaviors.js +620 -0
  190. package/dist/matterbridgeBehaviors.js.map +1 -0
  191. package/dist/matterbridgeDeviceTypes.d.ts +744 -0
  192. package/dist/matterbridgeDeviceTypes.d.ts.map +1 -0
  193. package/dist/matterbridgeDeviceTypes.js +1312 -0
  194. package/dist/matterbridgeDeviceTypes.js.map +1 -0
  195. package/dist/matterbridgeDynamicPlatform.d.ts +49 -0
  196. package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -0
  197. package/dist/matterbridgeDynamicPlatform.js +80 -0
  198. package/dist/matterbridgeDynamicPlatform.js.map +1 -0
  199. package/dist/matterbridgeEndpoint.d.ts +1548 -0
  200. package/dist/matterbridgeEndpoint.d.ts.map +1 -0
  201. package/dist/matterbridgeEndpoint.js +2883 -0
  202. package/dist/matterbridgeEndpoint.js.map +1 -0
  203. package/dist/matterbridgeEndpointHelpers.d.ts +1855 -0
  204. package/dist/matterbridgeEndpointHelpers.d.ts.map +1 -0
  205. package/dist/matterbridgeEndpointHelpers.js +1270 -0
  206. package/dist/matterbridgeEndpointHelpers.js.map +1 -0
  207. package/dist/matterbridgeEndpointTypes.d.ts +172 -0
  208. package/dist/matterbridgeEndpointTypes.d.ts.map +1 -0
  209. package/dist/matterbridgeEndpointTypes.js +28 -0
  210. package/dist/matterbridgeEndpointTypes.js.map +1 -0
  211. package/dist/matterbridgePlatform.d.ts +520 -0
  212. package/dist/matterbridgePlatform.d.ts.map +1 -0
  213. package/dist/matterbridgePlatform.js +921 -0
  214. package/dist/matterbridgePlatform.js.map +1 -0
  215. package/dist/mb_coap.d.ts +24 -0
  216. package/dist/mb_coap.d.ts.map +1 -0
  217. package/dist/mb_coap.js +89 -0
  218. package/dist/mb_coap.js.map +1 -0
  219. package/dist/mb_health.d.ts +77 -0
  220. package/dist/mb_health.d.ts.map +1 -0
  221. package/dist/mb_health.js +147 -0
  222. package/dist/mb_health.js.map +1 -0
  223. package/dist/mb_mdns.d.ts +24 -0
  224. package/dist/mb_mdns.d.ts.map +1 -0
  225. package/dist/mb_mdns.js +285 -0
  226. package/dist/mb_mdns.js.map +1 -0
  227. package/dist/pluginManager.d.ts +388 -0
  228. package/dist/pluginManager.d.ts.map +1 -0
  229. package/dist/pluginManager.js +1574 -0
  230. package/dist/pluginManager.js.map +1 -0
  231. package/dist/spawn.d.ts +33 -0
  232. package/dist/spawn.d.ts.map +1 -0
  233. package/dist/spawn.js +165 -0
  234. package/dist/spawn.js.map +1 -0
  235. package/dist/utils/export.d.ts +2 -0
  236. package/dist/utils/export.d.ts.map +1 -0
  237. package/dist/utils/export.js +2 -0
  238. package/dist/utils/export.js.map +1 -0
  239. package/dist/workers/brand.d.ts +25 -0
  240. package/dist/workers/brand.d.ts.map +1 -0
  241. package/dist/workers/brand.extend.d.ts +10 -0
  242. package/dist/workers/brand.extend.d.ts.map +1 -0
  243. package/dist/workers/brand.extend.js +15 -0
  244. package/dist/workers/brand.extend.js.map +1 -0
  245. package/dist/workers/brand.invalid.d.ts +9 -0
  246. package/dist/workers/brand.invalid.d.ts.map +1 -0
  247. package/dist/workers/brand.invalid.js +19 -0
  248. package/dist/workers/brand.invalid.js.map +1 -0
  249. package/dist/workers/brand.js +67 -0
  250. package/dist/workers/brand.js.map +1 -0
  251. package/dist/workers/clusterTypes.d.ts +47 -0
  252. package/dist/workers/clusterTypes.d.ts.map +1 -0
  253. package/dist/workers/clusterTypes.js +57 -0
  254. package/dist/workers/clusterTypes.js.map +1 -0
  255. package/dist/workers/frontendWorker.d.ts +2 -0
  256. package/dist/workers/frontendWorker.d.ts.map +1 -0
  257. package/dist/workers/frontendWorker.js +90 -0
  258. package/dist/workers/frontendWorker.js.map +1 -0
  259. package/dist/workers/helloWorld.d.ts +2 -0
  260. package/dist/workers/helloWorld.d.ts.map +1 -0
  261. package/dist/workers/helloWorld.js +135 -0
  262. package/dist/workers/helloWorld.js.map +1 -0
  263. package/dist/workers/matterWorker.d.ts +2 -0
  264. package/dist/workers/matterWorker.d.ts.map +1 -0
  265. package/dist/workers/matterWorker.js +104 -0
  266. package/dist/workers/matterWorker.js.map +1 -0
  267. package/dist/workers/matterbridgeWorker.d.ts +2 -0
  268. package/dist/workers/matterbridgeWorker.d.ts.map +1 -0
  269. package/dist/workers/matterbridgeWorker.js +75 -0
  270. package/dist/workers/matterbridgeWorker.js.map +1 -0
  271. package/dist/workers/messageLab.d.ts +134 -0
  272. package/dist/workers/messageLab.d.ts.map +1 -0
  273. package/dist/workers/messageLab.js +129 -0
  274. package/dist/workers/messageLab.js.map +1 -0
  275. package/dist/workers/testWorker.d.ts +2 -0
  276. package/dist/workers/testWorker.d.ts.map +1 -0
  277. package/dist/workers/testWorker.js +45 -0
  278. package/dist/workers/testWorker.js.map +1 -0
  279. package/dist/workers/usage.d.ts +19 -0
  280. package/dist/workers/usage.d.ts.map +1 -0
  281. package/dist/workers/usage.js +140 -0
  282. package/dist/workers/usage.js.map +1 -0
  283. package/dist/workers/workerManager.d.ts +115 -0
  284. package/dist/workers/workerManager.d.ts.map +1 -0
  285. package/dist/workers/workerManager.js +464 -0
  286. package/dist/workers/workerManager.js.map +1 -0
  287. package/dist/workers/workerServer.d.ts +126 -0
  288. package/dist/workers/workerServer.d.ts.map +1 -0
  289. package/dist/workers/workerServer.js +340 -0
  290. package/dist/workers/workerServer.js.map +1 -0
  291. package/dist/workers/workerTypes.d.ts +23 -0
  292. package/dist/workers/workerTypes.d.ts.map +1 -0
  293. package/dist/workers/workerTypes.js +3 -0
  294. package/dist/workers/workerTypes.js.map +1 -0
  295. package/package.json +120 -0
@@ -0,0 +1,1329 @@
1
+ /**
2
+ * This file contains the class MatterNode.
3
+ *
4
+ * @file matterNode.ts
5
+ * @author Luca Liguori
6
+ * @created 2025-10-01
7
+ * @version 1.0.0
8
+ * @license Apache-2.0
9
+ *
10
+ * Copyright 2025, 2026, 2027 Luca Liguori.
11
+ *
12
+ * Licensed under the Apache License, Version 2.0 (the "License");
13
+ * you may not use this file except in compliance with the License.
14
+ * You may obtain a copy of the License at
15
+ *
16
+ * http://www.apache.org/licenses/LICENSE-2.0
17
+ *
18
+ * Unless required by applicable law or agreed to in writing, software
19
+ * distributed under the License is distributed on an "AS IS" BASIS,
20
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21
+ * See the License for the specific language governing permissions and
22
+ * limitations under the License.
23
+ */
24
+ // Node modules
25
+ import path from 'node:path';
26
+ import fs from 'node:fs';
27
+ import EventEmitter from 'node:events';
28
+ // AnsiLogger module
29
+ import { AnsiLogger, BLUE, CYAN, db, debugStringify, er, nf, or, zb } from 'node-ansi-logger';
30
+ // Node persist manager module
31
+ import { NodeStorageManager } from 'node-persist-manager';
32
+ // @matter
33
+ import '@matter/nodejs';
34
+ import { Logger, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, StorageService, UINT32_MAX, UINT16_MAX, Environment, } from '@matter/general';
35
+ import { MdnsService } from '@matter/protocol';
36
+ import { VendorId, DeviceTypeId } from '@matter/types';
37
+ import { ServerNode, Endpoint } from '@matter/node';
38
+ import { AggregatorEndpoint } from '@matter/node/endpoints/aggregator';
39
+ import { BasicInformationServer } from '@matter/node/behaviors/basic-information';
40
+ import { BridgedDeviceBasicInformationServer } from '@matter/node/behaviors/bridged-device-basic-information';
41
+ // Matterbridge
42
+ import { copyDirectory, getIntParameter, getParameter, hasParameter, inspectError, isValidNumber, isValidString, parseVersionString, wait, withTimeout } from '@matterbridge/utils';
43
+ import { dev, MATTER_LOGGER_FILE, MATTER_STORAGE_NAME, plg, NODE_STORAGE_DIR, MATTERBRIDGE_LOGGER_FILE } from '@matterbridge/types';
44
+ import { BroadcastServer } from '@matterbridge/thread';
45
+ import { bridge } from './matterbridgeDeviceTypes.js';
46
+ import { toBaseDevice } from './deviceManager.js';
47
+ import { PluginManager } from './pluginManager.js';
48
+ import { addVirtualDevice } from './helpers.js';
49
+ /**
50
+ * Represents the Matter class.
51
+ */
52
+ export class MatterNode extends EventEmitter {
53
+ matterbridge;
54
+ pluginName;
55
+ device;
56
+ /** Matterbridge logger */
57
+ log = new AnsiLogger({ logName: 'MatterNode', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: "debug" /* LogLevel.DEBUG */ });
58
+ /** Matter logger */
59
+ matterLog = new AnsiLogger({ logName: 'MatterNode', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: "debug" /* LogLevel.DEBUG */ });
60
+ /** Matter environment default */
61
+ environment = Environment.default;
62
+ /** Matter storage id */
63
+ storeId;
64
+ /** Matter mdns service from environment default */
65
+ matterMdnsService;
66
+ /** Matter storage service from environment default */
67
+ matterStorageService;
68
+ /** Matter storage manager created with name 'Matterbridge' */
69
+ matterStorageManager;
70
+ /** Matter storage context created in the storage manager with name 'persist' */
71
+ matterStorageContext;
72
+ /** Matter mdns interface name e.g. 'eth0' or 'wlan0' or 'Wi-Fi' */
73
+ mdnsInterface;
74
+ /** Matter listeningAddressIpv4 address */
75
+ ipv4Address;
76
+ /** Matter listeningAddressIpv6 address */
77
+ ipv6Address;
78
+ /** Matter commissioning port It is incremented in childbridge mode. */
79
+ port;
80
+ /** Matter commissioning passcode. It is incremented in childbridge mode. */
81
+ passcode;
82
+ /** Matter commissioning discriminator. It is incremented in childbridge mode. */
83
+ discriminator;
84
+ /** Matter device certification */
85
+ certification;
86
+ /** Matter server node */
87
+ serverNode;
88
+ /** Matter aggregator node */
89
+ aggregatorNode;
90
+ // Default values for the aggregator node
91
+ aggregatorVendorId = VendorId(getIntParameter('vendorId') ?? 0xfff1);
92
+ aggregatorVendorName = getParameter('vendorName') ?? 'Matterbridge';
93
+ aggregatorProductId = getIntParameter('productId') ?? 0x8000;
94
+ aggregatorProductName = getParameter('productName') ?? 'Matterbridge aggregator';
95
+ aggregatorDeviceType = DeviceTypeId(getIntParameter('deviceType') ?? bridge.code);
96
+ aggregatorSerialNumber = getParameter('serialNumber');
97
+ aggregatorUniqueId = getParameter('uniqueId');
98
+ /** Advertising nodes map: time advertising started keyed by storeId */
99
+ advertisingNodes = new Map();
100
+ /** Plugins */
101
+ pluginManager;
102
+ /** Dependant MatterNodes keyed by device id */
103
+ dependantMatterNodes = new Map();
104
+ /** Broadcast server */
105
+ server;
106
+ debug = hasParameter('debug') || hasParameter('verbose');
107
+ verbose = hasParameter('verbose');
108
+ /**
109
+ * Creates an instance of the Matter class.
110
+ *
111
+ * @param {SharedMatterbridge} matterbridge - The shared matterbridge instance.
112
+ * @param {PluginName} [pluginName] - The plugin name (optional). If not provided, it is assumed to be the main matter node instance and all plugins are included.
113
+ * @param {MatterbridgeEndpoint} [device] - The matterbridge endpoint device (optional). It is used to create a server mode device.
114
+ */
115
+ constructor(matterbridge, pluginName, device) {
116
+ super();
117
+ this.matterbridge = matterbridge;
118
+ this.pluginName = pluginName;
119
+ this.device = device;
120
+ this.log.logNameColor = '\x1b[38;5;65m';
121
+ if (this.debug)
122
+ this.log.debug(`MatterNode ${this.pluginName ? 'for plugin ' + this.pluginName : 'bridge'} loading...`);
123
+ // Setup Matter parameters
124
+ this.port = matterbridge.port;
125
+ this.passcode = matterbridge.passcode;
126
+ this.discriminator = matterbridge.discriminator;
127
+ // Setup the broadcast server
128
+ this.server = new BroadcastServer('matter', this.log);
129
+ this.server.on('broadcast_message', this.msgHandler.bind(this));
130
+ if (this.verbose)
131
+ this.log.debug(`BroadcastServer is ready`);
132
+ // Ensure the matterbridge directory exists
133
+ fs.mkdirSync(matterbridge.matterbridgeDirectory, { recursive: true });
134
+ // Setup the plugin manager with thread server closed
135
+ this.pluginManager = new PluginManager(this.matterbridge);
136
+ this.pluginManager.logLevel = this.debug ? "debug" /* LogLevel.DEBUG */ : "info" /* LogLevel.INFO */;
137
+ // @ts-expect-error access private property
138
+ this.pluginManager.server.close();
139
+ if (this.verbose)
140
+ this.log.debug(`PluginManager is ready`);
141
+ // Setup the matter environment
142
+ this.environment.vars.set('log.level', MatterLogLevel.DEBUG);
143
+ this.environment.vars.set('log.format', MatterLogFormat.ANSI);
144
+ this.environment.vars.set('path.root', path.join(matterbridge.matterbridgeDirectory, MATTER_STORAGE_NAME));
145
+ this.environment.vars.set('runtime.signals', false);
146
+ this.environment.vars.set('runtime.exitcode', false);
147
+ if (this.verbose)
148
+ this.log.debug(`Matter Environment is ready`);
149
+ // Ensure MdnsService is registered in the default environment
150
+ this.matterMdnsService = new MdnsService(this.environment);
151
+ setImmediate(async () => {
152
+ await this.matterMdnsService?.construction.ready;
153
+ if (this.verbose)
154
+ this.log.debug(`Matter MdnsService is ready`);
155
+ });
156
+ // Setup the matterbridge logger
157
+ if (this.matterbridge.fileLogger) {
158
+ AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE), this.matterbridge.logLevel);
159
+ }
160
+ // Setup the matter logger
161
+ Logger.destinations.default.write = this.createDestinationMatterLogger();
162
+ const levels = ['debug', 'info', 'notice', 'warn', 'error', 'fatal'];
163
+ if (this.verbose)
164
+ this.log.debug(`Matter logLevel: ${levels[Logger.level]} fileLogger: ${matterbridge.matterFileLogger}.`);
165
+ if (this.debug)
166
+ this.log.debug(`MatterNode ${this.pluginName ? 'for plugin ' + this.pluginName : 'bridge'} loaded`);
167
+ }
168
+ /**
169
+ * Handles incoming messages from the broadcast server.
170
+ *
171
+ * @param {WorkerMessage} msg - The incoming message.
172
+ */
173
+ async msgHandler(msg) {
174
+ if (this.server.isWorkerRequest(msg) && (msg.dst === 'all' || msg.dst === 'matter')) {
175
+ if (this.verbose)
176
+ this.log.debug(`Received broadcast request ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}: ${debugStringify(msg)}${db}`);
177
+ switch (msg.type) {
178
+ case 'get_log_level':
179
+ this.server.respond({ ...msg, result: { logLevel: this.log.logLevel } });
180
+ break;
181
+ case 'set_log_level':
182
+ this.log.logLevel = msg.params.logLevel;
183
+ this.server.respond({ ...msg, result: { logLevel: this.log.logLevel } });
184
+ break;
185
+ default:
186
+ if (this.verbose)
187
+ this.log.debug(`Unknown broadcast request ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}`);
188
+ }
189
+ }
190
+ if (this.server.isWorkerResponse(msg) && (msg.dst === 'all' || msg.dst === 'matter')) {
191
+ if (this.verbose)
192
+ this.log.debug(`Received broadcast response ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}: ${debugStringify(msg)}${db}`);
193
+ switch (msg.type) {
194
+ case 'get_log_level':
195
+ case 'set_log_level':
196
+ break;
197
+ default:
198
+ if (this.verbose)
199
+ this.log.debug(`Unknown broadcast response ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}`);
200
+ }
201
+ }
202
+ }
203
+ /**
204
+ * Destroys the Matter instance.
205
+ * It closes the mDNS service and the broadcast server.
206
+ *
207
+ * @param {boolean} closeMdns - Whether to close the mDNS service. Default is true.
208
+ * @returns {Promise<void>} A promise that resolves when the instance is destroyed.
209
+ */
210
+ async destroy(closeMdns = true) {
211
+ if (this.verbose)
212
+ this.log.debug(`Destroying MatterNode instance for ${this.storeId}...`);
213
+ // Close mDNS service
214
+ if (closeMdns) {
215
+ if (this.verbose)
216
+ this.log.debug(`Closing Matter MdnsService for ${this.storeId}...`);
217
+ await this.matterMdnsService?.close();
218
+ if (this.verbose)
219
+ this.log.debug(`Closed Matter MdnsService for ${this.storeId}`);
220
+ }
221
+ // Close the plugin manager
222
+ this.pluginManager.destroy();
223
+ // Close the broadcast server
224
+ this.server.close();
225
+ // Yield to the Node.js event loop to allow all resources to be released
226
+ await this.yieldToNode();
227
+ if (this.verbose)
228
+ this.log.debug(`Destroyed MatterNode instance for ${this.storeId}`);
229
+ }
230
+ async create() {
231
+ this.log.info('Creating Matter node...');
232
+ // Start matter storage
233
+ await this.startMatterStorage();
234
+ // Load plugins from storage
235
+ // @ts-expect-error access private property
236
+ this.pluginManager.matterbridge.nodeStorage = new NodeStorageManager({
237
+ dir: path.join(this.matterbridge.matterbridgeDirectory, NODE_STORAGE_DIR),
238
+ writeQueue: false,
239
+ expiredInterval: undefined,
240
+ logging: false,
241
+ });
242
+ // @ts-expect-error access private property
243
+ this.pluginManager.matterbridge.nodeContext = await this.pluginManager.matterbridge.nodeStorage.createStorage('matterbridge');
244
+ await this.pluginManager.loadFromStorage();
245
+ // Create Matter node for a server mode device
246
+ if (this.pluginName && this.device && this.device.deviceName) {
247
+ this.log.debug(`Creating MatterNode instance for server node device ${CYAN}${this.device.deviceName}${db}...`);
248
+ await this.createDeviceServerNode(this.pluginName, this.device);
249
+ this.log.debug(`Created MatterNode instance for server node device ${CYAN}${this.device.deviceName}${db}`);
250
+ this.emit('ready', this.device.deviceName.replace(/[ .]/g, ''));
251
+ return;
252
+ }
253
+ if (!this.pluginName) {
254
+ this.log.debug('Creating MatterNode instance for all plugins...');
255
+ await this.createMatterbridgeServerNode();
256
+ // Load all enabled plugins
257
+ this.log.debug('Loading all plugins...');
258
+ const loadPromises = [];
259
+ for (const plugin of this.pluginManager.array().filter((p) => p.enabled)) {
260
+ loadPromises.push(this.pluginManager.load(plugin));
261
+ }
262
+ await Promise.all(loadPromises);
263
+ this.log.debug('Loaded all plugins');
264
+ this.log.debug('Created MatterNode instance for all plugins');
265
+ this.emit('ready', 'Matterbridge');
266
+ }
267
+ else {
268
+ this.log.debug(`Creating MatterNode instance for plugin ${CYAN}${this.pluginName}${db}...`);
269
+ // Load only the specified plugin
270
+ this.log.debug(`Loading plugin ${CYAN}${this.pluginName}${db}...`);
271
+ await this.pluginManager.load(this.pluginName);
272
+ this.log.debug(`Loaded plugin ${CYAN}${this.pluginName}${db}`);
273
+ this.log.debug(`Created MatterNode instance for plugin ${CYAN}${this.pluginName}${db}`);
274
+ this.emit('ready', this.pluginName);
275
+ }
276
+ this.log.info('Created Matter node');
277
+ await this.yieldToNode();
278
+ }
279
+ async start() {
280
+ if (!this.serverNode && !this.pluginName)
281
+ throw new Error('Matter server node not created yet. Call create() first.');
282
+ this.log.info('Starting MatterNode...');
283
+ // Start Matter node for a server mode device
284
+ if (this.pluginName && this.device && this.device.deviceName) {
285
+ // Start the server node
286
+ this.log.debug(`Starting MatterNode for server device ${this.pluginName}:${this.device.deviceName}...`);
287
+ await this.startServerNode();
288
+ this.log.debug(`Started MatterNode for server device ${this.pluginName}:${this.device.deviceName}`);
289
+ return;
290
+ }
291
+ if (!this.pluginName) {
292
+ // Start all loaded plugins
293
+ this.log.debug('Starting all plugins...');
294
+ const startPromises = [];
295
+ for (const plugin of this.pluginManager.array().filter((p) => p.enabled && p.loaded)) {
296
+ startPromises.push(this.pluginManager.start(plugin, 'Starting MatterNode'));
297
+ }
298
+ await Promise.all(startPromises);
299
+ this.log.debug('Started all plugins');
300
+ // Start the server node
301
+ this.log.debug('Starting MatterNode for all plugins...');
302
+ await this.startServerNode();
303
+ this.log.debug('Started MatterNode for all plugins');
304
+ // Configure all loaded plugins
305
+ this.log.debug('Configuring all plugins...');
306
+ const configurePromises = [];
307
+ for (const plugin of this.pluginManager.array().filter((p) => p.enabled && p.started)) {
308
+ configurePromises.push(this.pluginManager.configure(plugin));
309
+ }
310
+ await Promise.all(configurePromises);
311
+ this.log.debug('Configured all plugins');
312
+ }
313
+ else {
314
+ // Start the loaded plugin
315
+ await this.pluginManager.start(this.pluginName, 'Starting MatterNode');
316
+ // Start the server node
317
+ this.log.debug(`Starting MatterNode for plugin ${this.pluginName}...`);
318
+ await this.startServerNode();
319
+ this.log.debug(`Started MatterNode for plugin ${this.pluginName}`);
320
+ // Configure the plugin
321
+ await this.pluginManager.configure(this.pluginName);
322
+ }
323
+ // Start the dependant MatterNodes
324
+ this.log.debug(`Starting dependant MatterNodes...`);
325
+ for (const dependantMatterNode of this.dependantMatterNodes.values()) {
326
+ await dependantMatterNode.start();
327
+ }
328
+ this.log.debug(`Started dependant MatterNodes`);
329
+ this.log.info('Started MatterNode');
330
+ await this.yieldToNode();
331
+ }
332
+ async stop() {
333
+ if (!this.serverNode)
334
+ throw new Error('Matter server node not created yet. Call create() first.');
335
+ this.log.info('Stopping MatterNode...');
336
+ // Stop Matter node for a server mode device
337
+ if (this.pluginName && this.device && this.device.deviceName) {
338
+ // Stop the server node
339
+ this.log.debug(`Stopping MatterNode for server device ${this.pluginName}:${this.device.deviceName}...`);
340
+ await this.stopServerNode();
341
+ this.serverNode = undefined;
342
+ this.aggregatorNode = undefined;
343
+ await this.stopMatterStorage();
344
+ await this.destroy(false); // Do not close mDNS since it is shared
345
+ this.log.debug(`Stopped MatterNode for server device ${this.pluginName}:${this.device.deviceName}`);
346
+ this.log.info('Stopped MatterNode');
347
+ await this.yieldToNode();
348
+ return;
349
+ }
350
+ if (!this.pluginName) {
351
+ this.log.debug('Stopping all plugins...');
352
+ const shutdownPromises = [];
353
+ for (const plugin of this.pluginManager.array().filter((p) => p.enabled && p.started)) {
354
+ shutdownPromises.push(this.pluginManager.shutdown(plugin, 'Stopping MatterNode'));
355
+ }
356
+ await Promise.all(shutdownPromises);
357
+ this.log.debug('Stopped all plugins');
358
+ }
359
+ else {
360
+ this.log.debug(`Stopping plugin ${this.pluginName}...`);
361
+ await this.pluginManager.shutdown(this.pluginName, 'Stopping MatterNode');
362
+ this.log.debug(`Stopped plugin ${this.pluginName}`);
363
+ }
364
+ // Stop the dependant MatterNodes
365
+ this.log.debug(`Stopping dependant MatterNodes...`);
366
+ for (const dependantMatterNode of this.dependantMatterNodes.values()) {
367
+ await dependantMatterNode.stop();
368
+ }
369
+ this.log.debug(`Stopped dependant MatterNodes`);
370
+ await this.stopServerNode();
371
+ this.serverNode = undefined;
372
+ this.aggregatorNode = undefined;
373
+ await this.stopMatterStorage();
374
+ this.log.info('Stopped MatterNode');
375
+ await this.yieldToNode();
376
+ }
377
+ /**
378
+ * Creates a MatterLogger function to show the matter.js log messages in AnsiLogger (console and frontend).
379
+ * It also logs to file (matter.log) if fileLogger is true.
380
+ *
381
+ * @returns {(text: string, message: Diagnostic.Message) => void} The MatterLogger function to be used in Logger.destinations.default.write.
382
+ */
383
+ createDestinationMatterLogger() {
384
+ this.matterLog.logNameColor = '\x1b[34m'; // Blue matter.js Logger
385
+ if (this.matterbridge.matterFileLogger) {
386
+ this.matterLog.logFilePath = path.join(this.matterbridge.matterbridgeDirectory, MATTER_LOGGER_FILE);
387
+ }
388
+ return (text, message) => {
389
+ // 2024-08-21 08:55:19.488 DEBUG InteractionMessenger Sending DataReport chunk with 28 attributes and 0 events: 1004 bytes
390
+ const logger = text.slice(44, 44 + 20).trim();
391
+ const msg = text.slice(65);
392
+ this.matterLog.logName = logger;
393
+ this.matterLog.log(MatterLogLevel.names[message.level], msg);
394
+ };
395
+ }
396
+ /**
397
+ * Starts the matter storage with name Matterbridge and performs a backup.
398
+ *
399
+ * @returns {Promise<void>} - A promise that resolves when the storage is started.
400
+ */
401
+ async startMatterStorage() {
402
+ // Setup Matter storage
403
+ this.log.info(`Starting matter node storage...`);
404
+ this.matterStorageService = this.environment.get(StorageService);
405
+ this.log.info(`Started matter node storage in ${CYAN}${this.matterStorageService.location}${nf}`);
406
+ // Backup matter storage since it is created/opened correctly
407
+ await this.backupMatterStorage(path.join(this.matterbridge.matterbridgeDirectory, MATTER_STORAGE_NAME), path.join(this.matterbridge.matterbridgeDirectory, MATTER_STORAGE_NAME + '.backup'));
408
+ }
409
+ /**
410
+ * Makes a backup copy of the specified matter storage directory.
411
+ *
412
+ * @param {string} storageName - The name of the storage directory to be backed up.
413
+ * @param {string} backupName - The name of the backup directory to be created.
414
+ * @private
415
+ * @returns {Promise<void>} A promise that resolves when the has been done.
416
+ */
417
+ async backupMatterStorage(storageName, backupName) {
418
+ this.log.info(`Creating matter node storage backup from ${CYAN}${storageName}${nf} to ${CYAN}${backupName}${nf}...`);
419
+ try {
420
+ await copyDirectory(storageName, backupName);
421
+ this.log.info('Created matter node storage backup');
422
+ }
423
+ catch (error) {
424
+ // istanbul ignore next if
425
+ if (error instanceof Error && error?.code === 'ENOENT') {
426
+ this.log.info(`No matter node storage found to backup from ${CYAN}${storageName}${nf} to ${CYAN}${backupName}${nf}`);
427
+ }
428
+ else {
429
+ this.log.error(`Error creating matter node storage backup from ${storageName} to ${backupName}:`, error);
430
+ }
431
+ }
432
+ }
433
+ /**
434
+ * Stops the matter storage.
435
+ *
436
+ * @returns {Promise<void>} A promise that resolves when the storage is stopped.
437
+ */
438
+ async stopMatterStorage() {
439
+ this.log.info('Closing matter node storage...');
440
+ await this.matterStorageManager?.close();
441
+ this.matterStorageService = undefined;
442
+ this.matterStorageManager = undefined;
443
+ this.matterStorageContext = undefined;
444
+ this.log.info('Closed matter node storage');
445
+ this.emit('closed');
446
+ }
447
+ /**
448
+ * Creates a server node storage context.
449
+ *
450
+ * @param {string} storeId - The storeId.
451
+ * @param {string} deviceName - The name of the device.
452
+ * @param {DeviceTypeId} deviceType - The device type of the device.
453
+ * @param {VendorId} vendorId - The vendor ID.
454
+ * @param {string} vendorName - The vendor name.
455
+ * @param {number} productId - The product ID.
456
+ * @param {string} productName - The product name.
457
+ * @param {string} [serialNumber] - The serial number of the device (optional).
458
+ * @param {string} [uniqueId] - The unique ID of the device (optional).
459
+ * @returns {Promise<StorageContext>} The storage context for the commissioning server.
460
+ * @throws {Error} If the storage service is not initialized.
461
+ */
462
+ async createServerNodeContext(storeId, deviceName, deviceType, vendorId, vendorName, productId, productName, serialNumber, uniqueId) {
463
+ if (!this.matterStorageService) {
464
+ throw new Error('No storage service initialized');
465
+ }
466
+ const { randomBytes } = await import('node:crypto');
467
+ this.log.info(`Creating server node storage context "${storeId}.persist" for ${storeId}...`);
468
+ const storageManager = await this.matterStorageService.open(storeId);
469
+ const storageContext = storageManager.createContext('persist');
470
+ const random = randomBytes(8).toString('hex');
471
+ await storageContext.set('storeId', storeId);
472
+ await storageContext.set('deviceName', deviceName);
473
+ await storageContext.set('deviceType', deviceType);
474
+ await storageContext.set('vendorId', vendorId);
475
+ await storageContext.set('vendorName', vendorName.slice(0, 32));
476
+ await storageContext.set('productId', productId);
477
+ await storageContext.set('productName', productName.slice(0, 32));
478
+ await storageContext.set('nodeLabel', productName.slice(0, 32));
479
+ await storageContext.set('productLabel', productName.slice(0, 32));
480
+ await storageContext.set('serialNumber', await storageContext.get('serialNumber', serialNumber ? serialNumber.slice(0, 32) : 'SN' + random));
481
+ await storageContext.set('uniqueId', await storageContext.get('uniqueId', uniqueId ? uniqueId.slice(0, 32) : 'UI' + random));
482
+ await storageContext.set('softwareVersion', isValidNumber(parseVersionString(this.matterbridge.matterbridgeVersion), 0, UINT32_MAX) ? parseVersionString(this.matterbridge.matterbridgeVersion) : 1);
483
+ await storageContext.set('softwareVersionString', isValidString(this.matterbridge.matterbridgeVersion, 5, 64) ? this.matterbridge.matterbridgeVersion : '1.0.0');
484
+ await storageContext.set('hardwareVersion', isValidNumber(parseVersionString(this.matterbridge.systemInformation.osRelease), 0, UINT16_MAX) ? parseVersionString(this.matterbridge.systemInformation.osRelease) : 1);
485
+ await storageContext.set('hardwareVersionString', isValidString(this.matterbridge.systemInformation.osRelease, 5, 64) ? this.matterbridge.systemInformation.osRelease : '1.0.0');
486
+ this.log.debug(`Created server node storage context "${storeId}.persist" for ${storeId}:`);
487
+ this.log.debug(`- storeId: ${await storageContext.get('storeId')}`);
488
+ this.log.debug(`- deviceName: ${await storageContext.get('deviceName')}`);
489
+ this.log.debug(`- deviceType: ${await storageContext.get('deviceType')}(0x${(await storageContext.get('deviceType'))?.toString(16).padStart(4, '0')})`);
490
+ this.log.debug(`- vendorId: ${await storageContext.get('vendorId')}`);
491
+ this.log.debug(`- vendorName: ${await storageContext.get('vendorName')}`);
492
+ this.log.debug(`- productId: ${await storageContext.get('productId')}`);
493
+ this.log.debug(`- productName: ${await storageContext.get('productName')}`);
494
+ this.log.debug(`- nodeLabel: ${await storageContext.get('nodeLabel')}`);
495
+ this.log.debug(`- productLabel: ${await storageContext.get('productLabel')}`);
496
+ this.log.debug(`- serialNumber: ${await storageContext.get('serialNumber')}`);
497
+ this.log.debug(`- uniqueId: ${await storageContext.get('uniqueId')}`);
498
+ this.log.debug(`- softwareVersion: ${await storageContext.get('softwareVersion')}`);
499
+ this.log.debug(`- softwareVersionString: ${await storageContext.get('softwareVersionString')}`);
500
+ this.log.debug(`- hardwareVersion: ${await storageContext.get('hardwareVersion')}`);
501
+ this.log.debug(`- hardwareVersionString: ${await storageContext.get('hardwareVersionString')}`);
502
+ return storageContext;
503
+ }
504
+ /**
505
+ * Creates a server node.
506
+ *
507
+ * @param {number} [port] - The port number for the server node. Defaults to 5540.
508
+ * @param {number} [passcode] - The passcode for the server node. Defaults to 20242025.
509
+ * @param {number} [discriminator] - The discriminator for the server node. Defaults to 3850.
510
+ * @returns {Promise<ServerNode<ServerNode.RootEndpoint>>} A promise that resolves to the created server node.
511
+ * @throws {Error} If the matter storage context is not created yet.
512
+ */
513
+ async createServerNode(port = 5540, passcode = 20252026, discriminator = 3850) {
514
+ if (!this.matterStorageContext) {
515
+ throw new Error('Matter server node context not created yet. Call createServerNodeContext() first.');
516
+ }
517
+ const storeId = await this.matterStorageContext.get('storeId');
518
+ this.log.notice(`Creating server node for ${storeId} on port ${port} with passcode ${passcode} and discriminator ${discriminator}...`);
519
+ /**
520
+ * Create a Matter ServerNode, which contains the Root Endpoint and all relevant data and configuration
521
+ */
522
+ const serverNode = await ServerNode.create({
523
+ // Required: Give the Node a unique ID which is used to store the state of this node
524
+ id: storeId,
525
+ // Provide the environment to run this node in
526
+ environment: this.environment,
527
+ // Provide Network relevant configuration like the port
528
+ network: {
529
+ listeningAddressIpv4: this.ipv4Address,
530
+ listeningAddressIpv6: this.ipv6Address,
531
+ port,
532
+ },
533
+ // Provide the certificate for the device
534
+ operationalCredentials: {
535
+ certification: this.certification,
536
+ },
537
+ // Provide Commissioning relevant settings
538
+ commissioning: {
539
+ passcode,
540
+ discriminator,
541
+ },
542
+ // Provide Node announcement settings
543
+ productDescription: {
544
+ name: await this.matterStorageContext.get('deviceName'),
545
+ deviceType: DeviceTypeId(await this.matterStorageContext.get('deviceType')),
546
+ vendorId: VendorId(await this.matterStorageContext.get('vendorId')),
547
+ productId: await this.matterStorageContext.get('productId'),
548
+ },
549
+ // Provide defaults for the BasicInformation cluster on the Root endpoint
550
+ basicInformation: {
551
+ vendorId: VendorId(await this.matterStorageContext.get('vendorId')),
552
+ vendorName: await this.matterStorageContext.get('vendorName'),
553
+ productId: await this.matterStorageContext.get('productId'),
554
+ productName: await this.matterStorageContext.get('productName'),
555
+ productLabel: await this.matterStorageContext.get('productLabel'),
556
+ nodeLabel: await this.matterStorageContext.get('nodeLabel'),
557
+ serialNumber: await this.matterStorageContext.get('serialNumber'),
558
+ uniqueId: await this.matterStorageContext.get('uniqueId'),
559
+ softwareVersion: await this.matterStorageContext.get('softwareVersion'),
560
+ softwareVersionString: await this.matterStorageContext.get('softwareVersionString'),
561
+ hardwareVersion: await this.matterStorageContext.get('hardwareVersion'),
562
+ hardwareVersionString: await this.matterStorageContext.get('hardwareVersionString'),
563
+ reachable: true,
564
+ },
565
+ });
566
+ /**
567
+ * This event is triggered when the device is initially commissioned successfully.
568
+ * This means: It is added to the first fabric.
569
+ */
570
+ serverNode.lifecycle.commissioned.on(() => {
571
+ this.log.notice(`Server node for ${storeId} was initially commissioned successfully!`);
572
+ this.advertisingNodes.delete(storeId);
573
+ this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...this.getServerNodeData(serverNode) } } });
574
+ });
575
+ /** This event is triggered when all fabrics are removed from the device, usually it also does a factory reset then. */
576
+ serverNode.lifecycle.decommissioned.on(() => {
577
+ this.log.notice(`Server node for ${storeId} was fully decommissioned successfully!`);
578
+ this.advertisingNodes.delete(storeId);
579
+ this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...this.getServerNodeData(serverNode) } } });
580
+ this.server.request({ type: 'frontend_snackbarmessage', src: 'matter', dst: 'frontend', params: { message: `${storeId} is offline`, timeout: 5, severity: 'warning' } });
581
+ });
582
+ /** This event is triggered when the device went online. This means that it is discoverable in the network. */
583
+ serverNode.lifecycle.online.on(async () => {
584
+ this.log.notice(`Server node for ${storeId} is online`);
585
+ if (!serverNode.lifecycle.isCommissioned) {
586
+ this.log.notice(`Server node for ${storeId} is not commissioned. Pair to commission ...`);
587
+ this.advertisingNodes.set(storeId, Date.now());
588
+ const { qrPairingCode, manualPairingCode } = serverNode.state.commissioning.pairingCodes;
589
+ this.log.notice(`QR Code URL: https://project-chip.github.io/connectedhomeip/qrcode.html?data=${qrPairingCode}`);
590
+ this.log.notice(`Manual pairing code: ${manualPairingCode}`);
591
+ }
592
+ else {
593
+ this.log.notice(`Server node for ${storeId} is already commissioned. Waiting for controllers to connect ...`);
594
+ this.advertisingNodes.delete(storeId);
595
+ }
596
+ this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...this.getServerNodeData(serverNode) } } });
597
+ this.server.request({ type: 'frontend_snackbarmessage', src: 'matter', dst: 'frontend', params: { message: `${storeId} is online`, timeout: 5, severity: 'success' } });
598
+ this.emit('online', storeId);
599
+ });
600
+ /** This event is triggered when the device went offline. it is not longer discoverable or connectable in the network. */
601
+ serverNode.lifecycle.offline.on(() => {
602
+ this.log.notice(`Server node for ${storeId} is offline`);
603
+ this.advertisingNodes.delete(storeId);
604
+ this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...this.getServerNodeData(serverNode) } } });
605
+ this.server.request({ type: 'frontend_snackbarmessage', src: 'matter', dst: 'frontend', params: { message: `${storeId} is offline`, timeout: 5, severity: 'warning' } });
606
+ this.emit('offline', storeId);
607
+ });
608
+ /**
609
+ * This event is triggered when a fabric is added, removed or updated on the device. Use this if more granular
610
+ * information is needed.
611
+ */
612
+ serverNode.events.commissioning.fabricsChanged.on((fabricIndex, fabricAction) => {
613
+ let action = '';
614
+ switch (fabricAction) {
615
+ case 'added':
616
+ this.advertisingNodes.delete(storeId); // The advertising stops when a fabric is added
617
+ action = 'added';
618
+ break;
619
+ case 'deleted':
620
+ action = 'removed';
621
+ break;
622
+ case 'updated':
623
+ action = 'updated';
624
+ break;
625
+ }
626
+ this.log.notice(`Commissioned fabric index ${fabricIndex} ${action} on server node for ${storeId}: ${debugStringify(serverNode.state.commissioning.fabrics[fabricIndex])}`);
627
+ this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...this.getServerNodeData(serverNode) } } });
628
+ });
629
+ /**
630
+ * This event is triggered when an operative new session was opened by a Controller.
631
+ * It is not triggered for the initial commissioning process, just afterwards for real connections.
632
+ */
633
+ serverNode.events.sessions.opened.on((session) => {
634
+ this.log.notice(`Session opened on server node for ${storeId}: ${debugStringify(session)}`);
635
+ this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...this.getServerNodeData(serverNode) } } });
636
+ });
637
+ /**
638
+ * This event is triggered when an operative session is closed by a Controller or because the Device goes offline.
639
+ */
640
+ serverNode.events.sessions.closed.on((session) => {
641
+ this.log.notice(`Session closed on server node for ${storeId}: ${debugStringify(session)}`);
642
+ this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...this.getServerNodeData(serverNode) } } });
643
+ });
644
+ /** This event is triggered when a subscription gets added or removed on an operative session. */
645
+ serverNode.events.sessions.subscriptionsChanged.on((session) => {
646
+ this.log.notice(`Session subscriptions changed on server node for ${storeId}: ${debugStringify(session)}`);
647
+ this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...this.getServerNodeData(serverNode) } } });
648
+ });
649
+ this.storeId = storeId;
650
+ this.log.info(`Created server node for ${this.storeId}`);
651
+ return serverNode;
652
+ }
653
+ /**
654
+ * Gets the matter serializable data of the specified server node.
655
+ *
656
+ * @param {ServerNode} [serverNode] - The server node to start.
657
+ * @returns {ApiMatter} The serializable data of the server node.
658
+ */
659
+ getServerNodeData(serverNode) {
660
+ const advertiseTime = this.advertisingNodes.get(serverNode.id) || 0;
661
+ return {
662
+ id: serverNode.id,
663
+ online: serverNode.lifecycle.isOnline,
664
+ commissioned: serverNode.state.commissioning.commissioned,
665
+ advertising: advertiseTime > Date.now() - 15 * 60 * 1000,
666
+ advertiseTime,
667
+ windowStatus: serverNode.state.administratorCommissioning.windowStatus,
668
+ qrPairingCode: serverNode.state.commissioning.pairingCodes.qrPairingCode,
669
+ manualPairingCode: serverNode.state.commissioning.pairingCodes.manualPairingCode,
670
+ fabricInformations: this.sanitizeFabricInformations(Object.values(serverNode.state.commissioning.fabrics)),
671
+ sessionInformations: this.sanitizeSessionInformation(Object.values(serverNode.state.sessions.sessions)),
672
+ serialNumber: serverNode.state.basicInformation.serialNumber,
673
+ };
674
+ }
675
+ /**
676
+ * Starts the specified server node.
677
+ *
678
+ * @param {number} [timeout] - The timeout in milliseconds for starting the server node. Defaults to 30 seconds.
679
+ * @returns {Promise<void>} A promise that resolves when the server node has started.
680
+ * @throws {Error} If the server node is not created yet.
681
+ */
682
+ async startServerNode(timeout = 30000) {
683
+ if (!this.serverNode) {
684
+ throw new Error('Matter server node not created yet. Call create() first.');
685
+ }
686
+ this.log.notice(`Starting ${this.serverNode.id} server node...`);
687
+ try {
688
+ await withTimeout(this.serverNode.start(), timeout);
689
+ this.log.notice(`Started ${this.serverNode.id} server node`);
690
+ }
691
+ catch (error) {
692
+ // istanbul ignore next
693
+ this.log.error(`Failed to start ${this.serverNode.id} server node: ${error instanceof Error ? error.message : error}`);
694
+ }
695
+ }
696
+ /**
697
+ * Stops the specified server node.
698
+ *
699
+ * @param {number} [timeout] - The timeout in milliseconds for stopping the server node. Defaults to 30 seconds.
700
+ * @returns {Promise<void>} A promise that resolves when the server node has stopped.
701
+ * @throws {Error} If the server node is not created yet.
702
+ */
703
+ async stopServerNode(timeout = 30000) {
704
+ if (!this.serverNode) {
705
+ throw new Error('Matter server node not created yet. Call create() first.');
706
+ }
707
+ this.log.notice(`Closing ${this.serverNode.id} server node`);
708
+ try {
709
+ await withTimeout(this.serverNode.close(), timeout);
710
+ this.log.info(`Closed ${this.serverNode.id} server node`);
711
+ }
712
+ catch (error) {
713
+ // istanbul ignore next
714
+ this.log.error(`Failed to close ${this.serverNode.id} server node: ${error instanceof Error ? error.message : error}`);
715
+ }
716
+ }
717
+ /**
718
+ * Creates an aggregator node with the specified storage context.
719
+ *
720
+ * @returns {Promise<Endpoint<AggregatorEndpoint>>} A promise that resolves to the created aggregator node.
721
+ * @throws {Error} If the matter storage context is not created yet.
722
+ */
723
+ async createAggregatorNode() {
724
+ if (!this.matterStorageContext) {
725
+ throw new Error('Matter server node context not created yet. Call createServerNodeContext() first.');
726
+ }
727
+ this.log.notice(`Creating ${await this.matterStorageContext.get('storeId')} aggregator...`);
728
+ const aggregatorNode = new Endpoint(AggregatorEndpoint, { id: `${await this.matterStorageContext.get('storeId')}` });
729
+ this.log.info(`Created ${await this.matterStorageContext.get('storeId')} aggregator`);
730
+ return aggregatorNode;
731
+ }
732
+ /**
733
+ * Creates the matterbridge server node.
734
+ *
735
+ * @returns {Promise<ServerNode<ServerNode.RootEndpoint>>} A promise that resolves to the created matterbridge server node.
736
+ */
737
+ async createMatterbridgeServerNode() {
738
+ this.log.debug(`Creating ${plg}Matterbridge${db} server node...`);
739
+ this.matterStorageContext = await this.createServerNodeContext('Matterbridge', // storeId
740
+ 'Matterbridge', // deviceName
741
+ this.aggregatorDeviceType, this.aggregatorVendorId, this.aggregatorVendorName, this.aggregatorProductId, this.aggregatorProductName, this.aggregatorSerialNumber, this.aggregatorUniqueId);
742
+ this.serverNode = await this.createServerNode(this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
743
+ this.aggregatorNode = await this.createAggregatorNode();
744
+ this.log.debug(`Adding ${plg}Matterbridge${db} aggregator node...`);
745
+ await this.serverNode.add(this.aggregatorNode);
746
+ this.log.debug(`Added ${plg}Matterbridge${db} aggregator node`);
747
+ await this.serverNode.construction.ready;
748
+ await this.aggregatorNode.construction.ready;
749
+ this.log.debug(`Created ${plg}Matterbridge${db} server node`);
750
+ return this.serverNode;
751
+ }
752
+ /**
753
+ * Creates and configures the server node for an accessory plugin for a given device.
754
+ *
755
+ * @param {Plugin | PluginName} plugin - The plugin to configure.
756
+ * @param {MatterbridgeEndpoint} device - The device to associate with the plugin.
757
+ * @returns {Promise<ServerNode<ServerNode.RootEndpoint> | undefined>} A promise that resolves to the server node for the accessory plugin.
758
+ */
759
+ async createAccessoryPlugin(plugin, device) {
760
+ if (typeof plugin === 'string') {
761
+ const _plugin = this.pluginManager.get(plugin);
762
+ if (!_plugin)
763
+ throw new Error(`Plugin ${BLUE}${this.pluginName}${er} not found`);
764
+ plugin = _plugin;
765
+ }
766
+ if (!plugin.locked && device.deviceType && device.deviceName && device.vendorId && device.vendorName && device.productId && device.productName) {
767
+ plugin.locked = true;
768
+ this.log.debug(`Creating accessory plugin ${plg}${plugin.name}${db} server node...`);
769
+ this.matterStorageContext = await this.createServerNodeContext(plugin.name, device.deviceName, DeviceTypeId(device.deviceType), VendorId(device.vendorId), device.vendorName, device.productId, device.productName);
770
+ this.serverNode = await this.createServerNode(this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
771
+ this.log.debug(`Adding ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db} to ${plg}${plugin.name}${db} server node...`);
772
+ await this.serverNode.add(device);
773
+ this.log.debug(`Added ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db} to ${plg}${plugin.name}${db} server node`);
774
+ await this.serverNode.construction.ready;
775
+ await device.construction.ready;
776
+ this.log.debug(`Created accessory plugin ${plg}${plugin.name}${db} server node`);
777
+ }
778
+ return this.serverNode;
779
+ }
780
+ /**
781
+ * Creates and configures the server node and the aggregator node for a dynamic plugin.
782
+ *
783
+ * @param {Plugin | PluginName} plugin - The plugin to configure.
784
+ * @returns {Promise<ServerNode<ServerNode.RootEndpoint> | undefined>} A promise that resolves to the server node for the dynamic plugin.
785
+ */
786
+ async createDynamicPlugin(plugin) {
787
+ if (typeof plugin === 'string') {
788
+ const _plugin = this.pluginManager.get(plugin);
789
+ if (!_plugin)
790
+ throw new Error(`Plugin ${BLUE}${this.pluginName}${er} not found`);
791
+ plugin = _plugin;
792
+ }
793
+ if (!plugin.locked) {
794
+ plugin.locked = true;
795
+ this.log.debug(`Creating dynamic plugin ${plg}${plugin.name}${db} server node...`);
796
+ this.matterStorageContext = await this.createServerNodeContext(plugin.name, 'Matterbridge', this.aggregatorDeviceType, this.aggregatorVendorId, this.aggregatorVendorName, this.aggregatorProductId, plugin.description);
797
+ this.serverNode = await this.createServerNode(this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
798
+ this.log.debug(`Creating dynamic plugin ${plg}${plugin.name}${db} aggregator node...`);
799
+ this.aggregatorNode = await this.createAggregatorNode();
800
+ this.log.debug(`Adding dynamic plugin ${plg}${plugin.name}${db} aggregator node...`);
801
+ await this.serverNode.add(this.aggregatorNode);
802
+ this.log.debug(`Added dynamic plugin ${plg}${plugin.name}${db} aggregator node`);
803
+ await this.serverNode.construction.ready;
804
+ await this.aggregatorNode.construction.ready;
805
+ this.log.debug(`Created dynamic plugin ${plg}${plugin.name}${db} server node`);
806
+ }
807
+ return this.serverNode;
808
+ }
809
+ /**
810
+ * Creates and configures the server node for a single not bridged device.
811
+ *
812
+ * @param {Plugin | PluginName} plugin - The plugin to configure.
813
+ * @param {MatterbridgeEndpoint} device - The device to associate with the plugin.
814
+ * @returns {Promise<ServerNode<ServerNode.RootEndpoint> | undefined>} A promise that resolves to the server node for the device with mode server.
815
+ */
816
+ async createDeviceServerNode(plugin, device) {
817
+ if (typeof plugin === 'string') {
818
+ const _plugin = this.pluginManager.get(plugin);
819
+ if (!_plugin)
820
+ throw new Error(`Plugin ${BLUE}${this.pluginName}${er} not found`);
821
+ plugin = _plugin;
822
+ }
823
+ if (device.mode === 'server' && device.deviceType && device.deviceName && device.vendorId && device.vendorName && device.productId && device.productName) {
824
+ this.log.debug(`Creating device ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db} server node...`);
825
+ this.matterStorageContext = await this.createServerNodeContext(device.deviceName.replace(/[ .]/g, ''), device.deviceName, DeviceTypeId(device.deviceType), VendorId(device.vendorId), device.vendorName, device.productId, device.productName);
826
+ this.serverNode = await this.createServerNode(this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
827
+ this.log.debug(`Adding ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db} to server node...`);
828
+ await this.serverNode.add(device);
829
+ this.log.debug(`Added ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db} to server node`);
830
+ await this.serverNode.construction.ready;
831
+ await device.construction.ready;
832
+ this.log.debug(`Created device ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db} server node`);
833
+ }
834
+ return this.serverNode;
835
+ }
836
+ /**
837
+ * Adds a MatterbridgeEndpoint to the specified plugin.
838
+ *
839
+ * @param {string} pluginName - The name of the plugin.
840
+ * @param {MatterbridgeEndpoint} device - The device to add as a bridged endpoint.
841
+ * @returns {Promise<MatterbridgeEndpoint | undefined>} A promise that resolves to the added bridged endpoint, or undefined if there was an error.
842
+ */
843
+ async addBridgedEndpoint(pluginName, device) {
844
+ // Check if the plugin is registered
845
+ const plugin = this.pluginManager.get(pluginName);
846
+ if (!plugin)
847
+ throw new Error(`Error adding bridged endpoint ${plg}${pluginName}${er}:${dev}${device.deviceName}${er} (${zb}${device.name}${er}): plugin not found`);
848
+ if (device.mode === 'server') {
849
+ try {
850
+ this.log.debug(`Creating MatterNode for device ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} (${zb}${device.name}${db})...`);
851
+ // Create the MatterNode to manage the device
852
+ const matterNode = new MatterNode(this.matterbridge, pluginName, device);
853
+ matterNode.port = this.port ? this.port++ : undefined;
854
+ matterNode.passcode = this.passcode ? this.passcode++ : undefined;
855
+ matterNode.discriminator = this.discriminator ? this.discriminator++ : undefined;
856
+ this.dependantMatterNodes.set(device.id, matterNode);
857
+ await matterNode.create();
858
+ this.log.debug(`Created MatterNode for device ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} (${zb}${device.name}${db})`);
859
+ }
860
+ catch (error) {
861
+ inspectError(this.log, `Error creating MatterNode for device ${plg}${pluginName}${er}:${dev}${device.deviceName}${er} (${zb}${device.name}${er})`, error);
862
+ return;
863
+ }
864
+ }
865
+ else if (this.matterbridge.bridgeMode === 'bridge') {
866
+ if (device.mode === 'matter') {
867
+ // Register and add the device to the Matter server node
868
+ this.log.debug(`Adding matter endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} (${zb}${device.name}${db})...`);
869
+ if (!this.serverNode)
870
+ throw new Error(`Server node not found for matter endpoint ${plg}${pluginName}${er}:${dev}${device.deviceName}${er} (${zb}${device.name}${er})`);
871
+ try {
872
+ await this.serverNode.add(device);
873
+ }
874
+ catch (error) {
875
+ inspectError(this.log, `Matter error adding matter endpoint ${plg}${pluginName}${er}:${dev}${device.deviceName}${er} (${zb}${device.name}${er})`, error);
876
+ return;
877
+ }
878
+ }
879
+ else {
880
+ // Register and add the device to the Matter aggregator node
881
+ this.log.debug(`Adding bridged endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} (${zb}${device.name}${db})...`);
882
+ if (!this.aggregatorNode)
883
+ throw new Error(`Aggregator node not found for endpoint ${plg}${pluginName}${er}:${dev}${device.deviceName}${er} (${zb}${device.name}${er})`);
884
+ try {
885
+ await this.aggregatorNode.add(device);
886
+ }
887
+ catch (error) {
888
+ inspectError(this.log, `Matter error adding bridged endpoint ${plg}${pluginName}${er}:${dev}${device.deviceName}${er} (${zb}${device.name}${er})`, error);
889
+ return;
890
+ }
891
+ }
892
+ }
893
+ else if (this.matterbridge.bridgeMode === 'childbridge') {
894
+ // Register and add the device to the plugin server node
895
+ if (plugin.type === 'AccessoryPlatform') {
896
+ try {
897
+ this.log.debug(`Adding accessory endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} (${zb}${device.name}${db})...`);
898
+ if (this.serverNode) {
899
+ await this.serverNode.add(device);
900
+ }
901
+ else {
902
+ await this.createAccessoryPlugin(plugin, device);
903
+ }
904
+ }
905
+ catch (error) {
906
+ inspectError(this.log, `Matter error adding accessory endpoint ${plg}${pluginName}${er}:${dev}${device.deviceName}${er} (${zb}${device.name}${er})`, error);
907
+ return;
908
+ }
909
+ }
910
+ // Register and add the device to the plugin aggregator node
911
+ if (plugin.type === 'DynamicPlatform') {
912
+ try {
913
+ this.log.debug(`Adding bridged endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} (${zb}${device.name}${db})...`);
914
+ if (!this.serverNode) {
915
+ // Fast plugins can add another device before the server node is ready, so we wait for the server node to be ready
916
+ await this.createDynamicPlugin(plugin);
917
+ }
918
+ if (device.mode === 'matter')
919
+ await this.serverNode?.add(device);
920
+ else
921
+ await this.aggregatorNode?.add(device);
922
+ }
923
+ catch (error) {
924
+ inspectError(this.log, `Matter error adding bridged endpoint ${plg}${pluginName}${er}:${dev}${device.deviceName}${er} (${zb}${device.name}${er})`, error);
925
+ return;
926
+ }
927
+ }
928
+ }
929
+ if (plugin.registeredDevices !== undefined)
930
+ plugin.registeredDevices++;
931
+ // Add the device to the DeviceManager
932
+ await device.construction.ready;
933
+ await this.server.fetch({ type: 'devices_set', src: this.server.name, dst: 'devices', params: { device: toBaseDevice(device) } });
934
+ // Subscribe to the attributes changed event
935
+ await this.subscribeAttributeChanged(plugin, device);
936
+ this.log.info(`Added endpoint #${plugin.registeredDevices} ${plg}${pluginName}${nf}:${dev}${device.deviceName}${nf} (${zb}${device.name}${nf})`);
937
+ await this.yieldToNode(10);
938
+ return device;
939
+ }
940
+ /**
941
+ * Removes a MatterbridgeEndpoint from the specified plugin.
942
+ *
943
+ * @param {string} pluginName - The name of the plugin.
944
+ * @param {MatterbridgeEndpoint} device - The device to remove as a bridged endpoint.
945
+ * @returns {Promise<MatterbridgeEndpoint | undefined>} A promise that resolves to the removed bridged endpoint, or undefined if there was an error.
946
+ */
947
+ async removeBridgedEndpoint(pluginName, device) {
948
+ this.log.debug(`Removing bridged endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} (${zb}${device.name}${db})...`);
949
+ // Check if the plugin is registered
950
+ const plugin = this.pluginManager.get(pluginName);
951
+ if (!plugin)
952
+ throw new Error(`Error removing bridged endpoint ${plg}${pluginName}${er}:${dev}${device.deviceName}${er} (${zb}${device.name}${er}): plugin not found`);
953
+ if (device.serverNode) {
954
+ // TODO: Close and remove the MatterNode managing the device
955
+ }
956
+ else if (this.matterbridge.bridgeMode === 'bridge') {
957
+ if (!this.aggregatorNode)
958
+ throw new Error(`Error removing bridged endpoint ${plg}${pluginName}${er}:${dev}${device.deviceName}${er} (${zb}${device.name}${er}): aggregator node not found`);
959
+ await device.delete();
960
+ }
961
+ else if (this.matterbridge.bridgeMode === 'childbridge') {
962
+ if (plugin.type === 'AccessoryPlatform') {
963
+ if (!this.serverNode)
964
+ throw new Error(`Error removing endpoint ${plg}${pluginName}${er}:${dev}${device.deviceName}${er} (${zb}${device.name}${er}): server node not found`);
965
+ await device.delete();
966
+ }
967
+ else if (plugin.type === 'DynamicPlatform') {
968
+ if (!this.aggregatorNode)
969
+ throw new Error(`Error removing bridged endpoint ${plg}${pluginName}${er}:${dev}${device.deviceName}${er} (${zb}${device.name}${er}): aggregator node not found`);
970
+ await device.delete();
971
+ }
972
+ }
973
+ this.log.info(`Removed bridged endpoint #${plugin.registeredDevices} ${plg}${pluginName}${nf}:${dev}${device.deviceName}${nf} (${zb}${device.name}${nf})`);
974
+ if (plugin.registeredDevices !== undefined)
975
+ plugin.registeredDevices--;
976
+ // Remove the device from the DeviceManager
977
+ await this.server.fetch({ type: 'devices_remove', src: this.server.name, dst: 'devices', params: { device: toBaseDevice(device) } });
978
+ await this.yieldToNode(10);
979
+ return device;
980
+ }
981
+ /**
982
+ * Removes all bridged endpoints from the specified plugin.
983
+ *
984
+ * @param {string} pluginName - The name of the plugin.
985
+ * @param {number} [delay] - The delay in milliseconds between removing each bridged endpoint (default: 0).
986
+ * @returns {Promise<void>} A promise that resolves when all bridged endpoints have been removed.
987
+ *
988
+ * @remarks
989
+ * This method iterates through all devices in the DeviceManager and removes each bridged endpoint associated with the specified plugin.
990
+ * It also applies a delay between each removal if specified.
991
+ * The delay is useful to allow the controllers to receive a single subscription for each device removed.
992
+ */
993
+ async removeAllBridgedEndpoints(pluginName, delay = 0) {
994
+ // Check if the plugin is registered
995
+ const plugin = this.pluginManager.get(pluginName);
996
+ if (!plugin)
997
+ throw new Error(`Error removing all bridged endpoints for plugin ${plg}${pluginName}${er}: plugin not found`);
998
+ this.log.debug(`Removing all #${plugin.registeredDevices} bridged endpoints for plugin ${plg}${pluginName}${db}${delay > 0 ? ` with delay ${delay} ms` : ''}...`);
999
+ const devices = (await this.server.fetch({ type: 'devices_basearray', src: this.server.name, dst: 'devices', params: { pluginName } })).result.devices;
1000
+ for (const device of devices) {
1001
+ const endpoint = (this.aggregatorNode?.parts.get(device.id || '') || this.serverNode?.parts.get(device.id || ''));
1002
+ if (!endpoint)
1003
+ throw new Error(`Endpoint ${plg}${pluginName}${er}:${dev}${device.deviceName}${er} id ${device.id} not found removing all endpoints`);
1004
+ this.log.debug(`Removing bridged endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} (${zb}${endpoint?.name}${db})...`);
1005
+ await endpoint.delete();
1006
+ this.log.info(`Removed bridged endpoint #${plugin.registeredDevices} ${plg}${pluginName}${nf}:${dev}${device.deviceName}${nf} (${zb}${endpoint?.name}${nf})`);
1007
+ if (plugin.registeredDevices !== undefined)
1008
+ plugin.registeredDevices--;
1009
+ // Remove the device from the DeviceManager
1010
+ await this.server.fetch({ type: 'devices_remove', src: this.server.name, dst: 'devices', params: { device: toBaseDevice(device) } });
1011
+ await this.yieldToNode(10);
1012
+ if (delay > 0)
1013
+ await wait(delay);
1014
+ }
1015
+ if (delay > 0)
1016
+ await wait(Number(process.env['MATTERBRIDGE_REMOVE_ALL_ENDPOINT_TIMEOUT_MS']) || 2000);
1017
+ }
1018
+ /**
1019
+ * Registers a virtual device with the Matterbridge platform.
1020
+ * Virtual devices are only supported in bridge mode and childbridge mode with a DynamicPlatform.
1021
+ *
1022
+ * The virtual device is created as an instance of `Endpoint` with the provided device type.
1023
+ * When the virtual device is turned on, the provided callback function is executed.
1024
+ * The onOff state of the virtual device always reverts to false when the device is turned on.
1025
+ *
1026
+ * @param {string} pluginName - The name of the plugin.
1027
+ * @param { string } name - The name of the virtual device.
1028
+ * @param { 'light' | 'outlet' | 'switch' | 'mounted_switch' } type - The type of the virtual device.
1029
+ * @param { () => Promise<void> } callback - The callback to call when the virtual device is turned on.
1030
+ *
1031
+ * @returns {Promise<boolean>} A promise that resolves to true if the virtual device was successfully registered, false otherwise.
1032
+ *
1033
+ * @remarks
1034
+ * The virtual devices don't show up in the device list of the frontend.
1035
+ * Type 'switch' is not supported by Alexa and 'mounted_switch' is not supported by Apple Home.
1036
+ */
1037
+ async addVirtualEndpoint(pluginName, name, type, callback) {
1038
+ this.log.debug(`Creating virtual device ${plg}${pluginName}${db}:${dev}${name}${db}...`);
1039
+ const plugin = this.pluginManager.get(pluginName);
1040
+ if (!plugin) {
1041
+ this.log.error(`Error adding virtual endpoint ${plg}${pluginName}${er}:${dev}${name}${er}: plugin not found`);
1042
+ return false;
1043
+ }
1044
+ if (this.matterbridge.bridgeMode === 'childbridge' && plugin.type !== 'DynamicPlatform') {
1045
+ this.log.error(`Virtual devices are only supported in bridge mode and childbridge mode with a DynamicPlatform`);
1046
+ return false;
1047
+ }
1048
+ if (!this.aggregatorNode) {
1049
+ this.log.error(`Aggregator node not found for plugin ${plg}${plugin.name}${er} adding virtual endpoint ${dev}${name}${er}`);
1050
+ return false;
1051
+ }
1052
+ if (this.aggregatorNode.parts.has(name.replaceAll(' ', '') + ':' + type)) {
1053
+ this.log.error(`Virtual device ${plg}${pluginName}${er}:${dev}${name}${er} already registered. Please use a different name.`);
1054
+ return false;
1055
+ }
1056
+ await addVirtualDevice(this.aggregatorNode, name.slice(0, 32), type, callback);
1057
+ this.log.debug(`Created virtual device ${plg}${pluginName}${db}:${dev}${name}${db}`);
1058
+ await this.yieldToNode(10);
1059
+ return true;
1060
+ }
1061
+ /**
1062
+ * Subscribes to the attribute change event for the given device and plugin.
1063
+ * Specifically, it listens for changes in the 'reachable' attribute of the
1064
+ * BridgedDeviceBasicInformationServer cluster server of the bridged device or BasicInformationServer cluster server of server node.
1065
+ *
1066
+ * @param {Plugin} plugin - The plugin associated with the device.
1067
+ * @param {MatterbridgeEndpoint} device - The device to subscribe to attribute changes for.
1068
+ * @returns {Promise<void>} A promise that resolves when the subscription is set up.
1069
+ */
1070
+ async subscribeAttributeChanged(plugin, device) {
1071
+ if (!plugin || !device || !device.plugin || !device.serialNumber || !device.uniqueId || !device.maybeNumber)
1072
+ return;
1073
+ this.log.debug(`Subscribing attributes for endpoint ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db}:${or}${device.id}${db}:${or}${device.number}${db} (${zb}${device.name}${db})`);
1074
+ // Subscribe to the reachable$Changed event of the BasicInformationServer cluster server of the server node in childbridge mode
1075
+ if (this.matterbridge.bridgeMode === 'childbridge' && plugin.type === 'AccessoryPlatform' && this.serverNode) {
1076
+ this.serverNode.eventsOf(BasicInformationServer).reachable$Changed?.on((reachable) => {
1077
+ if (!device.plugin || !device.serialNumber || !device.uniqueId)
1078
+ return;
1079
+ this.log.debug(`Accessory endpoint ${plg}${plugin.name}${nf}:${dev}${device.deviceName}${nf}:${or}${device.id}${nf}:${or}${device.number}${nf} is ${reachable ? 'reachable' : 'unreachable'}`);
1080
+ this.server.request({
1081
+ type: 'frontend_attributechanged',
1082
+ src: 'matter',
1083
+ dst: 'frontend',
1084
+ params: {
1085
+ plugin: device.plugin,
1086
+ serialNumber: device.serialNumber,
1087
+ uniqueId: device.uniqueId,
1088
+ number: device.number,
1089
+ id: device.id,
1090
+ cluster: 'BasicInformation',
1091
+ attribute: 'reachable',
1092
+ value: reachable,
1093
+ },
1094
+ });
1095
+ });
1096
+ }
1097
+ const subscriptions = [
1098
+ { cluster: 'BridgedDeviceBasicInformation', attribute: 'reachable' },
1099
+ { cluster: 'OnOff', attribute: 'onOff' },
1100
+ { cluster: 'LevelControl', attribute: 'currentLevel' },
1101
+ { cluster: 'ColorControl', attribute: 'colorMode' },
1102
+ { cluster: 'ColorControl', attribute: 'currentHue' },
1103
+ { cluster: 'ColorControl', attribute: 'currentSaturation' },
1104
+ { cluster: 'ColorControl', attribute: 'currentX' },
1105
+ { cluster: 'ColorControl', attribute: 'currentY' },
1106
+ { cluster: 'ColorControl', attribute: 'colorTemperatureMireds' },
1107
+ { cluster: 'Thermostat', attribute: 'localTemperature' },
1108
+ { cluster: 'Thermostat', attribute: 'occupiedCoolingSetpoint' },
1109
+ { cluster: 'Thermostat', attribute: 'occupiedHeatingSetpoint' },
1110
+ { cluster: 'Thermostat', attribute: 'systemMode' },
1111
+ { cluster: 'WindowCovering', attribute: 'operationalStatus' },
1112
+ { cluster: 'WindowCovering', attribute: 'currentPositionLiftPercent100ths' },
1113
+ { cluster: 'DoorLock', attribute: 'lockState' },
1114
+ { cluster: 'PumpConfigurationAndControl', attribute: 'pumpStatus' },
1115
+ { cluster: 'FanControl', attribute: 'fanMode' },
1116
+ { cluster: 'FanControl', attribute: 'fanModeSequence' },
1117
+ { cluster: 'FanControl', attribute: 'percentSetting' },
1118
+ { cluster: 'ModeSelect', attribute: 'currentMode' },
1119
+ { cluster: 'RvcRunMode', attribute: 'currentMode' },
1120
+ { cluster: 'RvcCleanMode', attribute: 'currentMode' },
1121
+ { cluster: 'RvcOperationalState', attribute: 'operationalState' },
1122
+ { cluster: 'RvcOperationalState', attribute: 'operationalError' },
1123
+ { cluster: 'ServiceArea', attribute: 'currentArea' },
1124
+ { cluster: 'AirQuality', attribute: 'airQuality' },
1125
+ { cluster: 'TotalVolatileOrganicCompoundsConcentrationMeasurement', attribute: 'measuredValue' },
1126
+ { cluster: 'BooleanState', attribute: 'stateValue' },
1127
+ { cluster: 'OccupancySensing', attribute: 'occupancy' },
1128
+ { cluster: 'IlluminanceMeasurement', attribute: 'measuredValue' },
1129
+ { cluster: 'TemperatureMeasurement', attribute: 'measuredValue' },
1130
+ { cluster: 'RelativeHumidityMeasurement', attribute: 'measuredValue' },
1131
+ { cluster: 'PressureMeasurement', attribute: 'measuredValue' },
1132
+ { cluster: 'FlowMeasurement', attribute: 'measuredValue' },
1133
+ { cluster: 'SmokeCoAlarm', attribute: 'smokeState' },
1134
+ { cluster: 'SmokeCoAlarm', attribute: 'coState' },
1135
+ ];
1136
+ for (const sub of subscriptions) {
1137
+ if (device.hasAttributeServer(sub.cluster, sub.attribute)) {
1138
+ this.log.debug(`Subscribing to endpoint ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db}:${or}${device.id}${db}:${or}${device.number}${db} attribute ${dev}${sub.cluster}${db}.${dev}${sub.attribute}${db} changes...`);
1139
+ await device.subscribeAttribute(sub.cluster, sub.attribute, (value) => {
1140
+ if (!device.plugin || !device.serialNumber || !device.uniqueId)
1141
+ return;
1142
+ this.log.debug(`Bridged endpoint ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db}:${or}${device.id}${db}:${or}${device.number}${db} attribute ${dev}${sub.cluster}${db}.${dev}${sub.attribute}${db} changed to ${CYAN}${value}${db}`);
1143
+ this.server.request({
1144
+ type: 'frontend_attributechanged',
1145
+ src: 'matter',
1146
+ dst: 'frontend',
1147
+ params: {
1148
+ plugin: device.plugin,
1149
+ serialNumber: device.serialNumber,
1150
+ uniqueId: device.uniqueId,
1151
+ number: device.number,
1152
+ id: device.id,
1153
+ cluster: sub.cluster,
1154
+ attribute: sub.attribute,
1155
+ value: value,
1156
+ },
1157
+ });
1158
+ });
1159
+ }
1160
+ for (const child of device.getChildEndpoints()) {
1161
+ if (child.hasAttributeServer(sub.cluster, sub.attribute)) {
1162
+ this.log.debug(`Subscribing to child endpoint ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db}:${or}${child.id}${db}:${or}${child.number}${db} attribute ${dev}${sub.cluster}${db}.${dev}${sub.attribute}${db} changes...`);
1163
+ await child.subscribeAttribute(sub.cluster, sub.attribute, (value) => {
1164
+ if (!device.plugin || !device.serialNumber || !device.uniqueId)
1165
+ return;
1166
+ this.log.debug(`Bridged child endpoint ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db}:${or}${child.id}${db}:${or}${child.number}${db} attribute ${dev}${sub.cluster}${db}.${dev}${sub.attribute}${db} changed to ${CYAN}${value}${db}`);
1167
+ this.server.request({
1168
+ type: 'frontend_attributechanged',
1169
+ src: 'matter',
1170
+ dst: 'frontend',
1171
+ params: {
1172
+ plugin: device.plugin,
1173
+ serialNumber: device.serialNumber,
1174
+ uniqueId: device.uniqueId,
1175
+ number: child.number,
1176
+ id: child.id,
1177
+ cluster: sub.cluster,
1178
+ attribute: sub.attribute,
1179
+ value: value,
1180
+ },
1181
+ });
1182
+ });
1183
+ }
1184
+ }
1185
+ }
1186
+ }
1187
+ /**
1188
+ * Sanitizes the fabric information by converting bigint properties to strings because `res.json` doesn't support bigint.
1189
+ *
1190
+ * @param {ExposedFabricInformation[]} fabricInfo - The array of exposed fabric information objects.
1191
+ * @returns {SanitizedExposedFabricInformation[]} An array of sanitized exposed fabric information objects.
1192
+ */
1193
+ sanitizeFabricInformations(fabricInfo) {
1194
+ return fabricInfo.map((info) => {
1195
+ return {
1196
+ fabricIndex: info.fabricIndex,
1197
+ fabricId: info.fabricId.toString(),
1198
+ nodeId: info.nodeId.toString(),
1199
+ rootNodeId: info.rootNodeId.toString(),
1200
+ rootVendorId: info.rootVendorId,
1201
+ rootVendorName: this.getVendorIdName(info.rootVendorId),
1202
+ label: info.label,
1203
+ };
1204
+ });
1205
+ }
1206
+ /**
1207
+ * Sanitizes the session information by converting bigint properties to strings because `res.json` doesn't support bigint.
1208
+ *
1209
+ * @param {SessionsBehavior.Session[]} sessions - The array of session information objects.
1210
+ * @returns {SanitizedSession[]} An array of sanitized session information objects.
1211
+ */
1212
+ sanitizeSessionInformation(sessions) {
1213
+ return sessions
1214
+ .filter((session) => session.isPeerActive)
1215
+ .map((session) => {
1216
+ return {
1217
+ name: session.name,
1218
+ nodeId: session.nodeId.toString(),
1219
+ peerNodeId: session.peerNodeId.toString(),
1220
+ fabric: session.fabric
1221
+ ? {
1222
+ fabricIndex: session.fabric.fabricIndex,
1223
+ fabricId: session.fabric.fabricId.toString(),
1224
+ nodeId: session.fabric.nodeId.toString(),
1225
+ rootNodeId: session.fabric.rootNodeId.toString(),
1226
+ rootVendorId: session.fabric.rootVendorId,
1227
+ rootVendorName: this.getVendorIdName(session.fabric.rootVendorId),
1228
+ label: session.fabric.label,
1229
+ }
1230
+ : undefined,
1231
+ isPeerActive: session.isPeerActive,
1232
+ lastInteractionTimestamp: session.lastInteractionTimestamp?.toString(),
1233
+ lastActiveTimestamp: session.lastActiveTimestamp?.toString(),
1234
+ numberOfActiveSubscriptions: session.numberOfActiveSubscriptions,
1235
+ };
1236
+ });
1237
+ }
1238
+ /**
1239
+ * Sets the reachability of the specified server node and trigger the corresponding event.
1240
+ *
1241
+ * @param {boolean} reachable - A boolean indicating the reachability status to set.
1242
+ */
1243
+ async setServerReachability(reachable) {
1244
+ await this.serverNode?.setStateOf(BasicInformationServer, { reachable });
1245
+ this.serverNode?.act((agent) => this.serverNode?.eventsOf(BasicInformationServer).reachableChanged?.emit({ reachableNewValue: reachable }, agent.context));
1246
+ }
1247
+ /**
1248
+ * Sets the reachability of the specified aggregator node bridged devices and trigger.
1249
+ *
1250
+ * @param {Endpoint<AggregatorEndpoint>} aggregatorNode - The aggregator node to set the reachability for.
1251
+ * @param {boolean} reachable - A boolean indicating the reachability status to set.
1252
+ */
1253
+ async setAggregatorReachability(aggregatorNode, reachable) {
1254
+ for (const child of aggregatorNode.parts) {
1255
+ this.log.debug(`Setting reachability of ${child?.deviceName} to ${reachable}`);
1256
+ await child.setStateOf(BridgedDeviceBasicInformationServer, { reachable });
1257
+ child.act((agent) => child.eventsOf(BridgedDeviceBasicInformationServer).reachableChanged.emit({ reachableNewValue: true }, agent.context));
1258
+ }
1259
+ }
1260
+ getVendorIdName = (vendorId) => {
1261
+ if (!vendorId)
1262
+ return '';
1263
+ let vendorName = '(Unknown vendorId)';
1264
+ switch (vendorId) {
1265
+ case 4937:
1266
+ vendorName = '(AppleHome)';
1267
+ break;
1268
+ case 4996:
1269
+ vendorName = '(AppleKeyChain)';
1270
+ break;
1271
+ case 4362:
1272
+ vendorName = '(SmartThings)';
1273
+ break;
1274
+ case 4939:
1275
+ vendorName = '(HomeAssistant)';
1276
+ break;
1277
+ case 24582:
1278
+ vendorName = '(GoogleHome)';
1279
+ break;
1280
+ case 4631:
1281
+ vendorName = '(Alexa)';
1282
+ break;
1283
+ case 4701:
1284
+ vendorName = '(Tuya)';
1285
+ break;
1286
+ case 4718:
1287
+ vendorName = '(Xiaomi)';
1288
+ break;
1289
+ case 4742:
1290
+ vendorName = '(eWeLink)';
1291
+ break;
1292
+ case 5264:
1293
+ vendorName = '(Shelly)';
1294
+ break;
1295
+ case 0x1488:
1296
+ vendorName = '(ShortcutLabsFlic)';
1297
+ break;
1298
+ case 65521: // 0xFFF1
1299
+ vendorName = '(MatterTest)';
1300
+ break;
1301
+ }
1302
+ return vendorName;
1303
+ };
1304
+ /**
1305
+ * Yield to the Node.js event loop:
1306
+ * 1. Flushes the current microtask queue (Promise/async continuations queued so far).
1307
+ * 2. Yields one macrotask turn (setImmediate) and then its microtasks.
1308
+ * 3. Waits a bit (setTimeout) to allow other macrotasks to run.
1309
+ *
1310
+ * This does **not** guarantee that every promise in the process is settled,
1311
+ * but it gives all already-scheduled work a very good chance to run before continuing.
1312
+ *
1313
+ * @param {number} [timeout] - Optional timeout in milliseconds to wait after yielding. Default is 100 ms (minimum 10 ms).
1314
+ * @returns {Promise<void>}
1315
+ */
1316
+ async yieldToNode(timeout = 100) {
1317
+ // 1. Let all currently queued microtasks run
1318
+ await Promise.resolve();
1319
+ // 2. Yield to the next event-loop turn (macrotask + its microtasks)
1320
+ await new Promise((resolve) => {
1321
+ setImmediate(resolve);
1322
+ });
1323
+ // 3. Pause a bit to allow other macrotasks to run
1324
+ await new Promise((resolve) => {
1325
+ setTimeout(resolve, Math.min(timeout, 10));
1326
+ });
1327
+ }
1328
+ }
1329
+ //# sourceMappingURL=matterNode.js.map