@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,2880 @@
1
+ /**
2
+ * This file contains the class Matterbridge.
3
+ *
4
+ * @file matterbridge.ts
5
+ * @author Luca Liguori
6
+ * @created 2023-12-29
7
+ * @version 1.6.2
8
+ * @license Apache-2.0
9
+ *
10
+ * Copyright 2023, 2024, 2025, 2026 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
+ // eslint-disable-next-line no-console
25
+ if (process.argv.includes('--loader') || process.argv.includes('-loader'))
26
+ console.log('\u001B[32mMatterbridge loaded.\u001B[40;0m');
27
+ // Node.js modules
28
+ import os from 'node:os';
29
+ import path from 'node:path';
30
+ import { fileURLToPath } from 'node:url';
31
+ import fs, { unlinkSync } from 'node:fs';
32
+ import EventEmitter from 'node:events';
33
+ import { inspect } from 'node:util';
34
+ // AnsiLogger module
35
+ import { AnsiLogger, UNDERLINE, UNDERLINEOFF, db, debugStringify, BRIGHT, RESET, er, nf, rs, wr, RED, GREEN, zb, CYAN, nt, BLUE, or, } from 'node-ansi-logger';
36
+ // NodeStorage module
37
+ import { NodeStorageManager } from 'node-persist-manager';
38
+ // @matter
39
+ import '@matter/nodejs'; // Set up Node.js environment for matter.js
40
+ import { Logger, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, UINT32_MAX, UINT16_MAX, Crypto, Environment, StorageService, } from '@matter/general';
41
+ import { PaseClient } from '@matter/protocol';
42
+ import { Endpoint, ServerNode } from '@matter/node';
43
+ import { DeviceTypeId, VendorId } from '@matter/types/datatype';
44
+ import { AggregatorEndpoint } from '@matter/node/endpoints';
45
+ import { BasicInformationServer } from '@matter/node/behaviors/basic-information';
46
+ // @matterbridge
47
+ import { copyDirectory, createDirectory, formatBytes, formatPercent, formatUptime, getIntParameter, getParameter, hasParameter, isValidNumber, isValidObject, isValidString, parseVersionString, } from '@matterbridge/utils';
48
+ import { dev, MATTER_LOGGER_FILE, MATTER_STORAGE_NAME, MATTERBRIDGE_LOGGER_FILE, NODE_STORAGE_DIR, plg, typ } from '@matterbridge/types';
49
+ import { BroadcastServer } from '@matterbridge/thread';
50
+ // Matterbridge
51
+ import { PluginManager } from './pluginManager.js';
52
+ import { DeviceManager } from './deviceManager.js';
53
+ import { MatterbridgeEndpoint } from './matterbridgeEndpoint.js';
54
+ import { bridge } from './matterbridgeDeviceTypes.js';
55
+ import { Frontend } from './frontend.js';
56
+ import { addVirtualDevice, addVirtualDevices } from './helpers.js';
57
+ /**
58
+ * Represents the Matterbridge application.
59
+ */
60
+ export class Matterbridge extends EventEmitter {
61
+ /** Matterbridge system information */
62
+ systemInformation = {
63
+ // Network properties
64
+ interfaceName: '',
65
+ macAddress: '',
66
+ ipv4Address: '',
67
+ ipv6Address: '',
68
+ // Node.js properties
69
+ nodeVersion: '',
70
+ // Fixed system properties
71
+ hostname: '',
72
+ user: '',
73
+ osType: '',
74
+ osRelease: '',
75
+ osPlatform: '',
76
+ osArch: '',
77
+ // Cpu and memory properties
78
+ totalMemory: '',
79
+ freeMemory: '',
80
+ systemUptime: '',
81
+ processUptime: '',
82
+ cpuUsage: '',
83
+ processCpuUsage: '',
84
+ rss: '',
85
+ heapTotal: '',
86
+ heapUsed: '',
87
+ };
88
+ // Matterbridge settings
89
+ /** It indicates the home directory of the Matterbridge application. The home directory is the base directory where Matterbridge creates the matterbridge directories (os.homedir() if not overridden). */
90
+ homeDirectory = '';
91
+ /** It indicates the root directory of the Matterbridge application. The root directory is the directory where Matterbridge is executed. */
92
+ rootDirectory = '';
93
+ /** It indicates where the directory .matterbridge is located. */
94
+ matterbridgeDirectory = '';
95
+ /** It indicates where the directory Matterbridge is located. */
96
+ matterbridgePluginDirectory = '';
97
+ /** It indicates where the directory .mattercert is located. */
98
+ matterbridgeCertDirectory = '';
99
+ /** It indicates the global modules directory for npm. */
100
+ globalModulesDirectory = '';
101
+ matterbridgeVersion = '';
102
+ matterbridgeLatestVersion = '';
103
+ matterbridgeDevVersion = '';
104
+ frontendVersion = '';
105
+ /** It indicates the mode of the Matterbridge instance. It can be 'bridge', 'childbridge', 'controller' or ''. */
106
+ bridgeMode = '';
107
+ /** It indicates the restart mode of the Matterbridge instance. It can be 'service', 'docker' or ''. */
108
+ restartMode = '';
109
+ /** It indicates whether virtual mode is enabled and its type. The virtual mode control the creation of "Update matterbridge" and "Restart matterbridge" endpoints. */
110
+ virtualMode = 'outlet';
111
+ /** It indicates the Matterbridge profile in use. */
112
+ profile = getParameter('profile');
113
+ /** Matterbridge logger */
114
+ log = new AnsiLogger({
115
+ logName: 'Matterbridge',
116
+ logNameColor: '\x1b[38;5;115m',
117
+ logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */,
118
+ logLevel: hasParameter('debug') ? "debug" /* LogLevel.DEBUG */ : "info" /* LogLevel.INFO */,
119
+ });
120
+ /** Matterbridge logger level */
121
+ logLevel = this.log.logLevel;
122
+ /** Whether to log to a file */
123
+ fileLogger = false;
124
+ /** Matter logger */
125
+ matterLog = new AnsiLogger({ logName: 'Matter', logNameColor: '\x1b[34m', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: "debug" /* LogLevel.DEBUG */ });
126
+ /** Matter logger level */
127
+ matterLogLevel = this.matterLog.logLevel;
128
+ /** Whether to log Matter to a file */
129
+ matterFileLogger = false;
130
+ // Frontend settings
131
+ readOnly = hasParameter('readonly') || hasParameter('shelly');
132
+ shellyBoard = hasParameter('shelly');
133
+ shellySysUpdate = false;
134
+ shellyMainUpdate = false;
135
+ /** It indicates whether a restart is required. It can be unset in childbridge mode by restarting the plugin that triggered the restart. */
136
+ restartRequired = false;
137
+ /** It indicates whether a fixed restart is required. It cannot be unset once set. */
138
+ fixedRestartRequired = false;
139
+ /** It indicates whether an update is available. */
140
+ updateRequired = false;
141
+ // Managers
142
+ plugins = new PluginManager(this);
143
+ devices = new DeviceManager();
144
+ // Frontend
145
+ frontend = new Frontend(this);
146
+ /** Matterbridge node storage manager created in the directory 'storage' in matterbridgeDirectory */
147
+ nodeStorage;
148
+ /** Matterbridge node context created with name 'matterbridge' */
149
+ nodeContext;
150
+ /** The main instance of the Matterbridge class (singleton) */
151
+ static instance;
152
+ // Instance properties
153
+ shutdown = false;
154
+ failCountLimit = hasParameter('shelly') ? 600 : 120;
155
+ hasCleanupStarted = false;
156
+ initialized = false;
157
+ startMatterInterval;
158
+ startMatterIntervalMs = 1000;
159
+ checkUpdateInterval;
160
+ checkUpdateTimeout;
161
+ configureTimeout;
162
+ reachabilityTimeout;
163
+ sigintHandler;
164
+ sigtermHandler;
165
+ exceptionHandler;
166
+ rejectionHandler;
167
+ /** Matter environment default */
168
+ environment = Environment.default;
169
+ /** Matter storage service from environment default */
170
+ matterStorageService;
171
+ /** Matter storage manager created with name 'Matterbridge' */
172
+ matterStorageManager;
173
+ /** Matter matterbridge storage context created in the storage manager with name 'persist' */
174
+ matterbridgeContext;
175
+ controllerContext;
176
+ /** Matter mdns interface e.g. 'eth0' or 'wlan0' or 'Wi-Fi' */
177
+ mdnsInterface;
178
+ /** Matter listeningAddressIpv4 address */
179
+ ipv4Address;
180
+ /** Matter listeningAddressIpv6 address */
181
+ ipv6Address;
182
+ /** Matter commissioning port */
183
+ port; // first server node port
184
+ /** Matter commissioning passcode */
185
+ passcode; // first server node passcode
186
+ /** Matter commissioning discriminator */
187
+ discriminator; // first server node discriminator
188
+ /** Matter device certification */
189
+ certification; // device certification
190
+ /** Matter server node in bridge mode */
191
+ serverNode;
192
+ /** Matter aggregator node in bridge mode */
193
+ aggregatorNode;
194
+ aggregatorVendorId = VendorId(getIntParameter('vendorId') ?? 0xfff1);
195
+ aggregatorVendorName = getParameter('vendorName') ?? 'Matterbridge';
196
+ aggregatorProductId = getIntParameter('productId') ?? 0x8000;
197
+ aggregatorProductName = getParameter('productName') ?? 'Matterbridge aggregator';
198
+ aggregatorDeviceType = DeviceTypeId(getIntParameter('deviceType') ?? bridge.code);
199
+ aggregatorSerialNumber = getParameter('serialNumber');
200
+ aggregatorUniqueId = getParameter('uniqueId');
201
+ /** Advertising nodes map: time advertising started keyed by storeId */
202
+ advertisingNodes = new Map();
203
+ /** Broadcast server */
204
+ server;
205
+ verbose = hasParameter('verbose');
206
+ /** We load asyncronously so is private */
207
+ constructor() {
208
+ super();
209
+ this.server = new BroadcastServer('matterbridge', this.log);
210
+ this.server.on('broadcast_message', this.msgHandler.bind(this));
211
+ }
212
+ /** Close the broadcast server */
213
+ destroy() {
214
+ this.server.close();
215
+ }
216
+ /**
217
+ * Get a platform matterbridge object
218
+ *
219
+ * @returns {PlatformMatterbridge} The platform matterbridge object.
220
+ */
221
+ getPlatformMatterbridge() {
222
+ return {
223
+ systemInformation: { ...this.systemInformation },
224
+ rootDirectory: this.rootDirectory,
225
+ homeDirectory: this.homeDirectory,
226
+ matterbridgeDirectory: this.matterbridgeDirectory,
227
+ matterbridgePluginDirectory: this.matterbridgePluginDirectory,
228
+ matterbridgeCertDirectory: this.matterbridgeCertDirectory,
229
+ globalModulesDirectory: this.globalModulesDirectory,
230
+ matterbridgeVersion: this.matterbridgeVersion,
231
+ matterbridgeLatestVersion: this.matterbridgeLatestVersion,
232
+ matterbridgeDevVersion: this.matterbridgeDevVersion,
233
+ frontendVersion: this.frontendVersion,
234
+ bridgeMode: this.bridgeMode,
235
+ restartMode: this.restartMode,
236
+ virtualMode: this.virtualMode,
237
+ aggregatorVendorId: this.aggregatorVendorId,
238
+ aggregatorVendorName: this.aggregatorVendorName,
239
+ aggregatorProductId: this.aggregatorProductId,
240
+ aggregatorProductName: this.aggregatorProductName,
241
+ };
242
+ }
243
+ /**
244
+ * Get a shared matterbridge object
245
+ *
246
+ * @returns {SharedMatterbridge} The shared matterbridge object.
247
+ */
248
+ getSharedMatterbridge() {
249
+ return {
250
+ systemInformation: { ...this.systemInformation },
251
+ rootDirectory: this.rootDirectory,
252
+ homeDirectory: this.homeDirectory,
253
+ matterbridgeDirectory: this.matterbridgeDirectory,
254
+ matterbridgePluginDirectory: this.matterbridgePluginDirectory,
255
+ matterbridgeCertDirectory: this.matterbridgeCertDirectory,
256
+ globalModulesDirectory: this.globalModulesDirectory,
257
+ matterbridgeVersion: this.matterbridgeVersion,
258
+ matterbridgeLatestVersion: this.matterbridgeLatestVersion,
259
+ matterbridgeDevVersion: this.matterbridgeDevVersion,
260
+ frontendVersion: this.frontendVersion,
261
+ bridgeMode: this.bridgeMode,
262
+ restartMode: this.restartMode,
263
+ virtualMode: this.virtualMode,
264
+ profile: this.profile,
265
+ logLevel: this.logLevel,
266
+ fileLogger: this.fileLogger,
267
+ matterLogLevel: this.matterLogLevel,
268
+ matterFileLogger: this.matterFileLogger,
269
+ mdnsInterface: this.mdnsInterface,
270
+ ipv4Address: this.ipv4Address,
271
+ ipv6Address: this.ipv6Address,
272
+ port: this.port,
273
+ discriminator: this.discriminator,
274
+ passcode: this.passcode,
275
+ shellySysUpdate: this.shellySysUpdate,
276
+ shellyMainUpdate: this.shellyMainUpdate,
277
+ };
278
+ }
279
+ async msgHandler(msg) {
280
+ if (this.server.isWorkerRequest(msg) && (msg.dst === 'all' || msg.dst === 'matterbridge')) {
281
+ if (this.verbose)
282
+ this.log.debug(`Received broadcast request ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}: ${debugStringify(msg)}${db}`);
283
+ switch (msg.type) {
284
+ case 'get_log_level':
285
+ this.server.respond({ ...msg, result: { logLevel: this.log.logLevel } });
286
+ break;
287
+ case 'set_log_level':
288
+ this.log.logLevel = msg.params.logLevel;
289
+ this.server.respond({ ...msg, result: { logLevel: this.log.logLevel } });
290
+ break;
291
+ case 'matterbridge_latest_version':
292
+ this.matterbridgeLatestVersion = msg.params.version;
293
+ await this.nodeContext?.set('matterbridgeLatestVersion', msg.params.version);
294
+ this.server.respond({ ...msg, result: { success: true } });
295
+ break;
296
+ case 'matterbridge_dev_version':
297
+ this.matterbridgeDevVersion = msg.params.version;
298
+ await this.nodeContext?.set('matterbridgeDevVersion', msg.params.version);
299
+ this.server.respond({ ...msg, result: { success: true } });
300
+ break;
301
+ case 'matterbridge_global_prefix':
302
+ this.globalModulesDirectory = msg.params.prefix;
303
+ await this.nodeContext?.set('globalModulesDirectory', msg.params.prefix);
304
+ this.server.respond({ ...msg, result: { success: true } });
305
+ break;
306
+ case 'matterbridge_sys_update':
307
+ this.shellySysUpdate = true;
308
+ this.server.respond({ ...msg, result: { success: true } });
309
+ break;
310
+ case 'matterbridge_main_update':
311
+ this.shellyMainUpdate = true;
312
+ this.server.respond({ ...msg, result: { success: true } });
313
+ break;
314
+ case 'matterbridge_platform':
315
+ this.server.respond({ ...msg, result: { data: this.getPlatformMatterbridge(), success: true } });
316
+ break;
317
+ case 'matterbridge_shared':
318
+ this.server.respond({ ...msg, result: { data: this.getSharedMatterbridge(), success: true } });
319
+ break;
320
+ default:
321
+ if (this.verbose)
322
+ this.log.debug(`Unknown broadcast request ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}`);
323
+ }
324
+ }
325
+ }
326
+ //* ************************************************************************************************************************************ */
327
+ // loadInstance() and cleanup() methods */
328
+ //* ************************************************************************************************************************************ */
329
+ /**
330
+ * Loads an instance of the Matterbridge class.
331
+ * If an instance already exists, return that instance.
332
+ *
333
+ * @param {boolean} initialize - Whether to initialize the Matterbridge instance after loading. Defaults to false.
334
+ * @returns {Matterbridge} A promise that resolves to the Matterbridge instance.
335
+ */
336
+ static async loadInstance(initialize = false) {
337
+ if (!Matterbridge.instance) {
338
+ // eslint-disable-next-line no-console
339
+ if (hasParameter('debug'))
340
+ console.log(GREEN + 'Creating a new instance of Matterbridge.', initialize ? 'Initializing...' : 'Not initializing...', rs);
341
+ Matterbridge.instance = new Matterbridge();
342
+ if (initialize)
343
+ await Matterbridge.instance.initialize();
344
+ }
345
+ return Matterbridge.instance;
346
+ }
347
+ /**
348
+ * Initializes the Matterbridge application.
349
+ *
350
+ * @remarks
351
+ * This method performs the necessary setup and initialization steps for the Matterbridge application.
352
+ * It displays the help information if the 'help' parameter is provided, sets up the logger, checks the
353
+ * node version, registers signal handlers, initializes storage, and parses the command line.
354
+ *
355
+ * @returns {Promise<void>} A Promise that resolves when the initialization is complete.
356
+ */
357
+ async initialize() {
358
+ // for (let i = 1; i <= 255; i++) console.log(`\x1b[38;5;${i}mColor: ${i}`);
359
+ // Emit the initialize_started event
360
+ this.emit('initialize_started');
361
+ // Set the restart mode
362
+ if (hasParameter('service'))
363
+ this.restartMode = 'service';
364
+ if (hasParameter('docker'))
365
+ this.restartMode = 'docker';
366
+ // Set the matterbridge home directory
367
+ this.homeDirectory = getParameter('homedir') ?? os.homedir();
368
+ await createDirectory(this.homeDirectory, 'Matterbridge Home Directory', this.log);
369
+ // Set the matterbridge directory
370
+ this.matterbridgeDirectory = this.profile ? path.join(this.homeDirectory, '.matterbridge', 'profiles', this.profile) : path.join(this.homeDirectory, '.matterbridge');
371
+ await createDirectory(this.matterbridgeDirectory, 'Matterbridge Directory', this.log);
372
+ await createDirectory(path.join(this.matterbridgeDirectory, 'certs'), 'Matterbridge Frontend Certificate Directory', this.log);
373
+ await createDirectory(path.join(this.matterbridgeDirectory, 'uploads'), 'Matterbridge Frontend Uploads Directory', this.log);
374
+ // Set the matterbridge plugin directory
375
+ this.matterbridgePluginDirectory = this.profile ? path.join(this.homeDirectory, 'Matterbridge', 'profiles', this.profile) : path.join(this.homeDirectory, 'Matterbridge');
376
+ await createDirectory(this.matterbridgePluginDirectory, 'Matterbridge Plugin Directory', this.log);
377
+ // Set the matterbridge cert directory
378
+ this.matterbridgeCertDirectory = this.profile ? path.join(this.homeDirectory, '.mattercert', 'profiles', this.profile) : path.join(this.homeDirectory, '.mattercert');
379
+ await createDirectory(this.matterbridgeCertDirectory, 'Matterbridge Matter Certificate Directory', this.log);
380
+ // Set the matterbridge root directory
381
+ const currentFileDirectory = path.dirname(fileURLToPath(import.meta.url));
382
+ // Adjust the path for packages core dist directory or node_modules @matterbridge core dist directory
383
+ this.rootDirectory = currentFileDirectory.includes(path.join('packages', 'core'))
384
+ ? path.resolve(currentFileDirectory, '../', '../', '../')
385
+ : path.resolve(currentFileDirectory, '../', '../', '..', '../');
386
+ // Setup the matter environment with default values
387
+ this.environment.vars.set('log.level', MatterLogLevel.INFO);
388
+ this.environment.vars.set('log.format', hasParameter('no-ansi') || process.env.NO_COLOR === '1' ? MatterLogFormat.PLAIN : MatterLogFormat.ANSI);
389
+ this.environment.vars.set('path.root', path.join(this.matterbridgeDirectory, MATTER_STORAGE_NAME));
390
+ this.environment.vars.set('runtime.signals', false);
391
+ this.environment.vars.set('runtime.exitcode', false);
392
+ // Register process handlers
393
+ this.registerProcessHandlers();
394
+ // Initialize nodeStorage and nodeContext
395
+ try {
396
+ this.log.debug(`Creating node storage manager: ${CYAN}${NODE_STORAGE_DIR}${db}`);
397
+ this.nodeStorage = new NodeStorageManager({ dir: path.join(this.matterbridgeDirectory, NODE_STORAGE_DIR), writeQueue: false, expiredInterval: undefined, logging: false });
398
+ this.log.debug('Creating node storage context for matterbridge');
399
+ this.nodeContext = await this.nodeStorage.createStorage('matterbridge');
400
+ // TODO: Remove this code when node-persist-manager is updated
401
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
402
+ const keys = (await this.nodeStorage?.storage.keys());
403
+ for (const key of keys) {
404
+ this.log.debug(`Checking node storage manager key: ${CYAN}${key}${db}`);
405
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
406
+ await this.nodeStorage?.storage.get(key);
407
+ }
408
+ const storages = await this.nodeStorage.getStorageNames();
409
+ for (const storage of storages) {
410
+ this.log.debug(`Checking storage: ${CYAN}${storage}${db}`);
411
+ const nodeContext = await this.nodeStorage?.createStorage(storage);
412
+ // TODO: Remove this code when node-persist-manager is updated
413
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
414
+ const keys = (await nodeContext?.storage.keys());
415
+ keys.forEach(async (key) => {
416
+ this.log.debug(`Checking key: ${CYAN}${storage}:${key}${db}`);
417
+ await nodeContext?.get(key);
418
+ });
419
+ }
420
+ // Creating a backup of the node storage since it is not corrupted
421
+ this.log.debug('Creating node storage backup...');
422
+ await copyDirectory(path.join(this.matterbridgeDirectory, NODE_STORAGE_DIR), path.join(this.matterbridgeDirectory, NODE_STORAGE_DIR + '.backup'));
423
+ this.log.debug('Created node storage backup');
424
+ }
425
+ catch (error) {
426
+ // Restoring the backup of the node storage since it is corrupted
427
+ this.log.error(`Error creating node storage manager and context: ${error instanceof Error ? error.message : error}`);
428
+ if (hasParameter('norestore')) {
429
+ this.log.fatal(`The matterbridge storage is corrupted. Found -norestore parameter: exiting...`);
430
+ }
431
+ else {
432
+ this.log.notice(`The matterbridge storage is corrupted. Restoring it with backup...`);
433
+ await copyDirectory(path.join(this.matterbridgeDirectory, NODE_STORAGE_DIR + '.backup'), path.join(this.matterbridgeDirectory, NODE_STORAGE_DIR));
434
+ this.log.notice(`The matterbridge storage has been restored with backup`);
435
+ }
436
+ }
437
+ if (!this.nodeStorage || !this.nodeContext) {
438
+ throw new Error('Fatal error creating node storage manager and context for matterbridge');
439
+ }
440
+ // Set the first port to use for the commissioning server (will be incremented in childbridge mode)
441
+ this.port = getIntParameter('port') ?? (await this.nodeContext.get('matterport', 5540)) ?? 5540;
442
+ // Set the first passcode to use for the commissioning server (will be incremented in childbridge mode)
443
+ this.passcode = getIntParameter('passcode') ?? (await this.nodeContext.get('matterpasscode')) ?? PaseClient.generateRandomPasscode(this.environment.get(Crypto));
444
+ // Set the first discriminator to use for the commissioning server (will be incremented in childbridge mode)
445
+ this.discriminator =
446
+ getIntParameter('discriminator') ?? (await this.nodeContext.get('matterdiscriminator')) ?? PaseClient.generateRandomDiscriminator(this.environment.get(Crypto));
447
+ // Certificate management
448
+ const pairingFilePath = path.join(this.matterbridgeCertDirectory, 'pairing.json');
449
+ try {
450
+ await fs.promises.access(pairingFilePath, fs.constants.R_OK);
451
+ const pairingFileContent = await fs.promises.readFile(pairingFilePath, 'utf8');
452
+ const pairingFileJson = JSON.parse(pairingFileContent);
453
+ // Set the vendorId, vendorName, productId, productName, deviceType, serialNumber, uniqueId if they are present in the pairing file
454
+ if (isValidNumber(pairingFileJson.vendorId)) {
455
+ this.aggregatorVendorId = VendorId(pairingFileJson.vendorId);
456
+ this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using vendorId ${CYAN}${this.aggregatorVendorId}${nf} from pairing file.`);
457
+ }
458
+ if (isValidString(pairingFileJson.vendorName, 3)) {
459
+ this.aggregatorVendorName = pairingFileJson.vendorName;
460
+ this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using vendorName ${CYAN}${this.aggregatorVendorName}${nf} from pairing file.`);
461
+ }
462
+ if (isValidNumber(pairingFileJson.productId)) {
463
+ this.aggregatorProductId = pairingFileJson.productId;
464
+ this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using productId ${CYAN}${this.aggregatorProductId}${nf} from pairing file.`);
465
+ }
466
+ if (isValidString(pairingFileJson.productName, 3)) {
467
+ this.aggregatorProductName = pairingFileJson.productName;
468
+ this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using productName ${CYAN}${this.aggregatorProductName}${nf} from pairing file.`);
469
+ }
470
+ if (isValidNumber(pairingFileJson.deviceType)) {
471
+ this.aggregatorDeviceType = DeviceTypeId(pairingFileJson.deviceType);
472
+ this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using deviceType ${CYAN}${this.aggregatorDeviceType}(0x${this.aggregatorDeviceType.toString(16).padStart(4, '0')})${nf} from pairing file.`);
473
+ }
474
+ if (isValidString(pairingFileJson.serialNumber, 3)) {
475
+ this.aggregatorSerialNumber = pairingFileJson.serialNumber;
476
+ this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using serialNumber ${CYAN}${this.aggregatorSerialNumber}${nf} from pairing file.`);
477
+ }
478
+ if (isValidString(pairingFileJson.uniqueId, 3)) {
479
+ this.aggregatorUniqueId = pairingFileJson.uniqueId;
480
+ this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using uniqueId ${CYAN}${this.aggregatorUniqueId}${nf} from pairing file.`);
481
+ }
482
+ // Override the passcode and discriminator if they are present in the pairing file
483
+ if (isValidNumber(pairingFileJson.passcode) && isValidNumber(pairingFileJson.discriminator)) {
484
+ this.passcode = pairingFileJson.passcode;
485
+ this.discriminator = pairingFileJson.discriminator;
486
+ this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using passcode ${CYAN}${this.passcode}${nf} and discriminator ${CYAN}${this.discriminator}${nf} from pairing file.`);
487
+ }
488
+ // Set the certification for matter.js if it is present in the pairing file
489
+ if (pairingFileJson.privateKey && pairingFileJson.certificate && pairingFileJson.intermediateCertificate && pairingFileJson.declaration) {
490
+ const { hexToBuffer } = await import('@matterbridge/utils');
491
+ this.certification = {
492
+ privateKey: hexToBuffer(pairingFileJson.privateKey),
493
+ certificate: hexToBuffer(pairingFileJson.certificate),
494
+ intermediateCertificate: hexToBuffer(pairingFileJson.intermediateCertificate),
495
+ declaration: hexToBuffer(pairingFileJson.declaration),
496
+ };
497
+ this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using privateKey, certificate, intermediateCertificate and declaration from pairing file.`);
498
+ }
499
+ }
500
+ catch (error) {
501
+ this.log.debug(`Pairing file ${CYAN}${pairingFilePath}${db} not found: ${error instanceof Error ? error.message : error}`);
502
+ }
503
+ // Store the passcode, discriminator and port in the node context
504
+ await this.nodeContext.set('matterport', this.port);
505
+ await this.nodeContext.set('matterpasscode', this.passcode);
506
+ await this.nodeContext.set('matterdiscriminator', this.discriminator);
507
+ this.log.debug(`Initializing server node for Matterbridge on port ${this.port} with passcode ${this.passcode} and discriminator ${this.discriminator}`);
508
+ // Set matterbridge logger level (context: matterbridgeLogLevel)
509
+ if (hasParameter('logger')) {
510
+ const level = getParameter('logger');
511
+ if (level === 'debug') {
512
+ this.log.logLevel = "debug" /* LogLevel.DEBUG */;
513
+ }
514
+ else if (level === 'info') {
515
+ this.log.logLevel = "info" /* LogLevel.INFO */;
516
+ }
517
+ else if (level === 'notice') {
518
+ this.log.logLevel = "notice" /* LogLevel.NOTICE */;
519
+ }
520
+ else if (level === 'warn') {
521
+ this.log.logLevel = "warn" /* LogLevel.WARN */;
522
+ }
523
+ else if (level === 'error') {
524
+ this.log.logLevel = "error" /* LogLevel.ERROR */;
525
+ }
526
+ else if (level === 'fatal') {
527
+ this.log.logLevel = "fatal" /* LogLevel.FATAL */;
528
+ }
529
+ else {
530
+ this.log.warn(`Invalid matterbridge logger level: ${level}. Using default level "info".`);
531
+ this.log.logLevel = "info" /* LogLevel.INFO */;
532
+ }
533
+ }
534
+ else {
535
+ this.log.logLevel = await this.nodeContext.get('matterbridgeLogLevel', this.shellyBoard ? "notice" /* LogLevel.NOTICE */ : "info" /* LogLevel.INFO */);
536
+ }
537
+ this.logLevel = this.log.logLevel;
538
+ this.frontend.logLevel = this.log.logLevel;
539
+ MatterbridgeEndpoint.logLevel = this.log.logLevel;
540
+ // Create the file logger for matterbridge (context: matterbridgeFileLog)
541
+ if (hasParameter('filelogger') || (await this.nodeContext.get('matterbridgeFileLog', false))) {
542
+ AnsiLogger.setGlobalLogfile(path.join(this.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE), this.log.logLevel, true);
543
+ this.fileLogger = true;
544
+ }
545
+ this.log.notice('Matterbridge is starting...');
546
+ this.log.debug(`Matterbridge logLevel: ${this.log.logLevel} fileLoger: ${this.fileLogger}.`);
547
+ if (this.profile !== undefined)
548
+ this.log.debug(`Matterbridge profile: ${this.profile}.`);
549
+ // Set matter.js logger level, format and logger (context: matterLogLevel)
550
+ if (hasParameter('matterlogger')) {
551
+ const level = getParameter('matterlogger');
552
+ if (level === 'debug') {
553
+ Logger.level = MatterLogLevel.DEBUG;
554
+ }
555
+ else if (level === 'info') {
556
+ Logger.level = MatterLogLevel.INFO;
557
+ }
558
+ else if (level === 'notice') {
559
+ Logger.level = MatterLogLevel.NOTICE;
560
+ }
561
+ else if (level === 'warn') {
562
+ Logger.level = MatterLogLevel.WARN;
563
+ }
564
+ else if (level === 'error') {
565
+ Logger.level = MatterLogLevel.ERROR;
566
+ }
567
+ else if (level === 'fatal') {
568
+ Logger.level = MatterLogLevel.FATAL;
569
+ }
570
+ else {
571
+ this.log.warn(`Invalid matter.js logger level: ${level}. Using default level "info".`);
572
+ Logger.level = MatterLogLevel.INFO;
573
+ }
574
+ }
575
+ else {
576
+ Logger.level = (await this.nodeContext.get('matterLogLevel', this.shellyBoard ? MatterLogLevel.NOTICE : MatterLogLevel.INFO));
577
+ }
578
+ Logger.format = MatterLogFormat.ANSI;
579
+ this.matterLogLevel = MatterLogLevel.names[Logger.level];
580
+ // Create the logger for matter.js with file logging (context: matterFileLog)
581
+ if (hasParameter('matterfilelogger') || (await this.nodeContext.get('matterFileLog', false))) {
582
+ this.matterFileLogger = true;
583
+ }
584
+ Logger.destinations.default.write = this.createDestinationMatterLogger(this.matterFileLogger);
585
+ this.log.debug(`Matter logLevel: ${this.matterLogLevel} fileLoger: ${this.matterFileLogger}.`);
586
+ // Log network interfaces
587
+ const networkInterfaces = os.networkInterfaces();
588
+ const availableAddresses = Object.entries(networkInterfaces);
589
+ const availableInterfaceNames = Object.keys(networkInterfaces);
590
+ for (const [ifaceName, ifaces] of availableAddresses) {
591
+ if (ifaces && ifaces.length > 0) {
592
+ this.log.debug(`Network interface ${BLUE}${ifaceName}${db}:`);
593
+ ifaces.forEach((iface) => {
594
+ this.log.debug(`- ${CYAN}${iface.family}${db} address ${CYAN}${iface.address}${db} netmask ${CYAN}${iface.netmask}${db} mac ${CYAN}${iface.mac}${db}` +
595
+ `${iface.scopeid ? ` scopeid ${CYAN}${iface.scopeid}${db}` : ''}${iface.cidr ? ` cidr ${CYAN}${iface.cidr}${db}` : ''} ${CYAN}${iface.internal ? 'internal' : 'external'}${db}`);
596
+ });
597
+ }
598
+ }
599
+ // Set the interface to use for matter server node mdnsInterface
600
+ if (hasParameter('mdnsinterface')) {
601
+ this.mdnsInterface = getParameter('mdnsinterface');
602
+ }
603
+ else {
604
+ this.mdnsInterface = await this.nodeContext.get('mattermdnsinterface', undefined);
605
+ if (this.mdnsInterface === '')
606
+ this.mdnsInterface = undefined;
607
+ }
608
+ // Validate mdnsInterface
609
+ if (this.mdnsInterface) {
610
+ if (!availableInterfaceNames.includes(this.mdnsInterface)) {
611
+ this.log.error(`Invalid mdnsinterface: ${this.mdnsInterface}. Available interfaces are: ${availableInterfaceNames.join(', ')}. Using all available interfaces.`);
612
+ this.mdnsInterface = undefined;
613
+ await this.nodeContext.remove('mattermdnsinterface');
614
+ }
615
+ else {
616
+ this.log.info(`Using mdnsinterface ${CYAN}${this.mdnsInterface}${nf} for the Matter MdnsBroadcaster.`);
617
+ }
618
+ }
619
+ if (this.mdnsInterface)
620
+ this.environment.vars.set('mdns.networkInterface', this.mdnsInterface);
621
+ // Set the listeningAddressIpv4 for the matter commissioning server
622
+ if (hasParameter('ipv4address')) {
623
+ this.ipv4Address = getParameter('ipv4address');
624
+ }
625
+ else {
626
+ this.ipv4Address = await this.nodeContext.get('matteripv4address', undefined);
627
+ if (this.ipv4Address === '')
628
+ this.ipv4Address = undefined;
629
+ }
630
+ // Validate ipv4address
631
+ if (this.ipv4Address) {
632
+ let isValid = false;
633
+ for (const [ifaceName, ifaces] of availableAddresses) {
634
+ if (ifaces && ifaces.find((iface) => iface.address === this.ipv4Address)) {
635
+ this.log.info(`Using ipv4address ${CYAN}${this.ipv4Address}${nf} on interface ${CYAN}${ifaceName}${nf} for the Matter server node.`);
636
+ isValid = true;
637
+ break;
638
+ }
639
+ }
640
+ if (!isValid) {
641
+ this.log.error(`Invalid ipv4address: ${this.ipv4Address}. Using all available addresses.`);
642
+ this.ipv4Address = undefined;
643
+ await this.nodeContext.remove('matteripv4address');
644
+ }
645
+ }
646
+ // Set the listeningAddressIpv6 for the matter commissioning server
647
+ if (hasParameter('ipv6address')) {
648
+ this.ipv6Address = getParameter('ipv6address');
649
+ }
650
+ else {
651
+ this.ipv6Address = await this.nodeContext?.get('matteripv6address', undefined);
652
+ if (this.ipv6Address === '')
653
+ this.ipv6Address = undefined;
654
+ }
655
+ // Validate ipv6address
656
+ if (this.ipv6Address) {
657
+ let isValid = false;
658
+ for (const [ifaceName, ifaces] of availableAddresses) {
659
+ if (ifaces && ifaces.find((iface) => (iface.scopeid === undefined || iface.scopeid === 0) && iface.address === this.ipv6Address)) {
660
+ this.log.info(`Using ipv6address ${CYAN}${this.ipv6Address}${nf} on interface ${CYAN}${ifaceName}${nf} for the Matter server node.`);
661
+ isValid = true;
662
+ break;
663
+ }
664
+ /* istanbul ignore next */
665
+ if (ifaces &&
666
+ ifaces.find((iface) => iface.scopeid && iface.scopeid > 0 && iface.address + '%' + (process.platform === 'win32' ? iface.scopeid : ifaceName) === this.ipv6Address)) {
667
+ this.log.info(`Using ipv6address ${CYAN}${this.ipv6Address}${nf} on interface ${CYAN}${ifaceName}${nf} for the Matter server node.`);
668
+ isValid = true;
669
+ break;
670
+ }
671
+ }
672
+ if (!isValid) {
673
+ this.log.error(`Invalid ipv6address: ${this.ipv6Address}. Using all available addresses.`);
674
+ this.ipv6Address = undefined;
675
+ await this.nodeContext.remove('matteripv6address');
676
+ }
677
+ }
678
+ // Initialize the virtual mode
679
+ if (hasParameter('novirtual')) {
680
+ this.virtualMode = 'disabled';
681
+ await this.nodeContext.set('virtualmode', 'disabled');
682
+ }
683
+ else {
684
+ this.virtualMode = (await this.nodeContext.get('virtualmode', 'outlet'));
685
+ }
686
+ this.log.debug(`Virtual mode ${this.virtualMode}.`);
687
+ // Initialize PluginManager
688
+ this.plugins.logLevel = this.log.logLevel;
689
+ await this.plugins.loadFromStorage();
690
+ // Initialize DeviceManager
691
+ this.devices.logLevel = this.log.logLevel;
692
+ // Get the plugins from node storage and create the plugins node storage contexts
693
+ for (const plugin of this.plugins) {
694
+ // Try to reinstall the plugin from npm (for Docker pull and external plugins)
695
+ // We don't do this when the add and other shutdown parameters are set because we shut down the process after adding the plugin
696
+ if (!fs.existsSync(plugin.path) &&
697
+ !hasParameter('add') &&
698
+ !hasParameter('remove') &&
699
+ !hasParameter('enable') &&
700
+ !hasParameter('disable') &&
701
+ !hasParameter('reset') &&
702
+ !hasParameter('factoryreset')) {
703
+ this.log.info(`Error parsing plugin ${plg}${plugin.name}${nf}. Trying to reinstall it from npm...`);
704
+ const { spawnCommand } = await import('./spawn.js');
705
+ if (await spawnCommand('npm', ['install', '-g', `${plugin.name}${plugin.version.includes('-dev-') ? '@dev' : ''}`, '--omit=dev', '--verbose'], 'install', plugin.name)) {
706
+ this.log.info(`Plugin ${plg}${plugin.name}${nf} reinstalled.`);
707
+ plugin.error = false;
708
+ }
709
+ else {
710
+ this.log.error(`Error reinstalling plugin ${plg}${plugin.name}${nf}. The plugin is disabled.`);
711
+ plugin.error = true;
712
+ plugin.enabled = false;
713
+ continue;
714
+ }
715
+ }
716
+ if ((await this.plugins.parse(plugin)) === null) {
717
+ this.log.error(`Error parsing plugin ${plg}${plugin.name}${er}. The plugin is disabled.`);
718
+ plugin.error = true;
719
+ plugin.enabled = false;
720
+ continue;
721
+ }
722
+ this.log.debug(`Creating node storage context for plugin ${plg}${plugin.name}${db}`);
723
+ plugin.nodeContext = await this.nodeStorage.createStorage(plugin.name);
724
+ await plugin.nodeContext.set('name', plugin.name);
725
+ await plugin.nodeContext.set('type', plugin.type);
726
+ await plugin.nodeContext.set('path', plugin.path);
727
+ await plugin.nodeContext.set('version', plugin.version);
728
+ await plugin.nodeContext.set('description', plugin.description);
729
+ await plugin.nodeContext.set('author', plugin.author);
730
+ }
731
+ // Log system info and create .matterbridge directory
732
+ await this.logNodeAndSystemInfo();
733
+ this.log.notice(`Matterbridge version ${this.matterbridgeVersion} ` +
734
+ `${hasParameter('bridge') || (!hasParameter('childbridge') && (await this.nodeContext?.get('bridgeMode', '')) === 'bridge') ? 'mode bridge ' : ''}` +
735
+ `${hasParameter('childbridge') || (!hasParameter('bridge') && (await this.nodeContext?.get('bridgeMode', '')) === 'childbridge') ? 'mode childbridge ' : ''}` +
736
+ `${hasParameter('controller') ? 'mode controller ' : ''}` +
737
+ `${this.restartMode !== '' ? 'restart mode ' + this.restartMode + ' ' : ''}` +
738
+ `running on ${this.systemInformation.osType} (v.${this.systemInformation.osRelease}) platform ${this.systemInformation.osPlatform} arch ${this.systemInformation.osArch}`);
739
+ // Check node version and throw error
740
+ const minNodeVersion = 20;
741
+ const nodeVersion = process.versions.node;
742
+ const versionMajor = parseInt(nodeVersion.split('.')[0]);
743
+ if (versionMajor < minNodeVersion) {
744
+ this.log.error(`Node version ${versionMajor} is not supported. Please upgrade to ${minNodeVersion} or above.`);
745
+ throw new Error(`Node version ${versionMajor} is not supported. Please upgrade to ${minNodeVersion} or above.`);
746
+ }
747
+ // Parse command line
748
+ await this.parseCommandLine();
749
+ // Emit the initialize_completed event
750
+ this.emit('initialize_completed');
751
+ this.initialized = true;
752
+ }
753
+ /**
754
+ * Resolve a file path located in the `@matterbridge/core` distribution directory.
755
+ *
756
+ * @remarks
757
+ * Matterbridge spawns ESM workers from built JavaScript files (e.g. `workerCheckUpdates.js`).
758
+ * Depending on how the code is executed:
759
+ * - **Production**: `import.meta.url` points inside `.../node_modules/@matterbridge/core/dist/...`
760
+ * and the worker file is usually alongside the current module.
761
+ * - **Development / tests**: `import.meta.url` may point inside `.../packages/core/src/...`
762
+ * while the worker file exists in `.../packages/core/dist/...`.
763
+ *
764
+ * This helper tries both locations and returns the first existing candidate.
765
+ *
766
+ * @param {string} fileName - Worker/build artifact file name, e.g. `workerGlobalPrefix.js`.
767
+ * @returns {string} Absolute path to the resolved file. If none exists, returns the first candidate (best effort).
768
+ */
769
+ resolveWorkerDistFilePath(fileName) {
770
+ const currentModuleDirectory = path.dirname(fileURLToPath(import.meta.url));
771
+ // This core package's src or dist directory or the global installation dist directory for thread package
772
+ const candidates = [
773
+ path.join(currentModuleDirectory, fileName), // Current src directory for jest tests
774
+ path.join(currentModuleDirectory, '..', 'dist', fileName), // Dist directory for local development with local packages
775
+ path.join(this.rootDirectory, 'node_modules', '@matterbridge', 'thread', 'dist', fileName), // Global installation dist directory for production with thread package
776
+ ];
777
+ for (const candidate of candidates) {
778
+ if (fs.existsSync(candidate))
779
+ return candidate;
780
+ }
781
+ return candidates[0];
782
+ }
783
+ /**
784
+ * Parses the command line arguments and performs the corresponding actions.
785
+ *
786
+ * @private
787
+ * @returns {Promise<void>} A promise that resolves when the command line arguments have been processed, or the process exits.
788
+ */
789
+ async parseCommandLine() {
790
+ if (hasParameter('list')) {
791
+ this.log.info(`│ Registered plugins (${this.plugins.length})`);
792
+ let index = 0;
793
+ for (const plugin of this.plugins) {
794
+ if (index !== this.plugins.length - 1) {
795
+ this.log.info(`├─┬─ plugin ${plg}${plugin.name}${nf}: "${plg}${BRIGHT}${plugin.description}${RESET}${nf}" type: ${typ}${plugin.type}${nf} version: ${plg}${plugin.version}${nf} ${plugin.enabled ? GREEN : RED}enabled${nf}`);
796
+ this.log.info(`│ └─ entry ${UNDERLINE}${db}${plugin.path}${UNDERLINEOFF}${db}`);
797
+ }
798
+ else {
799
+ this.log.info(`└─┬─ plugin ${plg}${plugin.name}${nf}: "${plg}${BRIGHT}${plugin.description}${RESET}${nf}" type: ${typ}${plugin.type}${nf} version: ${plg}${plugin.version}${nf} ${plugin.enabled ? GREEN : RED}disabled${nf}`);
800
+ this.log.info(` └─ entry ${UNDERLINE}${db}${plugin.path}${UNDERLINEOFF}${db}`);
801
+ }
802
+ index++;
803
+ }
804
+ /*
805
+ const serializedRegisteredDevices = await this.nodeContext?.get<SerializedMatterbridgeEndpoint[]>('devices', []);
806
+ this.log.info(`│ Registered devices (${serializedRegisteredDevices?.length})`);
807
+ serializedRegisteredDevices?.forEach((device, index) => {
808
+ if (index !== serializedRegisteredDevices.length - 1) {
809
+ this.log.info(`├─┬─ plugin ${plg}${device.pluginName}${nf} device: ${dev}${device.deviceName}${nf} uniqueId: ${YELLOW}${device.uniqueId}${nf}`);
810
+ this.log.info(`│ └─ endpoint ${RED}${device.endpoint}${nf} ${typ}${device.endpointName}${nf} ${debugStringify(device.clusterServersId)}`);
811
+ } else {
812
+ this.log.info(`└─┬─ plugin ${plg}${device.pluginName}${nf} device: ${dev}${device.deviceName}${nf} uniqueId: ${YELLOW}${device.uniqueId}${nf}`);
813
+ this.log.info(` └─ endpoint ${RED}${device.endpoint}${nf} ${typ}${device.endpointName}${nf} ${debugStringify(device.clusterServersId)}`);
814
+ }
815
+ });
816
+ */
817
+ this.shutdown = true;
818
+ return;
819
+ }
820
+ if (hasParameter('logstorage')) {
821
+ this.log.info(`${plg}Matterbridge${nf} storage log`);
822
+ await this.nodeContext?.logStorage();
823
+ for (const plugin of this.plugins) {
824
+ this.log.info(`${plg}${plugin.name}${nf} storage log`);
825
+ await plugin.nodeContext?.logStorage();
826
+ }
827
+ this.shutdown = true;
828
+ return;
829
+ }
830
+ if (hasParameter('loginterfaces')) {
831
+ const { logInterfaces } = await import('@matterbridge/utils');
832
+ logInterfaces();
833
+ this.shutdown = true;
834
+ return;
835
+ }
836
+ if (getParameter('add')) {
837
+ this.log.debug(`Adding plugin ${getParameter('add')}`);
838
+ await this.plugins.add(getParameter('add'));
839
+ this.shutdown = true;
840
+ return;
841
+ }
842
+ if (getParameter('remove')) {
843
+ this.log.debug(`Removing plugin ${getParameter('remove')}`);
844
+ await this.plugins.remove(getParameter('remove'));
845
+ this.shutdown = true;
846
+ return;
847
+ }
848
+ if (getParameter('enable')) {
849
+ this.log.debug(`Enabling plugin ${getParameter('enable')}`);
850
+ await this.plugins.enable(getParameter('enable'));
851
+ this.shutdown = true;
852
+ return;
853
+ }
854
+ if (getParameter('disable')) {
855
+ this.log.debug(`Disabling plugin ${getParameter('disable')}`);
856
+ await this.plugins.disable(getParameter('disable'));
857
+ this.shutdown = true;
858
+ return;
859
+ }
860
+ if (hasParameter('factoryreset')) {
861
+ this.initialized = true;
862
+ await this.shutdownProcessAndFactoryReset();
863
+ this.shutdown = true;
864
+ return;
865
+ }
866
+ // Initialize frontend
867
+ if (getIntParameter('frontend') !== 0 || getIntParameter('frontend') === undefined)
868
+ await this.frontend.start(getIntParameter('frontend'));
869
+ // Start the matter storage and create the matterbridge context
870
+ try {
871
+ await this.startMatterStorage();
872
+ if (this.aggregatorSerialNumber && this.aggregatorUniqueId && this.matterStorageService) {
873
+ const storageManager = await this.matterStorageService.open('Matterbridge');
874
+ const storageContext = storageManager?.createContext('persist');
875
+ await storageContext?.set('serialNumber', this.aggregatorSerialNumber);
876
+ await storageContext?.set('uniqueId', this.aggregatorUniqueId);
877
+ }
878
+ }
879
+ catch (error) {
880
+ this.log.fatal(`Fatal error creating matter storage: ${error instanceof Error ? error.message : error}`);
881
+ throw new Error(`Fatal error creating matter storage: ${error instanceof Error ? error.message : error}`);
882
+ }
883
+ // Clear the matterbridge context if the reset parameter is set (bridge mode)
884
+ if (hasParameter('reset') && getParameter('reset') === undefined) {
885
+ this.initialized = true;
886
+ await this.shutdownProcessAndReset();
887
+ this.shutdown = true;
888
+ return;
889
+ }
890
+ // Clear matterbridge plugin context if the reset parameter is set (childbridge mode)
891
+ if (hasParameter('reset') && getParameter('reset') !== undefined) {
892
+ this.log.debug(`Reset plugin ${getParameter('reset')}`);
893
+ const plugin = this.plugins.get(getParameter('reset'));
894
+ if (plugin) {
895
+ const matterStorageManager = await this.matterStorageService?.open(plugin.name);
896
+ if (!matterStorageManager) {
897
+ /* istanbul ignore next */
898
+ this.log.error(`Plugin ${plg}${plugin.name}${er} storageManager not found`);
899
+ }
900
+ else {
901
+ await matterStorageManager.createContext('events')?.clearAll();
902
+ await matterStorageManager.createContext('fabrics')?.clearAll();
903
+ await matterStorageManager.createContext('root')?.clearAll();
904
+ await matterStorageManager.createContext('sessions')?.clearAll();
905
+ await matterStorageManager.createContext('persist')?.clearAll();
906
+ this.log.notice(`Reset commissioning for plugin ${plg}${plugin.name}${nt} done! Remove the device from the controller.`);
907
+ }
908
+ }
909
+ else {
910
+ this.log.warn(`Plugin ${plg}${getParameter('reset')}${wr} not registerd in matterbridge`);
911
+ }
912
+ await this.stopMatterStorage();
913
+ this.shutdown = true;
914
+ return;
915
+ }
916
+ // Check in 5 minutes the latest and dev versions of matterbridge and the plugins
917
+ clearTimeout(this.checkUpdateTimeout);
918
+ this.checkUpdateTimeout = setTimeout(async () => {
919
+ // const { checkUpdates } = await import('./checkUpdates.js');
920
+ // checkUpdates(this);
921
+ const { createESMWorker } = await import('@matterbridge/thread');
922
+ createESMWorker('CheckUpdates', this.resolveWorkerDistFilePath('workerCheckUpdates.js'));
923
+ }, 300 * 1000).unref();
924
+ // Check each 12 hours the latest and dev versions of matterbridge and the plugins
925
+ clearInterval(this.checkUpdateInterval);
926
+ this.checkUpdateInterval = setInterval(async () => {
927
+ // const { checkUpdates } = await import('./checkUpdates.js');
928
+ // checkUpdates(this);
929
+ const { createESMWorker } = await import('@matterbridge/thread');
930
+ createESMWorker('CheckUpdates', this.resolveWorkerDistFilePath('workerCheckUpdates.js'));
931
+ }, 12 * 60 * 60 * 1000).unref();
932
+ // Start the matterbridge in mode test
933
+ if (hasParameter('test')) {
934
+ this.bridgeMode = 'bridge';
935
+ return;
936
+ }
937
+ // Start the matterbridge in mode controller
938
+ if (hasParameter('controller')) {
939
+ this.bridgeMode = 'controller';
940
+ await this.startController();
941
+ return;
942
+ }
943
+ // Check if the bridge mode is set and start matterbridge in bridge mode if not set
944
+ if (!hasParameter('bridge') && !hasParameter('childbridge') && (await this.nodeContext?.get('bridgeMode', '')) === '') {
945
+ this.log.info('Setting default matterbridge start mode to bridge');
946
+ await this.nodeContext?.set('bridgeMode', 'bridge');
947
+ }
948
+ // Wait delay if specified (default 2 minutes) and the system uptime is less than 5 minutes. It solves race conditions on system startup.
949
+ if (hasParameter('delay') && os.uptime() <= 60 * 5) {
950
+ const { wait } = await import('@matterbridge/utils');
951
+ const delay = getIntParameter('delay') || 120;
952
+ this.log.warn('Delay switch found with system uptime less then 5 minutes. Waiting for ' + delay + ' seconds before starting matterbridge...');
953
+ await wait(delay * 1000, 'Race condition delay', true);
954
+ }
955
+ // Wait delay if specified (default 2 minutes). It solves race conditions on docker compose startup.
956
+ if (hasParameter('fixed_delay')) {
957
+ const { wait } = await import('@matterbridge/utils');
958
+ const delay = getIntParameter('fixed_delay') || 120;
959
+ this.log.warn('Fixed delay switch found. Waiting for ' + delay + ' seconds before starting matterbridge...');
960
+ await wait(delay * 1000, 'Fixed race condition delay', true);
961
+ }
962
+ // Start matterbridge in bridge mode
963
+ if (hasParameter('bridge') || (!hasParameter('childbridge') && (await this.nodeContext?.get('bridgeMode', '')) === 'bridge')) {
964
+ this.bridgeMode = 'bridge';
965
+ this.log.debug(`Starting matterbridge in mode ${this.bridgeMode}`);
966
+ await this.startBridge();
967
+ return;
968
+ }
969
+ // Start matterbridge in childbridge mode
970
+ if (hasParameter('childbridge') || (!hasParameter('bridge') && (await this.nodeContext?.get('bridgeMode', '')) === 'childbridge')) {
971
+ this.bridgeMode = 'childbridge';
972
+ this.log.debug(`Starting matterbridge in mode ${this.bridgeMode}`);
973
+ await this.startChildbridge();
974
+ return;
975
+ }
976
+ }
977
+ /**
978
+ * Asynchronously loads and starts the registered plugins.
979
+ *
980
+ * This method is responsible for initializing and starting all enabled plugins.
981
+ * It ensures that each plugin is properly loaded and started before the bridge starts.
982
+ *
983
+ * @param {boolean} [wait] - If true, the method will wait for all plugins to be fully loaded and started before resolving. Defaults to false.
984
+ * @param {boolean} [start] - If true, the method will start the plugins after loading them. Defaults to true.
985
+ * @returns {Promise<void>} A promise that resolves when all plugins have been loaded and started.
986
+ */
987
+ async startPlugins(wait = false, start = true) {
988
+ // Check, load and start the plugins
989
+ for (const plugin of this.plugins) {
990
+ plugin.configJson = await this.plugins.loadConfig(plugin);
991
+ plugin.schemaJson = await this.plugins.loadSchema(plugin);
992
+ // Check if the plugin is available
993
+ if (!(await this.plugins.resolve(plugin.path))) {
994
+ this.log.error(`Plugin ${plg}${plugin.name}${er} not found or not validated. Disabling it.`);
995
+ plugin.enabled = false;
996
+ plugin.error = true;
997
+ continue;
998
+ }
999
+ if (!plugin.enabled) {
1000
+ this.log.info(`Plugin ${plg}${plugin.name}${nf} not enabled`);
1001
+ continue;
1002
+ }
1003
+ plugin.error = false;
1004
+ plugin.locked = false;
1005
+ plugin.loaded = false;
1006
+ plugin.started = false;
1007
+ plugin.configured = false;
1008
+ plugin.registeredDevices = undefined;
1009
+ if (wait)
1010
+ await this.plugins.load(plugin, start, 'Matterbridge is starting');
1011
+ else
1012
+ this.plugins.load(plugin, start, 'Matterbridge is starting'); // No await do it asyncronously
1013
+ }
1014
+ this.frontend.wssSendRefreshRequired('plugins');
1015
+ }
1016
+ /**
1017
+ * Registers the process handlers for uncaughtException, unhandledRejection, SIGINT and SIGTERM.
1018
+ * - When an uncaught exception occurs, the exceptionHandler logs the error message and stack trace.
1019
+ * - When an unhandled promise rejection occurs, the rejectionHandler logs the reason and stack trace.
1020
+ * - When either of SIGINT and SIGTERM signals are received, the cleanup method is called with an appropriate message.
1021
+ */
1022
+ registerProcessHandlers() {
1023
+ this.log.debug(`Registering uncaughtException and unhandledRejection handlers...`);
1024
+ process.removeAllListeners('uncaughtException');
1025
+ process.removeAllListeners('unhandledRejection');
1026
+ this.exceptionHandler = async (error) => {
1027
+ const errorMessage = error instanceof Error ? error.message : error;
1028
+ const errorInspect = inspect(error, { depth: 10 });
1029
+ this.log.error(`Unhandled Exception detected: ${errorMessage}\nstack: ${errorInspect}}`);
1030
+ };
1031
+ process.on('uncaughtException', this.exceptionHandler);
1032
+ this.rejectionHandler = async (reason, promise) => {
1033
+ const errorMessage = reason instanceof Error ? reason.message : reason;
1034
+ const errorInspect = inspect(reason, { depth: 10 });
1035
+ this.log.error(`Unhandled Rejection detected: ${promise}\nreason: ${errorMessage}\nstack: ${errorInspect}`);
1036
+ };
1037
+ process.on('unhandledRejection', this.rejectionHandler);
1038
+ this.log.debug(`Registering SIGINT and SIGTERM signal handlers...`);
1039
+ this.sigintHandler = async () => {
1040
+ await this.cleanup('SIGINT received, cleaning up...');
1041
+ };
1042
+ process.on('SIGINT', this.sigintHandler);
1043
+ this.sigtermHandler = async () => {
1044
+ await this.cleanup('SIGTERM received, cleaning up...');
1045
+ };
1046
+ process.on('SIGTERM', this.sigtermHandler);
1047
+ }
1048
+ /**
1049
+ * Deregisters the process uncaughtException, unhandledRejection, SIGINT and SIGTERM signal handlers.
1050
+ */
1051
+ deregisterProcessHandlers() {
1052
+ this.log.debug(`Deregistering uncaughtException and unhandledRejection handlers...`);
1053
+ if (this.exceptionHandler)
1054
+ process.off('uncaughtException', this.exceptionHandler);
1055
+ this.exceptionHandler = undefined;
1056
+ if (this.rejectionHandler)
1057
+ process.off('unhandledRejection', this.rejectionHandler);
1058
+ this.rejectionHandler = undefined;
1059
+ this.log.debug(`Deregistering SIGINT and SIGTERM signal handlers...`);
1060
+ if (this.sigintHandler)
1061
+ process.off('SIGINT', this.sigintHandler);
1062
+ this.sigintHandler = undefined;
1063
+ if (this.sigtermHandler)
1064
+ process.off('SIGTERM', this.sigtermHandler);
1065
+ this.sigtermHandler = undefined;
1066
+ }
1067
+ /**
1068
+ * Logs the node and system information.
1069
+ *
1070
+ * @remarks
1071
+ * This method retrieves and logs various details about the host system, including:
1072
+ * - IP address information (IPv4, IPv6, MAC address)
1073
+ * - Node.js version
1074
+ * - Hostname and user information
1075
+ * - Operating system details (type, release, platform, architecture)
1076
+ * - Memory usage statistics
1077
+ * - Uptime information for both the system and the process
1078
+ */
1079
+ async logNodeAndSystemInfo() {
1080
+ // IP address information
1081
+ const excludedInterfaceNamePattern = /(tailscale|wireguard|openvpn|zerotier|hamachi|\bwg\d+\b|\btun\d+\b|\btap\d+\b|\butun\d+\b|docker|podman|\bveth[a-z0-9]*\b|\bbr-[a-z0-9]+\b|cni|kube|flannel|calico|virbr\d*\b|vmware|vmnet\d*\b|virtualbox|vboxnet\d*\b|teredo|isatap)/i;
1082
+ const networkInterfaces = os.networkInterfaces();
1083
+ this.systemInformation.interfaceName = '';
1084
+ this.systemInformation.ipv4Address = '';
1085
+ this.systemInformation.ipv6Address = '';
1086
+ this.systemInformation.macAddress = '';
1087
+ for (const [interfaceName, interfaceDetails] of Object.entries(networkInterfaces)) {
1088
+ if (this.mdnsInterface && interfaceName !== this.mdnsInterface)
1089
+ continue;
1090
+ if (!this.mdnsInterface && excludedInterfaceNamePattern.test(interfaceName))
1091
+ continue;
1092
+ if (!interfaceDetails)
1093
+ continue;
1094
+ for (const detail of interfaceDetails) {
1095
+ if (detail.family === 'IPv4' && !detail.internal && this.systemInformation.ipv4Address === '') {
1096
+ this.systemInformation.interfaceName = interfaceName;
1097
+ this.systemInformation.ipv4Address = detail.address;
1098
+ this.systemInformation.macAddress = detail.mac;
1099
+ }
1100
+ else if (detail.family === 'IPv6' && !detail.internal && this.systemInformation.ipv6Address === '') {
1101
+ this.systemInformation.interfaceName = interfaceName;
1102
+ this.systemInformation.ipv6Address = detail.address;
1103
+ this.systemInformation.macAddress = detail.mac;
1104
+ }
1105
+ }
1106
+ if (this.systemInformation.ipv4Address !== '' || this.systemInformation.ipv6Address !== '') {
1107
+ this.log.debug(`Using interface: '${this.systemInformation.interfaceName}'`);
1108
+ this.log.debug(`- with MAC address: '${this.systemInformation.macAddress}'`);
1109
+ this.log.debug(`- with IPv4 address: '${this.systemInformation.ipv4Address}'`);
1110
+ this.log.debug(`- with IPv6 address: '${this.systemInformation.ipv6Address}'`);
1111
+ break;
1112
+ }
1113
+ }
1114
+ // Node information
1115
+ this.systemInformation.nodeVersion = process.versions.node;
1116
+ const versionMajor = parseInt(this.systemInformation.nodeVersion.split('.')[0]);
1117
+ const versionMinor = parseInt(this.systemInformation.nodeVersion.split('.')[1]);
1118
+ const versionPatch = parseInt(this.systemInformation.nodeVersion.split('.')[2]);
1119
+ // Host system information
1120
+ this.systemInformation.hostname = os.hostname();
1121
+ this.systemInformation.user = os.userInfo().username;
1122
+ this.systemInformation.osType = os.type(); // "Windows_NT", "Darwin", etc.
1123
+ this.systemInformation.osRelease = os.release(); // Kernel version
1124
+ this.systemInformation.osPlatform = os.platform(); // "win32", "linux", "darwin", etc.
1125
+ this.systemInformation.osArch = os.arch(); // "x64", "arm", etc.
1126
+ this.systemInformation.totalMemory = formatBytes(os.totalmem());
1127
+ this.systemInformation.freeMemory = formatBytes(os.freemem());
1128
+ this.systemInformation.systemUptime = formatUptime(os.uptime());
1129
+ this.systemInformation.processUptime = formatUptime(process.uptime());
1130
+ this.systemInformation.cpuUsage = formatPercent(0);
1131
+ this.systemInformation.processCpuUsage = formatPercent(0);
1132
+ this.systemInformation.rss = formatBytes(process.memoryUsage().rss);
1133
+ this.systemInformation.heapTotal = formatBytes(process.memoryUsage().heapTotal);
1134
+ this.systemInformation.heapUsed = formatBytes(process.memoryUsage().heapUsed);
1135
+ // Log the system information
1136
+ this.log.debug('Host System Information:');
1137
+ this.log.debug(`- Hostname: ${this.systemInformation.hostname}`);
1138
+ this.log.debug(`- User: ${this.systemInformation.user}`);
1139
+ this.log.debug(`- Interface: ${this.systemInformation.interfaceName}`);
1140
+ this.log.debug(`- MAC Address: ${this.systemInformation.macAddress}`);
1141
+ this.log.debug(`- IPv4 Address: ${this.systemInformation.ipv4Address}`);
1142
+ this.log.debug(`- IPv6 Address: ${this.systemInformation.ipv6Address}`);
1143
+ this.log.debug(`- Node.js: ${versionMajor}.${versionMinor}.${versionPatch}`);
1144
+ this.log.debug(`- OS Type: ${this.systemInformation.osType}`);
1145
+ this.log.debug(`- OS Release: ${this.systemInformation.osRelease}`);
1146
+ this.log.debug(`- Platform: ${this.systemInformation.osPlatform}`);
1147
+ this.log.debug(`- Architecture: ${this.systemInformation.osArch}`);
1148
+ this.log.debug(`- Total Memory: ${this.systemInformation.totalMemory}`);
1149
+ this.log.debug(`- Free Memory: ${this.systemInformation.freeMemory}`);
1150
+ this.log.debug(`- System Uptime: ${this.systemInformation.systemUptime}`);
1151
+ this.log.debug(`- Process Uptime: ${this.systemInformation.processUptime}`);
1152
+ this.log.debug(`- RSS: ${this.systemInformation.rss}`);
1153
+ this.log.debug(`- Heap Total: ${this.systemInformation.heapTotal}`);
1154
+ this.log.debug(`- Heap Used: ${this.systemInformation.heapUsed}`);
1155
+ // Log directories
1156
+ this.log.debug(`Root Directory: ${this.rootDirectory}`);
1157
+ this.log.debug(`Home Directory: ${this.homeDirectory}`);
1158
+ this.log.debug(`Matterbridge Directory: ${this.matterbridgeDirectory}`);
1159
+ this.log.debug(`Matterbridge Plugin Directory: ${this.matterbridgePluginDirectory}`);
1160
+ this.log.debug(`Matterbridge Matter Certificate Directory: ${this.matterbridgeCertDirectory}`);
1161
+ // Global node_modules directory
1162
+ if (this.nodeContext)
1163
+ this.globalModulesDirectory = await this.nodeContext.get('globalModulesDirectory', '');
1164
+ if (this.globalModulesDirectory === '') {
1165
+ // First run of Matterbridge so the node storage is empty
1166
+ this.log.debug(`Getting global node_modules directory...`);
1167
+ try {
1168
+ const { getGlobalNodeModules } = await import('@matterbridge/utils');
1169
+ this.globalModulesDirectory = await getGlobalNodeModules();
1170
+ this.log.debug(`Global node_modules Directory: ${this.globalModulesDirectory}`);
1171
+ await this.nodeContext?.set('globalModulesDirectory', this.globalModulesDirectory);
1172
+ }
1173
+ catch (error) {
1174
+ this.log.error(`Error getting global node_modules directory: ${error}`);
1175
+ }
1176
+ }
1177
+ else {
1178
+ // The global node_modules directory is already set in the node storage and we check if it is still valid
1179
+ this.log.debug(`Global node_modules Directory: ${this.globalModulesDirectory}`);
1180
+ const { createESMWorker } = await import('@matterbridge/thread');
1181
+ createESMWorker('NpmGlobalPrefix', this.resolveWorkerDistFilePath('workerGlobalPrefix.js'));
1182
+ }
1183
+ // Matterbridge version
1184
+ this.log.debug(`Reading matterbridge package.json...`);
1185
+ const packageJson = JSON.parse(await fs.promises.readFile(path.join(this.rootDirectory, 'package.json'), 'utf-8'));
1186
+ this.matterbridgeVersion = this.matterbridgeLatestVersion = this.matterbridgeDevVersion = packageJson.version;
1187
+ this.log.debug(`Matterbridge Version: ${this.matterbridgeVersion}`);
1188
+ // Matterbridge latest version (will be set in the checkUpdate function)
1189
+ if (this.nodeContext)
1190
+ this.matterbridgeLatestVersion = await this.nodeContext.get('matterbridgeLatestVersion', this.matterbridgeVersion);
1191
+ this.log.debug(`Matterbridge Latest Version: ${this.matterbridgeLatestVersion}`);
1192
+ // Matterbridge dev version (will be set in the checkUpdate function)
1193
+ if (this.nodeContext)
1194
+ this.matterbridgeDevVersion = await this.nodeContext.get('matterbridgeDevVersion', this.matterbridgeVersion);
1195
+ this.log.debug(`Matterbridge Dev Version: ${this.matterbridgeDevVersion}`);
1196
+ // Frontend version
1197
+ this.log.debug(`Reading frontend package.json...`);
1198
+ const frontendPackageJson = JSON.parse(await fs.promises.readFile(path.join(this.rootDirectory, 'apps', 'frontend', 'package.json'), 'utf8'));
1199
+ this.frontendVersion = frontendPackageJson.version;
1200
+ this.log.debug(`Frontend version ${CYAN}${this.frontendVersion}${db}`);
1201
+ // Current working directory
1202
+ const currentDir = process.cwd();
1203
+ this.log.debug(`Current Working Directory: ${currentDir}`);
1204
+ // Command line arguments (excluding 'node' and the script name)
1205
+ const cmdArgs = process.argv.slice(2).join(' ');
1206
+ this.log.debug(`Command Line Arguments: ${cmdArgs}`);
1207
+ }
1208
+ /**
1209
+ * Set the logger logLevel for the Matterbridge classes and call onChangeLoggerLevel() for each plugin.
1210
+ *
1211
+ * @param {LogLevel} logLevel The logger logLevel to set.
1212
+ * @returns {Promise<LogLevel>} A promise that resolves when the logLevel has been set.
1213
+ */
1214
+ async setLogLevel(logLevel) {
1215
+ this.logLevel = logLevel;
1216
+ this.log.logLevel = logLevel;
1217
+ this.frontend.logLevel = logLevel;
1218
+ MatterbridgeEndpoint.logLevel = logLevel;
1219
+ this.devices.logLevel = logLevel;
1220
+ this.plugins.logLevel = logLevel;
1221
+ let pluginDebug = false;
1222
+ for (const plugin of this.plugins) {
1223
+ if (!plugin.platform || !plugin.platform.log || !plugin.platform.config)
1224
+ continue;
1225
+ if (plugin.platform.config.debug === true)
1226
+ pluginDebug = true;
1227
+ plugin.platform.log.logLevel = plugin.platform.config.debug === true ? "debug" /* LogLevel.DEBUG */ : logLevel;
1228
+ await plugin.platform.onChangeLoggerLevel(plugin.platform.config.debug === true ? "debug" /* LogLevel.DEBUG */ : logLevel);
1229
+ }
1230
+ // Set the global logger callback for the WebSocketServer to the common minimum logLevel
1231
+ let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
1232
+ if (logLevel === "info" /* LogLevel.INFO */ || Logger.level === MatterLogLevel.INFO)
1233
+ callbackLogLevel = "info" /* LogLevel.INFO */;
1234
+ if (logLevel === "debug" /* LogLevel.DEBUG */ || Logger.level === MatterLogLevel.DEBUG || pluginDebug)
1235
+ callbackLogLevel = "debug" /* LogLevel.DEBUG */;
1236
+ AnsiLogger.setGlobalCallbackLevel(callbackLogLevel);
1237
+ this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
1238
+ return logLevel;
1239
+ }
1240
+ /**
1241
+ * Get the current logger logLevel.
1242
+ *
1243
+ * @returns {LogLevel} The current logger logLevel.
1244
+ */
1245
+ getLogLevel() {
1246
+ return this.log.logLevel;
1247
+ }
1248
+ /**
1249
+ * Creates a MatterLogger function to show the matter.js log messages in AnsiLogger (for the frontend).
1250
+ * It also logs to file (matter.log) if fileLogger is true.
1251
+ *
1252
+ * @param {boolean} fileLogger - Whether to log to file or not.
1253
+ * @returns {(text: string, message: Diagnostic.Message) => void} The MatterLogger function. \x1b[35m for violet \x1b[34m is blue
1254
+ */
1255
+ createDestinationMatterLogger(fileLogger) {
1256
+ if (fileLogger) {
1257
+ this.matterLog.logFilePath = path.join(this.matterbridgeDirectory, MATTER_LOGGER_FILE);
1258
+ }
1259
+ return (text, message) => {
1260
+ // 2024-08-21 08:55:19.488 DEBUG InteractionMessenger Sending DataReport chunk with 28 attributes and 0 events: 1004 bytes
1261
+ const logger = text.slice(44, 44 + 20).trim();
1262
+ const msg = text.slice(65);
1263
+ this.matterLog.logName = logger;
1264
+ switch (message.level) {
1265
+ case MatterLogLevel.DEBUG:
1266
+ this.matterLog.log("debug" /* LogLevel.DEBUG */, msg);
1267
+ break;
1268
+ case MatterLogLevel.INFO:
1269
+ this.matterLog.log("info" /* LogLevel.INFO */, msg);
1270
+ break;
1271
+ case MatterLogLevel.NOTICE:
1272
+ this.matterLog.log("notice" /* LogLevel.NOTICE */, msg);
1273
+ break;
1274
+ case MatterLogLevel.WARN:
1275
+ this.matterLog.log("warn" /* LogLevel.WARN */, msg);
1276
+ break;
1277
+ case MatterLogLevel.ERROR:
1278
+ this.matterLog.log("error" /* LogLevel.ERROR */, msg);
1279
+ break;
1280
+ case MatterLogLevel.FATAL:
1281
+ this.matterLog.log("fatal" /* LogLevel.FATAL */, msg);
1282
+ break;
1283
+ }
1284
+ };
1285
+ }
1286
+ /**
1287
+ * Restarts the process by exiting the current instance and loading a new instance (/api/restart).
1288
+ *
1289
+ * @returns {Promise<void>} A promise that resolves when the restart is completed.
1290
+ */
1291
+ async restartProcess() {
1292
+ await this.cleanup('restarting...', true);
1293
+ }
1294
+ /**
1295
+ * Shut down the process (/api/shutdown).
1296
+ *
1297
+ * @returns {Promise<void>} A promise that resolves when the shutdown is completed.
1298
+ */
1299
+ async shutdownProcess() {
1300
+ await this.cleanup('shutting down...', false);
1301
+ }
1302
+ /**
1303
+ * Update matterbridge and shut down the process (virtual device 'Update Matterbridge').
1304
+ *
1305
+ * @returns {Promise<void>} A promise that resolves when the update is completed.
1306
+ */
1307
+ async updateProcess() {
1308
+ this.log.info('Updating matterbridge...');
1309
+ const { spawnCommand } = await import('./spawn.js');
1310
+ if (await spawnCommand('npm', ['install', '-g', 'matterbridge', '--omit=dev', '--verbose'], 'install', 'matterbridge')) {
1311
+ this.log.info('Matterbridge has been updated. Full restart required.');
1312
+ }
1313
+ else {
1314
+ this.log.error('Error updating matterbridge.');
1315
+ }
1316
+ this.frontend.wssSendRestartRequired();
1317
+ await this.cleanup('updating...', false);
1318
+ }
1319
+ /**
1320
+ * Unregister all devices and shut down the process (/api/unregister).
1321
+ *
1322
+ * @param {number} [timeout] - The timeout duration to wait for the message exchange to complete in milliseconds. Default is 1000.
1323
+ *
1324
+ * @returns {Promise<void>} A promise that resolves when the cleanup is completed.
1325
+ */
1326
+ async unregisterAndShutdownProcess(timeout = 1000) {
1327
+ const { wait } = await import('@matterbridge/utils');
1328
+ this.log.info('Unregistering all devices and shutting down...');
1329
+ for (const plugin of this.plugins.array()) {
1330
+ if (plugin.error || !plugin.enabled)
1331
+ continue;
1332
+ const registeredDevices = plugin.registeredDevices;
1333
+ await this.plugins.shutdown(plugin, 'unregistering all devices and shutting down...', false, true);
1334
+ plugin.registeredDevices = registeredDevices;
1335
+ await this.removeAllBridgedEndpoints(plugin.name, 100);
1336
+ }
1337
+ this.log.debug('Waiting for the MessageExchange to finish...');
1338
+ await wait(timeout); // Wait for MessageExchange to finish
1339
+ this.log.debug('Cleaning up and shutting down...');
1340
+ await this.cleanup('unregistered all devices and shutting down...', false, timeout);
1341
+ }
1342
+ /**
1343
+ * Reset commissioning and shut down the process (/api/reset).
1344
+ *
1345
+ * @returns {Promise<void>} A promise that resolves when the cleanup is completed.
1346
+ */
1347
+ async shutdownProcessAndReset() {
1348
+ await this.cleanup('shutting down with reset...', false);
1349
+ }
1350
+ /**
1351
+ * Factory reset and shut down the process (/api/factory-reset).
1352
+ *
1353
+ * @returns {Promise<void>} A promise that resolves when the cleanup is completed.
1354
+ */
1355
+ async shutdownProcessAndFactoryReset() {
1356
+ await this.cleanup('shutting down with factory reset...', false);
1357
+ }
1358
+ /**
1359
+ * Cleans up the Matterbridge instance.
1360
+ *
1361
+ * @param {string} message - The cleanup message.
1362
+ * @param {boolean} [restart] - Indicates whether to restart the instance after cleanup. Default is `false`.
1363
+ * @param {number} [pause] - The pause in ms to wait for the message exchange to complete in milliseconds. Default is 1000.
1364
+ *
1365
+ * @returns {Promise<void>} A promise that resolves when the cleanup is completed.
1366
+ */
1367
+ async cleanup(message, restart = false, pause = 1000) {
1368
+ if (this.initialized && !this.hasCleanupStarted) {
1369
+ this.emit('cleanup_started');
1370
+ this.hasCleanupStarted = true;
1371
+ this.log.info(message);
1372
+ // Clear the start matter interval
1373
+ if (this.startMatterInterval) {
1374
+ clearInterval(this.startMatterInterval);
1375
+ this.startMatterInterval = undefined;
1376
+ this.log.debug('Start matter interval cleared');
1377
+ }
1378
+ // Clear the check update timeout
1379
+ if (this.checkUpdateTimeout) {
1380
+ clearTimeout(this.checkUpdateTimeout);
1381
+ this.checkUpdateTimeout = undefined;
1382
+ this.log.debug('Check update timeout cleared');
1383
+ }
1384
+ // Clear the check update interval
1385
+ if (this.checkUpdateInterval) {
1386
+ clearInterval(this.checkUpdateInterval);
1387
+ this.checkUpdateInterval = undefined;
1388
+ this.log.debug('Check update interval cleared');
1389
+ }
1390
+ // Clear the configure timeout
1391
+ if (this.configureTimeout) {
1392
+ clearTimeout(this.configureTimeout);
1393
+ this.configureTimeout = undefined;
1394
+ this.log.debug('Matterbridge configure timeout cleared');
1395
+ }
1396
+ // Clear the reachability timeout
1397
+ if (this.reachabilityTimeout) {
1398
+ clearTimeout(this.reachabilityTimeout);
1399
+ this.reachabilityTimeout = undefined;
1400
+ this.log.debug('Matterbridge reachability timeout cleared');
1401
+ }
1402
+ // Call the shutdown method of each plugin and clear the plugins reachability timeout
1403
+ for (const plugin of this.plugins) {
1404
+ if (!plugin.enabled || plugin.error)
1405
+ continue;
1406
+ await this.plugins.shutdown(plugin, 'Matterbridge is closing: ' + message, false);
1407
+ if (plugin.reachabilityTimeout) {
1408
+ clearTimeout(plugin.reachabilityTimeout);
1409
+ plugin.reachabilityTimeout = undefined;
1410
+ this.log.debug(`Plugin ${plg}${plugin.name}${db} reachability timeout cleared`);
1411
+ }
1412
+ }
1413
+ // Stop matter server nodes
1414
+ this.log.notice(`Stopping matter server nodes in ${this.bridgeMode} mode...`);
1415
+ if (pause > 0) {
1416
+ const { wait } = await import('@matterbridge/utils');
1417
+ this.log.debug(`Waiting ${pause}ms for the MessageExchange to finish...`);
1418
+ await wait(pause, `Waiting ${pause}ms for the MessageExchange to finish...`, false);
1419
+ }
1420
+ if (this.bridgeMode === 'bridge') {
1421
+ if (this.serverNode) {
1422
+ await this.stopServerNode(this.serverNode);
1423
+ this.serverNode = undefined;
1424
+ }
1425
+ }
1426
+ if (this.bridgeMode === 'childbridge') {
1427
+ for (const plugin of this.plugins.array()) {
1428
+ if (plugin.serverNode) {
1429
+ await this.stopServerNode(plugin.serverNode);
1430
+ plugin.serverNode = undefined;
1431
+ }
1432
+ }
1433
+ }
1434
+ for (const device of this.devices.array()) {
1435
+ if (device.mode === 'server' && device.serverNode) {
1436
+ await this.stopServerNode(device.serverNode);
1437
+ device.serverNode = undefined;
1438
+ }
1439
+ }
1440
+ this.log.notice('Stopped matter server nodes');
1441
+ // Matter commisioning reset
1442
+ if (message === 'shutting down with reset...') {
1443
+ this.log.info('Resetting Matterbridge commissioning information...');
1444
+ await this.matterStorageManager?.createContext('events')?.clearAll();
1445
+ await this.matterStorageManager?.createContext('fabrics')?.clearAll();
1446
+ await this.matterStorageManager?.createContext('root')?.clearAll();
1447
+ await this.matterStorageManager?.createContext('sessions')?.clearAll();
1448
+ await this.matterbridgeContext?.clearAll();
1449
+ this.log.info('Matter storage reset done! Remove the bridge from the controller.');
1450
+ }
1451
+ // Unregister all devices
1452
+ if (message === 'unregistered all devices and shutting down...') {
1453
+ if (this.bridgeMode === 'bridge') {
1454
+ await this.matterStorageManager?.createContext('root')?.createContext('parts')?.createContext('Matterbridge')?.createContext('parts')?.clearAll();
1455
+ await this.matterStorageManager?.createContext('root')?.createContext('subscription')?.clearAll();
1456
+ await this.matterStorageManager?.createContext('sessions')?.clearAll();
1457
+ }
1458
+ else if (this.bridgeMode === 'childbridge') {
1459
+ for (const plugin of this.plugins.array()) {
1460
+ if (plugin.type === 'DynamicPlatform') {
1461
+ await plugin.storageContext?.createContext('root')?.createContext('parts')?.createContext(plugin.name)?.createContext('parts')?.clearAll();
1462
+ }
1463
+ await plugin.storageContext?.createContext('root')?.createContext('subscription')?.clearAll();
1464
+ await plugin.storageContext?.createContext('sessions')?.clearAll();
1465
+ }
1466
+ }
1467
+ this.log.info('Matter storage reset done!');
1468
+ }
1469
+ // Stop matter storage
1470
+ await this.stopMatterStorage();
1471
+ /**
1472
+ * Unlink a file safely, ignoring errors.
1473
+ *
1474
+ * @param {string} path - The path to the file to unlink.
1475
+ * @param {AnsiLogger} log - The logger to use for logging.
1476
+ */
1477
+ function unlinkSafe(path, log) {
1478
+ try {
1479
+ log.debug(`Removing ${path}...`);
1480
+ unlinkSync(path);
1481
+ log.debug(`Removed ${path}`);
1482
+ }
1483
+ catch {
1484
+ // Ignore errors if the file does not exist
1485
+ }
1486
+ }
1487
+ // Remove the resumption records for Matterbridge (bridge mode)
1488
+ this.log.debug(`Cleaning matter storage context for ${GREEN}Matterbridge${db}...`);
1489
+ unlinkSafe(path.join(this.matterbridgeDirectory, MATTER_STORAGE_NAME, 'Matterbridge', 'sessions.resumptionRecords'), this.log);
1490
+ unlinkSafe(path.join(this.matterbridgeDirectory, MATTER_STORAGE_NAME, 'Matterbridge', 'root.subscriptions.subscriptions'), this.log);
1491
+ for (const plugin of this.plugins.array()) {
1492
+ // Remove the resumption records for the plugins (childbridge mode)
1493
+ this.log.debug(`Cleaning matter storage context for plugin ${plg}${plugin.name}${db}...`);
1494
+ unlinkSafe(path.join(this.matterbridgeDirectory, MATTER_STORAGE_NAME, plugin.name, 'sessions.resumptionRecords'), this.log);
1495
+ unlinkSafe(path.join(this.matterbridgeDirectory, MATTER_STORAGE_NAME, plugin.name, 'root.subscriptions.subscriptions'), this.log);
1496
+ }
1497
+ for (const device of this.devices.array().filter((d) => d.mode === 'server')) {
1498
+ if (!device.deviceName)
1499
+ continue;
1500
+ // Remove the resumption records for the server mode devices
1501
+ this.log.debug(`Cleaning matter storage context for server node device ${dev}${device.deviceName}${db}...`);
1502
+ unlinkSafe(path.join(this.matterbridgeDirectory, MATTER_STORAGE_NAME, device.deviceName.replace(/[ .]/g, ''), 'sessions.resumptionRecords'), this.log);
1503
+ unlinkSafe(path.join(this.matterbridgeDirectory, MATTER_STORAGE_NAME, device.deviceName.replace(/[ .]/g, ''), 'root.subscriptions.subscriptions'), this.log);
1504
+ }
1505
+ // Stop the frontend
1506
+ await this.frontend.stop();
1507
+ this.frontend.destroy();
1508
+ // Close PluginManager and DeviceManager
1509
+ this.plugins.destroy();
1510
+ this.devices.destroy();
1511
+ // Stop thread messaging server
1512
+ this.server.close();
1513
+ // Close the matterbridge node storage and context
1514
+ if (this.nodeStorage && this.nodeContext) {
1515
+ /*
1516
+ TODO: Implement serialization of registered devices
1517
+ this.log.info('Saving registered devices...');
1518
+ const serializedRegisteredDevices: SerializedMatterbridgeEndpoint[] = [];
1519
+ this.devices.forEach(async (device) => {
1520
+ const serializedMatterbridgeDevice = MatterbridgeEndpoint.serialize(device);
1521
+ this.log.info(`- ${serializedMatterbridgeDevice.deviceName}${rs}\n`, serializedMatterbridgeDevice);
1522
+ if (serializedMatterbridgeDevice) serializedRegisteredDevices.push(serializedMatterbridgeDevice);
1523
+ });
1524
+ await this.nodeContext.set<SerializedMatterbridgeEndpoint[]>('devices', serializedRegisteredDevices);
1525
+ this.log.info(`Saved registered devices (${serializedRegisteredDevices?.length})`);
1526
+ */
1527
+ // Clear nodeContext and nodeStorage (they just need 1000ms to write the data to disk)
1528
+ this.log.debug(`Closing node storage context for ${plg}Matterbridge${db}...`);
1529
+ await this.nodeContext.close();
1530
+ this.nodeContext = undefined;
1531
+ // Clear nodeContext for each plugin (they just need 1000ms to write the data to disk)
1532
+ for (const plugin of this.plugins) {
1533
+ if (plugin.nodeContext) {
1534
+ this.log.debug(`Closing node storage context for plugin ${plg}${plugin.name}${db}...`);
1535
+ await plugin.nodeContext.close();
1536
+ plugin.nodeContext = undefined;
1537
+ }
1538
+ }
1539
+ this.log.debug('Closing node storage manager...');
1540
+ await this.nodeStorage.close();
1541
+ this.nodeStorage = undefined;
1542
+ }
1543
+ else {
1544
+ this.log.error('Error close the matterbridge node storage and context: nodeStorage or nodeContext not found!');
1545
+ }
1546
+ this.plugins.clear();
1547
+ this.devices.clear();
1548
+ // Factory reset
1549
+ if (message === 'shutting down with factory reset...') {
1550
+ try {
1551
+ // Delete matter storage directory with its subdirectories and backup
1552
+ const dir = path.join(this.matterbridgeDirectory, MATTER_STORAGE_NAME);
1553
+ this.log.info(`Removing matter storage directory: ${dir}`);
1554
+ await fs.promises.rm(dir, { recursive: true });
1555
+ const backup = path.join(this.matterbridgeDirectory, MATTER_STORAGE_NAME + '.backup');
1556
+ this.log.info(`Removing matter storage backup directory: ${backup}`);
1557
+ await fs.promises.rm(backup, { recursive: true });
1558
+ }
1559
+ catch (error) {
1560
+ // istanbul ignore next if
1561
+ if (error instanceof Error && error.code !== 'ENOENT') {
1562
+ this.log.error(`Error removing matter storage directory: ${error}`);
1563
+ }
1564
+ }
1565
+ try {
1566
+ // Delete matterbridge storage directory with its subdirectories and backup
1567
+ const dir = path.join(this.matterbridgeDirectory, NODE_STORAGE_DIR);
1568
+ this.log.info(`Removing matterbridge storage directory: ${dir}`);
1569
+ await fs.promises.rm(dir, { recursive: true });
1570
+ const backup = path.join(this.matterbridgeDirectory, NODE_STORAGE_DIR + '.backup');
1571
+ this.log.info(`Removing matterbridge storage backup directory: ${backup}`);
1572
+ await fs.promises.rm(backup, { recursive: true });
1573
+ }
1574
+ catch (error) {
1575
+ // istanbul ignore next if
1576
+ if (error instanceof Error && error.code !== 'ENOENT') {
1577
+ this.log.error(`Error removing matterbridge storage directory: ${error}`);
1578
+ }
1579
+ }
1580
+ this.log.info('Factory reset done! Remove all paired fabrics from the controllers.');
1581
+ }
1582
+ // Deregisters the process handlers
1583
+ this.deregisterProcessHandlers();
1584
+ if (restart) {
1585
+ if (message === 'updating...') {
1586
+ this.log.info('Cleanup completed. Updating...');
1587
+ Matterbridge.instance = undefined;
1588
+ this.emit('update'); // Restart the process but the update has been done before. TODO move all updates to the cli
1589
+ }
1590
+ else if (message === 'restarting...') {
1591
+ this.log.info('Cleanup completed. Restarting...');
1592
+ Matterbridge.instance = undefined;
1593
+ this.emit('restart');
1594
+ }
1595
+ }
1596
+ else {
1597
+ this.log.notice('Cleanup completed. Shutting down...');
1598
+ Matterbridge.instance = undefined;
1599
+ this.emit('shutdown');
1600
+ }
1601
+ this.hasCleanupStarted = false;
1602
+ this.initialized = false;
1603
+ this.emit('cleanup_completed');
1604
+ }
1605
+ else {
1606
+ if (!this.initialized) {
1607
+ this.log.debug('Cleanup with instance not initialized...');
1608
+ this.destroy();
1609
+ this.frontend.destroy();
1610
+ this.plugins.destroy();
1611
+ this.devices.destroy();
1612
+ }
1613
+ if (this.hasCleanupStarted)
1614
+ this.log.debug('Cleanup already started...');
1615
+ }
1616
+ }
1617
+ /**
1618
+ * Starts the Matterbridge in bridge mode.
1619
+ *
1620
+ * @private
1621
+ * @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
1622
+ */
1623
+ async startBridge() {
1624
+ // Plugins are configured by a timer when matter server is started and plugin.configured is set to true
1625
+ if (!this.matterStorageManager)
1626
+ throw new Error('No storage manager initialized');
1627
+ if (!this.matterbridgeContext)
1628
+ throw new Error('No storage context initialized');
1629
+ this.serverNode = await this.createServerNode(this.matterbridgeContext, this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
1630
+ this.aggregatorNode = await this.createAggregatorNode(this.matterbridgeContext);
1631
+ await this.serverNode.add(this.aggregatorNode);
1632
+ await addVirtualDevices(this, this.aggregatorNode);
1633
+ await this.startPlugins();
1634
+ this.log.debug('Starting start matter interval in bridge mode...');
1635
+ this.frontend.wssSendSnackbarMessage(`The bridge is starting...`, 0, 'info');
1636
+ let failCount = 0;
1637
+ this.startMatterInterval = setInterval(async () => {
1638
+ // istanbul ignore if cause is just a logging statement
1639
+ if (failCount && failCount % 10 === 0) {
1640
+ this.frontend.wssSendSnackbarMessage(`The bridge is still starting...`, 10, 'info');
1641
+ this.frontend.wssSendRefreshRequired('plugins');
1642
+ }
1643
+ for (const plugin of this.plugins) {
1644
+ if (!plugin.enabled)
1645
+ continue;
1646
+ if (plugin.error) {
1647
+ clearInterval(this.startMatterInterval);
1648
+ this.startMatterInterval = undefined;
1649
+ this.log.debug('Cleared startMatterInterval interval for Matterbridge for plugin in error state');
1650
+ this.log.error(`The plugin ${plg}${plugin.name}${er} is in error state.`);
1651
+ this.log.error('The bridge will not start until the problem is solved to prevent the controllers from deleting all registered devices.');
1652
+ this.log.error('If you want to start the bridge disable the plugin in error state and restart.');
1653
+ this.frontend.wssSendSnackbarMessage(`The plugin ${plugin.name} is in error state. Check the logs.`, 0, 'error');
1654
+ this.frontend.wssSendSnackbarMessage(`The bridge is offline. Startup halted due to plugin errors.`, 0, 'error');
1655
+ this.frontend.wssSendRefreshRequired('plugins');
1656
+ this.frontend.wssSendCloseSnackbarMessage(`The bridge is starting...`);
1657
+ return;
1658
+ }
1659
+ if (!plugin.loaded || !plugin.started) {
1660
+ this.log.debug(`Waiting (failSafeCount=${failCount}/${this.failCountLimit}) in startMatterInterval interval for plugin ${plg}${plugin.name}${db} loaded: ${plugin.loaded} started: ${plugin.started}...`);
1661
+ failCount++;
1662
+ if (failCount > this.failCountLimit) {
1663
+ this.log.error(`Error waiting for plugin ${plg}${plugin.name}${er} to load and start. Plugin is in error state.`);
1664
+ plugin.error = true;
1665
+ }
1666
+ return;
1667
+ }
1668
+ }
1669
+ clearInterval(this.startMatterInterval);
1670
+ this.startMatterInterval = undefined;
1671
+ this.log.debug('Cleared startMatterInterval interval in bridge mode');
1672
+ // Start the Matter server node
1673
+ this.startServerNode(this.serverNode); // We don't await this, because the server node is started in the background
1674
+ // Start the Matter server node of single devices in mode 'server'
1675
+ for (const device of this.devices.array()) {
1676
+ if (device.mode === 'server' && device.serverNode) {
1677
+ this.log.debug(`Starting server node for device ${dev}${device.deviceName}${db} in server mode...`);
1678
+ this.startServerNode(device.serverNode); // We don't await this, because the server node is started in the background
1679
+ }
1680
+ }
1681
+ // Configure the plugins
1682
+ this.configureTimeout = setTimeout(async () => {
1683
+ for (const plugin of this.plugins.array()) {
1684
+ if (!plugin.enabled || !plugin.loaded || !plugin.started || plugin.error)
1685
+ continue;
1686
+ try {
1687
+ if ((await this.plugins.configure(plugin)) === undefined) {
1688
+ if (plugin.configured !== true)
1689
+ this.frontend.wssSendSnackbarMessage(`The plugin ${plugin.name} failed to configure. Check the logs.`, 0, 'error');
1690
+ }
1691
+ }
1692
+ catch (error) {
1693
+ plugin.error = true;
1694
+ this.log.error(`Error configuring plugin ${plg}${plugin.name}${er}`, error);
1695
+ }
1696
+ }
1697
+ this.frontend.wssSendRefreshRequired('plugins');
1698
+ }, 30 * 1000).unref();
1699
+ // Setting reachability to true
1700
+ this.reachabilityTimeout = setTimeout(() => {
1701
+ this.log.info(`Setting reachability to true for ${plg}Matterbridge${db}`);
1702
+ if (this.aggregatorNode)
1703
+ this.setAggregatorReachability(this.aggregatorNode, true);
1704
+ }, 60 * 1000).unref();
1705
+ // Logger.get('LogServerNode').info(this.serverNode);
1706
+ this.emit('bridge_started');
1707
+ this.log.notice('Matterbridge bridge started successfully');
1708
+ this.frontend.wssSendRefreshRequired('settings');
1709
+ this.frontend.wssSendRefreshRequired('plugins');
1710
+ this.frontend.wssSendCloseSnackbarMessage(`The bridge is starting...`);
1711
+ }, Number(process.env['MATTERBRIDGE_START_MATTER_INTERVAL_MS']) || this.startMatterIntervalMs);
1712
+ }
1713
+ /**
1714
+ * Starts the Matterbridge in childbridge mode.
1715
+ *
1716
+ * @param {number} [delay] - The delay before starting the childbridge. Default is 1000 milliseconds.
1717
+ *
1718
+ * @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
1719
+ */
1720
+ async startChildbridge(delay = 1000) {
1721
+ if (!this.matterStorageManager)
1722
+ throw new Error('No storage manager initialized');
1723
+ const { wait } = await import('@matterbridge/utils');
1724
+ // Load with await all plugins but don't start them. We get the platform.type to pre-create server nodes for DynamicPlatform plugins
1725
+ this.log.debug('Loading all plugins in childbridge mode...');
1726
+ await this.startPlugins(true, false);
1727
+ // Create server nodes for DynamicPlatform plugins and start all plugins in the background
1728
+ this.log.debug('Creating server nodes for DynamicPlatform plugins and starting all plugins in childbridge mode...');
1729
+ for (const plugin of this.plugins.array().filter((p) => p.enabled && !p.error)) {
1730
+ if (plugin.type === 'DynamicPlatform')
1731
+ await this.createDynamicPlugin(plugin);
1732
+ this.plugins.start(plugin, 'Matterbridge is starting'); // Start the plugin in the background
1733
+ }
1734
+ // Start the Matterbridge in childbridge mode when all plugins are loaded and started
1735
+ this.log.debug('Starting start matter interval in childbridge mode...');
1736
+ this.frontend.wssSendSnackbarMessage(`The bridge is starting...`, 0, 'info');
1737
+ let failCount = 0;
1738
+ this.startMatterInterval = setInterval(async () => {
1739
+ // istanbul ignore if cause is just a logging statement
1740
+ if (failCount && failCount % 10 === 0) {
1741
+ this.frontend.wssSendSnackbarMessage(`The bridge is still starting...`, 10, 'info');
1742
+ this.frontend.wssSendRefreshRequired('plugins');
1743
+ }
1744
+ let allStarted = true;
1745
+ for (const plugin of this.plugins.array()) {
1746
+ if (!plugin.enabled)
1747
+ continue;
1748
+ if (plugin.error) {
1749
+ clearInterval(this.startMatterInterval);
1750
+ this.startMatterInterval = undefined;
1751
+ this.log.debug('Cleared startMatterInterval interval for a plugin in error state');
1752
+ this.log.error(`The plugin ${plg}${plugin.name}${er} is in error state.`);
1753
+ this.log.error('The bridge will not start until the problem is solved to prevent the controllers from deleting all registered devices.');
1754
+ this.log.error('If you want to start the bridge disable the plugin in error state and restart.');
1755
+ this.frontend.wssSendSnackbarMessage(`The plugin ${plugin.name} is in error state. Check the logs.`, 0, 'error');
1756
+ this.frontend.wssSendSnackbarMessage(`The bridge is offline. Startup halted due to plugin errors.`, 0, 'error');
1757
+ this.frontend.wssSendRefreshRequired('plugins');
1758
+ this.frontend.wssSendCloseSnackbarMessage(`The bridge is starting...`);
1759
+ return;
1760
+ }
1761
+ this.log.debug(`Checking plugin ${plg}${plugin.name}${db} to start matter in childbridge mode...`);
1762
+ if (!plugin.loaded || !plugin.started) {
1763
+ allStarted = false;
1764
+ this.log.debug(`Waiting (failSafeCount=${failCount}/${this.failCountLimit}) for plugin ${plg}${plugin.name}${db} to load (${plugin.loaded}) and start (${plugin.started}) ...`);
1765
+ failCount++;
1766
+ if (failCount > this.failCountLimit) {
1767
+ this.log.error(`Error waiting for plugin ${plg}${plugin.name}${er} to load and start. Plugin is in error state.`);
1768
+ plugin.error = true;
1769
+ }
1770
+ }
1771
+ }
1772
+ if (!allStarted)
1773
+ return;
1774
+ clearInterval(this.startMatterInterval);
1775
+ this.startMatterInterval = undefined;
1776
+ if (delay > 0)
1777
+ await wait(Number(process.env['MATTERBRIDGE_PAUSE_MATTER_INTERVAL_MS']) || delay); // Wait for the specified delay to ensure all plugins server nodes are ready
1778
+ this.log.debug('Cleared startMatterInterval interval in childbridge mode');
1779
+ // Configure the plugins
1780
+ this.configureTimeout = setTimeout(async () => {
1781
+ for (const plugin of this.plugins.array()) {
1782
+ if (!plugin.enabled || !plugin.loaded || !plugin.started || plugin.error)
1783
+ continue;
1784
+ try {
1785
+ if ((await this.plugins.configure(plugin)) === undefined) {
1786
+ if (plugin.configured !== true)
1787
+ this.frontend.wssSendSnackbarMessage(`The plugin ${plugin.name} failed to configure. Check the logs.`, 0, 'error');
1788
+ }
1789
+ }
1790
+ catch (error) {
1791
+ plugin.error = true;
1792
+ this.log.error(`Error configuring plugin ${plg}${plugin.name}${er}`, error);
1793
+ }
1794
+ }
1795
+ this.frontend.wssSendRefreshRequired('plugins');
1796
+ }, 30 * 1000).unref();
1797
+ for (const plugin of this.plugins.array()) {
1798
+ if (!plugin.enabled || plugin.error)
1799
+ continue;
1800
+ if (plugin.type !== 'DynamicPlatform' && (!plugin.registeredDevices || plugin.registeredDevices === 0)) {
1801
+ this.log.error(`Plugin ${plg}${plugin.name}${er} didn't register any devices to Matterbridge. Verify the plugin configuration.`);
1802
+ continue;
1803
+ }
1804
+ // istanbul ignore next if cause is just a safety check
1805
+ if (!plugin.serverNode) {
1806
+ this.log.error(`Server node not found for plugin ${plg}${plugin.name}${er}`);
1807
+ continue;
1808
+ }
1809
+ if (!plugin.storageContext) {
1810
+ this.log.error(`Storage context not found for plugin ${plg}${plugin.name}${er}`);
1811
+ continue;
1812
+ }
1813
+ if (!plugin.nodeContext) {
1814
+ this.log.error(`Node storage context not found for plugin ${plg}${plugin.name}${er}`);
1815
+ continue;
1816
+ }
1817
+ // Start the Matter server node
1818
+ this.startServerNode(plugin.serverNode); // We don't await this, because the server node is started in the background
1819
+ // Setting reachability to true
1820
+ plugin.reachabilityTimeout = setTimeout(() => {
1821
+ this.log.info(`Setting reachability to true for ${plg}${plugin.name}${nf}`);
1822
+ if (plugin.type === 'DynamicPlatform' && plugin.aggregatorNode)
1823
+ this.setAggregatorReachability(plugin.aggregatorNode, true);
1824
+ }, 60 * 1000).unref();
1825
+ }
1826
+ // Start the Matter server node of single devices in mode 'server'
1827
+ for (const device of this.devices.array()) {
1828
+ if (device.mode === 'server' && device.serverNode) {
1829
+ this.log.debug(`Starting server node for device ${dev}${device.deviceName}${db} in server mode...`);
1830
+ this.startServerNode(device.serverNode); // We don't await this, because the server node is started in the background
1831
+ }
1832
+ }
1833
+ // Logger.get('LogServerNode').info(this.serverNode);
1834
+ this.emit('childbridge_started');
1835
+ this.log.notice('Matterbridge childbridge started successfully');
1836
+ this.frontend.wssSendRefreshRequired('settings');
1837
+ this.frontend.wssSendRefreshRequired('plugins');
1838
+ this.frontend.wssSendCloseSnackbarMessage(`The bridge is starting...`);
1839
+ }, Number(process.env['MATTERBRIDGE_START_MATTER_INTERVAL_MS']) || this.startMatterIntervalMs);
1840
+ }
1841
+ /**
1842
+ * Starts the Matterbridge controller.
1843
+ *
1844
+ * @private
1845
+ * @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
1846
+ */
1847
+ async startController() {
1848
+ /*
1849
+ if (!this.matterStorageManager) {
1850
+ this.log.error('No storage manager initialized');
1851
+ await this.cleanup('No storage manager initialized');
1852
+ return;
1853
+ }
1854
+ this.log.info('Creating context: mattercontrollerContext');
1855
+ this.controllerContext = this.matterStorageManager.createContext('mattercontrollerContext');
1856
+ if (!this.controllerContext) {
1857
+ this.log.error('No storage context mattercontrollerContext initialized');
1858
+ await this.cleanup('No storage context mattercontrollerContext initialized');
1859
+ return;
1860
+ }
1861
+
1862
+ this.log.debug('Starting matterbridge in mode', this.bridgeMode);
1863
+ this.matterServer = await this.createMatterServer(this.storageManager);
1864
+ this.log.info('Creating matter commissioning controller');
1865
+ this.commissioningController = new CommissioningController({
1866
+ autoConnect: false,
1867
+ });
1868
+ this.log.info('Adding matter commissioning controller to matter server');
1869
+ await this.matterServer.addCommissioningController(this.commissioningController);
1870
+
1871
+ this.log.info('Starting matter server');
1872
+ await this.matterServer.start();
1873
+ this.log.info('Matter server started');
1874
+ const commissioningOptions: ControllerCommissioningFlowOptions = {
1875
+ regulatoryLocation: GeneralCommissioning.RegulatoryLocationType.IndoorOutdoor,
1876
+ regulatoryCountryCode: 'XX',
1877
+ };
1878
+ const commissioningController = new CommissioningController({
1879
+ environment: {
1880
+ environment,
1881
+ id: uniqueId,
1882
+ },
1883
+ autoConnect: false, // Do not auto connect to the commissioned nodes
1884
+ adminFabricLabel,
1885
+ });
1886
+
1887
+ if (hasParameter('pairingcode')) {
1888
+ this.log.info('Pairing device with pairingcode:', getParameter('pairingcode'));
1889
+ const pairingCode = getParameter('pairingcode');
1890
+ const ip = this.controllerContext.has('ip') ? this.controllerContext.get<string>('ip') : undefined;
1891
+ const port = this.controllerContext.has('port') ? this.controllerContext.get<number>('port') : undefined;
1892
+
1893
+ let longDiscriminator, setupPin, shortDiscriminator;
1894
+ if (pairingCode !== undefined) {
1895
+ const pairingCodeCodec = ManualPairingCodeCodec.decode(pairingCode);
1896
+ shortDiscriminator = pairingCodeCodec.shortDiscriminator;
1897
+ longDiscriminator = undefined;
1898
+ setupPin = pairingCodeCodec.passcode;
1899
+ this.log.info(`Data extracted from pairing code: ${Logger.toJSON(pairingCodeCodec)}`);
1900
+ } else {
1901
+ longDiscriminator = await this.controllerContext.get('longDiscriminator', 3840);
1902
+ if (longDiscriminator > 4095) throw new Error('Discriminator value must be less than 4096');
1903
+ setupPin = this.controllerContext.get('pin', 20202021);
1904
+ }
1905
+ if ((shortDiscriminator === undefined && longDiscriminator === undefined) || setupPin === undefined) {
1906
+ throw new Error('Please specify the longDiscriminator of the device to commission with -longDiscriminator or provide a valid passcode with -passcode');
1907
+ }
1908
+
1909
+ const options = {
1910
+ commissioning: commissioningOptions,
1911
+ discovery: {
1912
+ knownAddress: ip !== undefined && port !== undefined ? { ip, port, type: 'udp' } : undefined,
1913
+ identifierData: longDiscriminator !== undefined ? { longDiscriminator } : shortDiscriminator !== undefined ? { shortDiscriminator } : {},
1914
+ },
1915
+ passcode: setupPin,
1916
+ } as NodeCommissioningOptions;
1917
+ this.log.info('Commissioning with options:', options);
1918
+ const nodeId = await this.commissioningController.commissionNode(options);
1919
+ this.log.info(`Commissioning successfully done with nodeId: ${nodeId}`);
1920
+ this.log.info('ActiveSessionInformation:', this.commissioningController.getActiveSessionInformation());
1921
+ } // (hasParameter('pairingcode'))
1922
+
1923
+ if (hasParameter('unpairall')) {
1924
+ this.log.info('***Commissioning controller unpairing all nodes...');
1925
+ const nodeIds = this.commissioningController.getCommissionedNodes();
1926
+ for (const nodeId of nodeIds) {
1927
+ this.log.info('***Commissioning controller unpairing node:', nodeId);
1928
+ await this.commissioningController.removeNode(nodeId);
1929
+ }
1930
+ return;
1931
+ }
1932
+
1933
+ if (hasParameter('discover')) {
1934
+ // const discover = await this.commissioningController.discoverCommissionableDevices({ productId: 0x8000, deviceType: 0xfff1 });
1935
+ // console.log(discover);
1936
+ }
1937
+
1938
+ if (!this.commissioningController.isCommissioned()) {
1939
+ this.log.info('***Commissioning controller is not commissioned: use matterbridge -controller -pairingcode [pairingcode] to commission a device');
1940
+ return;
1941
+ }
1942
+
1943
+ const nodeIds = this.commissioningController.getCommissionedNodes();
1944
+ this.log.info(`***Commissioning controller is commissioned ${this.commissioningController.isCommissioned()} and has ${nodeIds.length} nodes commisioned: `);
1945
+ for (const nodeId of nodeIds) {
1946
+ this.log.info(`***Connecting to commissioned node: ${nodeId}`);
1947
+
1948
+ const node = await this.commissioningController.connectNode(nodeId, {
1949
+ autoSubscribe: false,
1950
+ attributeChangedCallback: (peerNodeId, { path: { nodeId, clusterId, endpointId, attributeName }, value }) =>
1951
+ this.log.info(`***Commissioning controller attributeChangedCallback ${peerNodeId}: attribute ${nodeId}/${endpointId}/${clusterId}/${attributeName} changed to ${Logger.toJSON(value)}`),
1952
+ eventTriggeredCallback: (peerNodeId, { path: { nodeId, clusterId, endpointId, eventName }, events }) =>
1953
+ this.log.info(`***Commissioning controller eventTriggeredCallback ${peerNodeId}: Event ${nodeId}/${endpointId}/${clusterId}/${eventName} triggered with ${Logger.toJSON(events)}`),
1954
+ stateInformationCallback: (peerNodeId, info) => {
1955
+ switch (info) {
1956
+ case NodeStateInformation.Connected:
1957
+ this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} connected`);
1958
+ break;
1959
+ case NodeStateInformation.Disconnected:
1960
+ this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} disconnected`);
1961
+ break;
1962
+ case NodeStateInformation.Reconnecting:
1963
+ this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} reconnecting`);
1964
+ break;
1965
+ case NodeStateInformation.WaitingForDeviceDiscovery:
1966
+ this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} waiting for device discovery`);
1967
+ break;
1968
+ case NodeStateInformation.StructureChanged:
1969
+ this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} structure changed`);
1970
+ break;
1971
+ case NodeStateInformation.Decommissioned:
1972
+ this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} decommissioned`);
1973
+ break;
1974
+ default:
1975
+ this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} NodeStateInformation.${info}`);
1976
+ break;
1977
+ }
1978
+ },
1979
+ });
1980
+
1981
+ node.logStructure();
1982
+
1983
+ // Get the interaction client
1984
+ this.log.info('Getting the interaction client');
1985
+ const interactionClient = await node.getInteractionClient();
1986
+ let cluster;
1987
+ let attributes;
1988
+
1989
+ // Log BasicInformationCluster
1990
+ cluster = BasicInformationCluster;
1991
+ attributes = await interactionClient.getMultipleAttributes({
1992
+ attributes: [{ clusterId: cluster.id }],
1993
+ });
1994
+ if (attributes.length > 0) this.log.info(`Cluster: ${idn}${cluster.name}${rs}${nf} attributes:`);
1995
+ attributes.forEach((attribute) => {
1996
+ this.log.info(
1997
+ `- endpoint ${or}${attribute.path.endpointId}${nf} cluster ${hk}${getClusterNameById(attribute.path.clusterId)}${nf} (${hk}0x${attribute.path.clusterId.toString(16)}${nf}) attribute ${zb}${attribute.path.attributeName}${nf} (${zb}0x${attribute.path.attributeId.toString(16)}${nf}): ${typeof attribute.value === 'object' ? stringify(attribute.value) : attribute.value}`,
1998
+ );
1999
+ });
2000
+
2001
+ // Log PowerSourceCluster
2002
+ cluster = PowerSourceCluster;
2003
+ attributes = await interactionClient.getMultipleAttributes({
2004
+ attributes: [{ clusterId: cluster.id }],
2005
+ });
2006
+ if (attributes.length > 0) this.log.info(`Cluster: ${idn}${cluster.name}${rs}${nf} attributes:`);
2007
+ attributes.forEach((attribute) => {
2008
+ this.log.info(
2009
+ `- endpoint ${or}${attribute.path.endpointId}${nf} cluster ${hk}${getClusterNameById(attribute.path.clusterId)}${nf} (${hk}0x${attribute.path.clusterId.toString(16)}${nf}) attribute ${zb}${attribute.path.attributeName}${nf} (${zb}0x${attribute.path.attributeId.toString(16)}${nf}): ${typeof attribute.value === 'object' ? stringify(attribute.value) : attribute.value}`,
2010
+ );
2011
+ });
2012
+
2013
+ // Log ThreadNetworkDiagnostics
2014
+ cluster = ThreadNetworkDiagnosticsCluster;
2015
+ attributes = await interactionClient.getMultipleAttributes({
2016
+ attributes: [{ clusterId: cluster.id }],
2017
+ });
2018
+ if (attributes.length > 0) this.log.info(`Cluster: ${idn}${cluster.name}${rs}${nf} attributes:`);
2019
+ attributes.forEach((attribute) => {
2020
+ this.log.info(
2021
+ `- endpoint ${or}${attribute.path.endpointId}${nf} cluster ${hk}${getClusterNameById(attribute.path.clusterId)}${nf} (${hk}0x${attribute.path.clusterId.toString(16)}${nf}) attribute ${zb}${attribute.path.attributeName}${nf} (${zb}0x${attribute.path.attributeId.toString(16)}${nf}): ${typeof attribute.value === 'object' ? stringify(attribute.value) : attribute.value}`,
2022
+ );
2023
+ });
2024
+
2025
+ // Log SwitchCluster
2026
+ cluster = SwitchCluster;
2027
+ attributes = await interactionClient.getMultipleAttributes({
2028
+ attributes: [{ clusterId: cluster.id }],
2029
+ });
2030
+ if (attributes.length > 0) this.log.info(`Cluster: ${idn}${cluster.name}${rs}${nf} attributes:`);
2031
+ attributes.forEach((attribute) => {
2032
+ this.log.info(
2033
+ `- endpoint ${or}${attribute.path.endpointId}${nf} cluster ${hk}${getClusterNameById(attribute.path.clusterId)}${nf} (${hk}0x${attribute.path.clusterId.toString(16)}${nf}) attribute ${zb}${attribute.path.attributeName}${nf} (${zb}0x${attribute.path.attributeId.toString(16)}${nf}): ${typeof attribute.value === 'object' ? stringify(attribute.value) : attribute.value}`,
2034
+ );
2035
+ });
2036
+
2037
+ this.log.info('Subscribing to all attributes and events');
2038
+ await node.subscribeAllAttributesAndEvents({
2039
+ ignoreInitialTriggers: false,
2040
+ attributeChangedCallback: ({ path: { nodeId, clusterId, endpointId, attributeName }, version, value }) =>
2041
+ this.log.info(
2042
+ `***${db}Commissioning controller attributeChangedCallback version ${version}: attribute ${BLUE}${nodeId}${db}/${or}${endpointId}${db}/${hk}${getClusterNameById(clusterId)}${db}/${zb}${attributeName}${db} changed to ${typeof value === 'object' ? debugStringify(value ?? { none: true }) : value}`,
2043
+ ),
2044
+ eventTriggeredCallback: ({ path: { nodeId, clusterId, endpointId, eventName }, events }) => {
2045
+ this.log.info(
2046
+ `***${db}Commissioning controller eventTriggeredCallback: event ${BLUE}${nodeId}${db}/${or}${endpointId}${db}/${hk}${getClusterNameById(clusterId)}${db}/${zb}${eventName}${db} triggered with ${debugStringify(events ?? { none: true })}`,
2047
+ );
2048
+ },
2049
+ });
2050
+ this.log.info('Subscribed to all attributes and events');
2051
+ }
2052
+ */
2053
+ }
2054
+ /** */
2055
+ /** Matter.js methods */
2056
+ /** */
2057
+ /**
2058
+ * Starts the matter storage with name Matterbridge, create the matterbridge context and performs a backup.
2059
+ *
2060
+ * @returns {Promise<void>} - A promise that resolves when the storage is started.
2061
+ */
2062
+ async startMatterStorage() {
2063
+ // Setup Matter storage
2064
+ this.log.info(`Starting matter node storage...`);
2065
+ this.matterStorageService = this.environment.get(StorageService);
2066
+ this.log.info(`Matter node storage service created: ${this.matterStorageService.location}`);
2067
+ this.matterStorageManager = await this.matterStorageService.open('Matterbridge');
2068
+ this.log.info('Matter node storage manager "Matterbridge" created');
2069
+ this.matterbridgeContext = await this.createServerNodeContext('Matterbridge', 'Matterbridge', this.aggregatorDeviceType, this.aggregatorVendorId, this.aggregatorVendorName, this.aggregatorProductId, this.aggregatorProductName, this.aggregatorSerialNumber, this.aggregatorUniqueId);
2070
+ this.log.info('Matter node storage started');
2071
+ // Backup matter storage since it is created/opened correctly
2072
+ await this.backupMatterStorage(path.join(this.matterbridgeDirectory, MATTER_STORAGE_NAME), path.join(this.matterbridgeDirectory, MATTER_STORAGE_NAME + '.backup'));
2073
+ }
2074
+ /**
2075
+ * Makes a backup copy of the specified matter storage directory.
2076
+ *
2077
+ * @param {string} storageName - The name of the storage directory to be backed up.
2078
+ * @param {string} backupName - The name of the backup directory to be created.
2079
+ * @private
2080
+ * @returns {Promise<void>} A promise that resolves when the has been done.
2081
+ */
2082
+ async backupMatterStorage(storageName, backupName) {
2083
+ this.log.info('Creating matter node storage backup...');
2084
+ try {
2085
+ await copyDirectory(storageName, backupName);
2086
+ this.log.info('Created matter node storage backup');
2087
+ }
2088
+ catch (error) {
2089
+ this.log.error(`Error creating matter node storage backup from ${storageName} to ${backupName}:`, error);
2090
+ }
2091
+ }
2092
+ /**
2093
+ * Stops the matter storage.
2094
+ *
2095
+ * @returns {Promise<void>} A promise that resolves when the storage is stopped.
2096
+ */
2097
+ async stopMatterStorage() {
2098
+ this.log.info('Closing matter node storage...');
2099
+ await this.matterStorageManager?.close();
2100
+ this.matterStorageService = undefined;
2101
+ this.matterStorageManager = undefined;
2102
+ this.matterbridgeContext = undefined;
2103
+ this.log.info('Matter node storage closed');
2104
+ }
2105
+ /**
2106
+ * Creates a server node storage context.
2107
+ *
2108
+ * @param {string} storeId - The storeId.
2109
+ * @param {string} deviceName - The name of the device.
2110
+ * @param {DeviceTypeId} deviceType - The device type of the device.
2111
+ * @param {number} vendorId - The vendor ID.
2112
+ * @param {string} vendorName - The vendor name.
2113
+ * @param {number} productId - The product ID.
2114
+ * @param {string} productName - The product name.
2115
+ * @param {string} [serialNumber] - The serial number of the device (optional).
2116
+ * @param {string} [uniqueId] - The unique ID of the device (optional).
2117
+ * @returns {Promise<StorageContext>} The storage context for the commissioning server.
2118
+ */
2119
+ async createServerNodeContext(storeId, deviceName, deviceType, vendorId, vendorName, productId, productName, serialNumber, uniqueId) {
2120
+ const { randomBytes } = await import('node:crypto');
2121
+ if (!this.matterStorageService)
2122
+ throw new Error('No storage service initialized');
2123
+ this.log.info(`Creating server node storage context "${storeId}.persist" for ${storeId}...`);
2124
+ const storageManager = await this.matterStorageService.open(storeId);
2125
+ const storageContext = storageManager.createContext('persist');
2126
+ const random = randomBytes(8).toString('hex');
2127
+ await storageContext.set('storeId', storeId);
2128
+ await storageContext.set('deviceName', deviceName);
2129
+ await storageContext.set('deviceType', deviceType);
2130
+ await storageContext.set('vendorId', vendorId);
2131
+ await storageContext.set('vendorName', vendorName.slice(0, 32));
2132
+ await storageContext.set('productId', productId);
2133
+ await storageContext.set('productName', productName.slice(0, 32));
2134
+ await storageContext.set('productLabel', productName.slice(0, 32));
2135
+ await storageContext.set('nodeLabel', deviceName.slice(0, 32));
2136
+ await storageContext.set('serialNumber', await storageContext.get('serialNumber', serialNumber ? serialNumber.slice(0, 32) : 'SN' + random));
2137
+ await storageContext.set('uniqueId', await storageContext.get('uniqueId', uniqueId ? uniqueId.slice(0, 32) : 'UI' + random));
2138
+ await storageContext.set('softwareVersion', isValidNumber(parseVersionString(this.matterbridgeVersion), 0, UINT32_MAX) ? parseVersionString(this.matterbridgeVersion) : 1);
2139
+ await storageContext.set('softwareVersionString', isValidString(this.matterbridgeVersion, 5, 64) ? this.matterbridgeVersion : '1.0.0');
2140
+ await storageContext.set('hardwareVersion', isValidNumber(parseVersionString(this.systemInformation.osRelease), 0, UINT16_MAX) ? parseVersionString(this.systemInformation.osRelease) : 1);
2141
+ await storageContext.set('hardwareVersionString', isValidString(this.systemInformation.osRelease, 5, 64) ? this.systemInformation.osRelease : '1.0.0');
2142
+ this.log.debug(`Created server node storage context "${storeId}.persist" for ${storeId}:`);
2143
+ this.log.debug(`- storeId: ${await storageContext.get('storeId')}`);
2144
+ this.log.debug(`- deviceName: ${await storageContext.get('deviceName')}`);
2145
+ this.log.debug(`- deviceType: ${await storageContext.get('deviceType')}(0x${(await storageContext.get('deviceType'))?.toString(16).padStart(4, '0')})`);
2146
+ this.log.debug(`- vendorId: ${await storageContext.get('vendorId')}`);
2147
+ this.log.debug(`- vendorName: ${await storageContext.get('vendorName')}`);
2148
+ this.log.debug(`- productId: ${await storageContext.get('productId')}`);
2149
+ this.log.debug(`- productName: ${await storageContext.get('productName')}`);
2150
+ this.log.debug(`- productLabel: ${await storageContext.get('productLabel')}`);
2151
+ this.log.debug(`- nodeLabel: ${await storageContext.get('nodeLabel')}`);
2152
+ this.log.debug(`- serialNumber: ${await storageContext.get('serialNumber')}`);
2153
+ this.log.debug(`- uniqueId: ${await storageContext.get('uniqueId')}`);
2154
+ this.log.debug(`- softwareVersion: ${await storageContext.get('softwareVersion')}`);
2155
+ this.log.debug(`- softwareVersionString: ${await storageContext.get('softwareVersionString')}`);
2156
+ this.log.debug(`- hardwareVersion: ${await storageContext.get('hardwareVersion')}`);
2157
+ this.log.debug(`- hardwareVersionString: ${await storageContext.get('hardwareVersionString')}`);
2158
+ return storageContext;
2159
+ }
2160
+ /**
2161
+ * Creates a server node.
2162
+ *
2163
+ * @param {StorageContext} storageContext - The storage context for the server node.
2164
+ * @param {number} [port] - The port number for the server node. Defaults to 5540.
2165
+ * @param {number} [passcode] - The passcode for the server node. Defaults to 20242025.
2166
+ * @param {number} [discriminator] - The discriminator for the server node. Defaults to 3850.
2167
+ * @returns {Promise<ServerNode<ServerNode.RootEndpoint>>} A promise that resolves to the created server node.
2168
+ */
2169
+ async createServerNode(storageContext, port = 5540, passcode = 20242025, discriminator = 3850) {
2170
+ const storeId = await storageContext.get('storeId');
2171
+ this.log.notice(`Creating server node for ${storeId} on port ${port} with passcode ${passcode} and discriminator ${discriminator}...`);
2172
+ this.log.debug(`- storeId: ${await storageContext.get('storeId')}`);
2173
+ this.log.debug(`- deviceName: ${await storageContext.get('deviceName')}`);
2174
+ this.log.debug(`- deviceType: ${await storageContext.get('deviceType')}(0x${(await storageContext.get('deviceType'))?.toString(16).padStart(4, '0')})`);
2175
+ this.log.debug(`- vendorId: ${await storageContext.get('vendorId')}`);
2176
+ this.log.debug(`- vendorName: ${await storageContext.get('vendorName')}`);
2177
+ this.log.debug(`- productId: ${await storageContext.get('productId')}`);
2178
+ this.log.debug(`- productName: ${await storageContext.get('productName')}`);
2179
+ this.log.debug(`- productLabel: ${await storageContext.get('productLabel')}`);
2180
+ this.log.debug(`- nodeLabel: ${await storageContext.get('nodeLabel')}`);
2181
+ this.log.debug(`- serialNumber: ${await storageContext.get('serialNumber')}`);
2182
+ this.log.debug(`- uniqueId: ${await storageContext.get('uniqueId')}`);
2183
+ this.log.debug(`- softwareVersion: ${await storageContext.get('softwareVersion')}`);
2184
+ this.log.debug(`- softwareVersionString: ${await storageContext.get('softwareVersionString')}`);
2185
+ this.log.debug(`- hardwareVersion: ${await storageContext.get('hardwareVersion')}`);
2186
+ this.log.debug(`- hardwareVersionString: ${await storageContext.get('hardwareVersionString')}`);
2187
+ /**
2188
+ * Create a Matter ServerNode, which contains the Root Endpoint and all relevant data and configuration
2189
+ */
2190
+ const serverNode = await ServerNode.create({
2191
+ // Required: Give the Node a unique ID which is used to store the state of this node
2192
+ id: storeId,
2193
+ // Environment to run the server node in
2194
+ environment: this.environment,
2195
+ // Provide Network relevant configuration like the port
2196
+ network: {
2197
+ listeningAddressIpv4: this.ipv4Address,
2198
+ listeningAddressIpv6: this.ipv6Address,
2199
+ port,
2200
+ },
2201
+ // Provide the certificate for the device
2202
+ operationalCredentials: {
2203
+ certification: this.certification,
2204
+ },
2205
+ // Provide Commissioning relevant settings
2206
+ commissioning: {
2207
+ passcode,
2208
+ discriminator,
2209
+ },
2210
+ // Provide Node announcement settings
2211
+ productDescription: {
2212
+ name: await storageContext.get('deviceName'),
2213
+ deviceType: DeviceTypeId(await storageContext.get('deviceType')),
2214
+ },
2215
+ // Provide defaults for the BasicInformation cluster on the Root endpoint
2216
+ basicInformation: {
2217
+ nodeLabel: await storageContext.get('nodeLabel'),
2218
+ vendorId: VendorId(await storageContext.get('vendorId')),
2219
+ vendorName: await storageContext.get('vendorName'),
2220
+ productId: await storageContext.get('productId'),
2221
+ productName: await storageContext.get('productName'),
2222
+ productLabel: await storageContext.get('productName'),
2223
+ serialNumber: await storageContext.get('serialNumber'),
2224
+ uniqueId: await storageContext.get('uniqueId'),
2225
+ softwareVersion: await storageContext.get('softwareVersion'),
2226
+ softwareVersionString: await storageContext.get('softwareVersionString'),
2227
+ hardwareVersion: await storageContext.get('hardwareVersion'),
2228
+ hardwareVersionString: await storageContext.get('hardwareVersionString'),
2229
+ reachable: true,
2230
+ },
2231
+ });
2232
+ /**
2233
+ * This event is triggered when the device is initially commissioned successfully.
2234
+ * This means: It is added to the first fabric.
2235
+ */
2236
+ serverNode.lifecycle.commissioned.on(() => {
2237
+ this.log.notice(`Server node for ${storeId} was initially commissioned successfully!`);
2238
+ this.advertisingNodes.delete(storeId);
2239
+ this.frontend.wssSendRefreshRequired('matter', { matter: { ...this.getServerNodeData(serverNode) } });
2240
+ });
2241
+ /** This event is triggered when all fabrics are removed from the device, usually it also does a factory reset then. */
2242
+ serverNode.lifecycle.decommissioned.on(() => {
2243
+ this.log.notice(`Server node for ${storeId} was fully decommissioned successfully!`);
2244
+ this.advertisingNodes.delete(storeId);
2245
+ this.frontend.wssSendRefreshRequired('matter', { matter: { ...this.getServerNodeData(serverNode) } });
2246
+ this.frontend.wssSendSnackbarMessage(`${storeId} is offline`, 5, 'warning');
2247
+ });
2248
+ /** This event is triggered when the device went online. This means that it is discoverable in the network. */
2249
+ serverNode.lifecycle.online.on(async () => {
2250
+ this.log.notice(`Server node for ${storeId} is online`);
2251
+ if (!serverNode.lifecycle.isCommissioned) {
2252
+ this.log.notice(`Server node for ${storeId} is not commissioned. Pair to commission ...`);
2253
+ this.advertisingNodes.set(storeId, Date.now());
2254
+ const { qrPairingCode, manualPairingCode } = serverNode.state.commissioning.pairingCodes;
2255
+ this.log.notice(`QR Code URL: https://project-chip.github.io/connectedhomeip/qrcode.html?data=${qrPairingCode}`);
2256
+ this.log.notice(`Manual pairing code: ${manualPairingCode}`);
2257
+ }
2258
+ else {
2259
+ // istanbul ignore next
2260
+ this.log.notice(`Server node for ${storeId} is already commissioned. Waiting for controllers to connect ...`);
2261
+ // istanbul ignore next
2262
+ this.advertisingNodes.delete(storeId);
2263
+ }
2264
+ this.frontend.wssSendRefreshRequired('matter', { matter: { ...this.getServerNodeData(serverNode) } });
2265
+ this.frontend.wssSendSnackbarMessage(`${storeId} is online`, 5, 'success');
2266
+ this.emit('online', storeId);
2267
+ });
2268
+ /** This event is triggered when the device went offline. it is not longer discoverable or connectable in the network. */
2269
+ serverNode.lifecycle.offline.on(() => {
2270
+ this.log.notice(`Server node for ${storeId} is offline`);
2271
+ this.advertisingNodes.delete(storeId);
2272
+ this.frontend.wssSendRefreshRequired('matter', { matter: { ...this.getServerNodeData(serverNode) } });
2273
+ this.frontend.wssSendSnackbarMessage(`${storeId} is offline`, 5, 'warning');
2274
+ this.emit('offline', storeId);
2275
+ });
2276
+ /**
2277
+ * This event is triggered when a fabric is added, removed or updated on the device. Use this if more granular
2278
+ * information is needed.
2279
+ */
2280
+ serverNode.events.commissioning.fabricsChanged.on((fabricIndex, fabricAction) => {
2281
+ let action = '';
2282
+ switch (fabricAction) {
2283
+ case 'added':
2284
+ this.advertisingNodes.delete(storeId); // The advertising stops when a fabric is added
2285
+ action = 'added';
2286
+ break;
2287
+ case 'deleted':
2288
+ action = 'removed';
2289
+ break;
2290
+ case 'updated':
2291
+ action = 'updated';
2292
+ break;
2293
+ }
2294
+ this.log.notice(`Commissioned fabric index ${fabricIndex} ${action} on server node for ${storeId}: ${debugStringify(serverNode.state.commissioning.fabrics[fabricIndex])}`);
2295
+ this.frontend.wssSendRefreshRequired('matter', { matter: { ...this.getServerNodeData(serverNode) } });
2296
+ });
2297
+ /**
2298
+ * This event is triggered when an operative new session was opened by a Controller.
2299
+ * It is not triggered for the initial commissioning process, just afterwards for real connections.
2300
+ */
2301
+ serverNode.events.sessions.opened.on((session) => {
2302
+ this.log.notice(`Session opened on server node for ${storeId}: ${debugStringify(session)}`);
2303
+ this.frontend.wssSendRefreshRequired('matter', { matter: { ...this.getServerNodeData(serverNode) } });
2304
+ });
2305
+ /**
2306
+ * This event is triggered when an operative session is closed by a Controller or because the Device goes offline.
2307
+ */
2308
+ serverNode.events.sessions.closed.on((session) => {
2309
+ this.log.notice(`Session closed on server node for ${storeId}: ${debugStringify(session)}`);
2310
+ this.frontend.wssSendRefreshRequired('matter', { matter: { ...this.getServerNodeData(serverNode) } });
2311
+ });
2312
+ /** This event is triggered when a subscription gets added or removed on an operative session. */
2313
+ serverNode.events.sessions.subscriptionsChanged.on((session) => {
2314
+ this.log.notice(`Session subscriptions changed on server node for ${storeId}: ${debugStringify(session)}`);
2315
+ this.frontend.wssSendRefreshRequired('matter', { matter: { ...this.getServerNodeData(serverNode) } });
2316
+ });
2317
+ this.log.info(`Created server node for ${storeId}`);
2318
+ return serverNode;
2319
+ }
2320
+ /**
2321
+ * Gets the matter sanitized data of the specified server node.
2322
+ *
2323
+ * @param {ServerNode} [serverNode] - The server node to start.
2324
+ * @returns {ApiMatter} The sanitized data of the server node.
2325
+ */
2326
+ getServerNodeData(serverNode) {
2327
+ const advertiseTime = this.advertisingNodes.get(serverNode.id) || 0;
2328
+ return {
2329
+ id: serverNode.id,
2330
+ online: serverNode.lifecycle.isOnline,
2331
+ commissioned: serverNode.state.commissioning.commissioned,
2332
+ advertising: advertiseTime > Date.now() - 15 * 60 * 1000,
2333
+ advertiseTime,
2334
+ windowStatus: serverNode.state.administratorCommissioning.windowStatus,
2335
+ qrPairingCode: serverNode.state.commissioning.pairingCodes.qrPairingCode,
2336
+ manualPairingCode: serverNode.state.commissioning.pairingCodes.manualPairingCode,
2337
+ fabricInformations: this.sanitizeFabricInformations(Object.values(serverNode.state.commissioning.fabrics)),
2338
+ sessionInformations: this.sanitizeSessionInformation(Object.values(serverNode.state.sessions.sessions)),
2339
+ serialNumber: serverNode.state.basicInformation.serialNumber,
2340
+ };
2341
+ }
2342
+ /**
2343
+ * Starts the specified server node.
2344
+ *
2345
+ * @param {ServerNode} [matterServerNode] - The server node to start.
2346
+ * @returns {Promise<void>} A promise that resolves when the server node has started.
2347
+ */
2348
+ async startServerNode(matterServerNode) {
2349
+ if (!matterServerNode)
2350
+ return;
2351
+ this.log.notice(`Starting ${matterServerNode.id} server node`);
2352
+ await matterServerNode.start();
2353
+ }
2354
+ /**
2355
+ * Stops the specified server node.
2356
+ *
2357
+ * @param {ServerNode} matterServerNode - The server node to stop.
2358
+ * @param {number} [timeout] - The timeout in milliseconds for stopping the server node. Defaults to 10 seconds.
2359
+ * @returns {Promise<void>} A promise that resolves when the server node has stopped.
2360
+ */
2361
+ async stopServerNode(matterServerNode, timeout = 10000) {
2362
+ const { withTimeout } = await import('@matterbridge/utils');
2363
+ if (!matterServerNode)
2364
+ return;
2365
+ this.log.notice(`Closing ${matterServerNode.id} server node`);
2366
+ try {
2367
+ await withTimeout(matterServerNode.close(), timeout);
2368
+ this.log.info(`Closed ${matterServerNode.id} server node`);
2369
+ }
2370
+ catch (error) {
2371
+ this.log.error(`Failed to close ${matterServerNode.id} server node: ${error instanceof Error ? error.message : error}`);
2372
+ }
2373
+ }
2374
+ /**
2375
+ * Creates an aggregator node with the specified storage context.
2376
+ *
2377
+ * @param {StorageContext} storageContext - The storage context for the aggregator node.
2378
+ * @returns {Promise<Endpoint<AggregatorEndpoint>>} A promise that resolves to the created aggregator node.
2379
+ */
2380
+ async createAggregatorNode(storageContext) {
2381
+ this.log.notice(`Creating ${await storageContext.get('storeId')} aggregator...`);
2382
+ const aggregatorNode = new Endpoint(AggregatorEndpoint, { id: `${await storageContext.get('storeId')}` });
2383
+ this.log.info(`Created ${await storageContext.get('storeId')} aggregator`);
2384
+ return aggregatorNode;
2385
+ }
2386
+ /**
2387
+ * Creates and configures the server node for an accessory plugin for a given device.
2388
+ *
2389
+ * @param {Plugin} plugin - The plugin to configure.
2390
+ * @param {MatterbridgeEndpoint} device - The device to associate with the plugin.
2391
+ * @returns {Promise<void>} A promise that resolves when the server node for the accessory plugin is created and configured.
2392
+ */
2393
+ async createAccessoryPlugin(plugin, device) {
2394
+ if (!plugin.locked && device.deviceType && device.deviceName && device.vendorId && device.productId && device.vendorName && device.productName) {
2395
+ plugin.locked = true;
2396
+ plugin.device = device;
2397
+ this.log.debug(`Creating accessory plugin ${plg}${plugin.name}${db} server node...`);
2398
+ plugin.storageContext = await this.createServerNodeContext(plugin.name, device.deviceName, DeviceTypeId(device.deviceType), device.vendorId, device.vendorName, device.productId, device.productName);
2399
+ plugin.serverNode = await this.createServerNode(plugin.storageContext, this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
2400
+ this.log.debug(`Adding ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db} to ${plg}${plugin.name}${db} server node...`);
2401
+ await plugin.serverNode.add(device);
2402
+ }
2403
+ }
2404
+ /**
2405
+ * Creates and configures the server node and the aggregator node for a dynamic plugin.
2406
+ *
2407
+ * @param {Plugin} plugin - The plugin to configure.
2408
+ * @returns {Promise<void>} A promise that resolves when the server node and the aggregator node for the dynamic plugin is created and configured.
2409
+ */
2410
+ async createDynamicPlugin(plugin) {
2411
+ if (!plugin.locked) {
2412
+ plugin.locked = true;
2413
+ this.log.debug(`Creating dynamic plugin ${plg}${plugin.name}${db} server node...`);
2414
+ plugin.storageContext = await this.createServerNodeContext(plugin.name, plugin.description, this.aggregatorDeviceType, this.aggregatorVendorId, this.aggregatorVendorName, this.aggregatorProductId, this.aggregatorProductName);
2415
+ plugin.serverNode = await this.createServerNode(plugin.storageContext, this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
2416
+ this.log.debug(`Creating dynamic plugin ${plg}${plugin.name}${db} aggregator node...`);
2417
+ plugin.aggregatorNode = await this.createAggregatorNode(plugin.storageContext);
2418
+ this.log.debug(`Adding dynamic plugin ${plg}${plugin.name}${db} aggregator node...`);
2419
+ await plugin.serverNode.add(plugin.aggregatorNode);
2420
+ }
2421
+ }
2422
+ /**
2423
+ * Creates and configures the server node for a single not bridged device.
2424
+ *
2425
+ * @param {Plugin} plugin - The plugin to configure.
2426
+ * @param {MatterbridgeEndpoint} device - The device to associate with the plugin.
2427
+ * @returns {Promise<void>} A promise that resolves when the server node for the accessory plugin is created and configured.
2428
+ */
2429
+ async createDeviceServerNode(plugin, device) {
2430
+ if (device.mode === 'server' &&
2431
+ !device.serverNode &&
2432
+ device.deviceType &&
2433
+ device.deviceName &&
2434
+ device.vendorId &&
2435
+ device.vendorName &&
2436
+ device.productId &&
2437
+ device.productName) {
2438
+ this.log.debug(`Creating device ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db} server node...`);
2439
+ const context = await this.createServerNodeContext(device.deviceName.replace(/[ .]/g, ''), device.deviceName, DeviceTypeId(device.deviceType), device.vendorId, device.vendorName, device.productId, device.productName);
2440
+ device.serverNode = await this.createServerNode(context, this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
2441
+ this.log.debug(`Adding ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db} to server node...`);
2442
+ await device.serverNode.add(device);
2443
+ this.log.debug(`Added ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db} to server node`);
2444
+ }
2445
+ }
2446
+ /**
2447
+ * Adds a MatterbridgeEndpoint to the specified plugin.
2448
+ *
2449
+ * @param {string} pluginName - The name of the plugin.
2450
+ * @param {MatterbridgeEndpoint} device - The device to add as a bridged endpoint.
2451
+ * @returns {Promise<void>} A promise that resolves when the bridged endpoint has been added.
2452
+ */
2453
+ async addBridgedEndpoint(pluginName, device) {
2454
+ const { waiter } = await import('@matterbridge/utils');
2455
+ // Check if the plugin is registered
2456
+ const plugin = this.plugins.get(pluginName);
2457
+ if (!plugin) {
2458
+ this.log.error(`Error adding bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.id}${er}) plugin ${plg}${pluginName}${er} not found`);
2459
+ return;
2460
+ }
2461
+ if (device.mode === 'server') {
2462
+ try {
2463
+ this.log.debug(`Creating server node for device ${dev}${device.deviceName}${db} of plugin ${plg}${plugin.name}${db}...`);
2464
+ await this.createDeviceServerNode(plugin, device);
2465
+ }
2466
+ catch (error) {
2467
+ const errorMessage = error instanceof Error ? error.message : error;
2468
+ const errorInspect = inspect(error, { depth: 10 });
2469
+ this.log.error(`Error creating server node for device ${dev}${device.deviceName}${er} (${zb}${device.id}${er}) of plugin ${plg}${pluginName}${er}: ${errorMessage}\nstack: ${errorInspect}`);
2470
+ return;
2471
+ }
2472
+ }
2473
+ else if (this.bridgeMode === 'bridge') {
2474
+ if (device.mode === 'matter') {
2475
+ // Register and add the device to the matterbridge server node
2476
+ this.log.debug(`Adding matter endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} to Matterbridge server node...`);
2477
+ if (!this.serverNode) {
2478
+ this.log.error('Server node not found for Matterbridge');
2479
+ return;
2480
+ }
2481
+ try {
2482
+ await this.serverNode.add(device);
2483
+ }
2484
+ catch (error) {
2485
+ const errorMessage = error instanceof Error ? error.message : error;
2486
+ const errorInspect = inspect(error, { depth: 10 });
2487
+ this.log.error(`Error adding matter endpoint ${dev}${device.deviceName}${er} (${zb}${device.id}${er}) for plugin ${plg}${pluginName}${er}: ${errorMessage}\nstack: ${errorInspect}`);
2488
+ return;
2489
+ }
2490
+ }
2491
+ else {
2492
+ // Register and add the device to the matterbridge aggregator node
2493
+ this.log.debug(`Adding bridged endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} to Matterbridge aggregator node`);
2494
+ if (!this.aggregatorNode) {
2495
+ this.log.error('Aggregator node not found for Matterbridge');
2496
+ return;
2497
+ }
2498
+ try {
2499
+ await this.aggregatorNode.add(device);
2500
+ }
2501
+ catch (error) {
2502
+ const errorMessage = error instanceof Error ? error.message : error;
2503
+ const errorInspect = inspect(error, { depth: 10 });
2504
+ this.log.error(`Error adding bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.id}${er}) for plugin ${plg}${pluginName}${er}: ${errorMessage}\nstack: ${errorInspect}`);
2505
+ return;
2506
+ }
2507
+ }
2508
+ }
2509
+ else if (this.bridgeMode === 'childbridge') {
2510
+ // Register and add the device to the plugin server node
2511
+ if (plugin.type === 'AccessoryPlatform') {
2512
+ try {
2513
+ this.log.debug(`Creating endpoint ${dev}${device.deviceName}${db} for AccessoryPlatform plugin ${plg}${plugin.name}${db} server node`);
2514
+ if (plugin.serverNode) {
2515
+ if (device.mode === 'matter') {
2516
+ await plugin.serverNode.add(device);
2517
+ }
2518
+ else {
2519
+ this.log.error(`The plugin ${plg}${plugin.name}${er} has already added a device. Only one device is allowed per AccessoryPlatform plugin.`);
2520
+ return;
2521
+ }
2522
+ }
2523
+ else {
2524
+ await this.createAccessoryPlugin(plugin, device);
2525
+ }
2526
+ }
2527
+ catch (error) {
2528
+ const errorMessage = error instanceof Error ? error.message : error;
2529
+ const errorInspect = inspect(error, { depth: 10 });
2530
+ this.log.error(`Error creating endpoint ${dev}${device.deviceName}${er} (${zb}${device.id}${er}) for AccessoryPlatform plugin ${plg}${pluginName}${er} server node: ${errorMessage}\nstack: ${errorInspect}`);
2531
+ return;
2532
+ }
2533
+ }
2534
+ // Register and add the device to the plugin aggregator node
2535
+ if (plugin.type === 'DynamicPlatform') {
2536
+ try {
2537
+ this.log.debug(`Adding bridged endpoint ${dev}${device.deviceName}${db} for DynamicPlatform plugin ${plg}${plugin.name}${db} aggregator node`);
2538
+ await this.createDynamicPlugin(plugin);
2539
+ // Fast plugins can add another device before the server node is ready, so we wait for the server node to be ready
2540
+ await waiter(`createDynamicPlugin(${plugin.name})`, () => plugin.serverNode?.hasParts === true);
2541
+ if (!plugin.aggregatorNode) {
2542
+ this.log.error(`Aggregator node not found for plugin ${plg}${plugin.name}${er}`);
2543
+ return;
2544
+ }
2545
+ if (device.mode === 'matter')
2546
+ await plugin.serverNode?.add(device);
2547
+ else
2548
+ await plugin.aggregatorNode.add(device);
2549
+ }
2550
+ catch (error) {
2551
+ const errorMessage = error instanceof Error ? error.message : error;
2552
+ const errorInspect = inspect(error, { depth: 10 });
2553
+ this.log.error(`Error adding bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.id}${er}) for DynamicPlatform plugin ${plg}${pluginName}${er} aggregator node: ${errorMessage}\nstack: ${errorInspect}`);
2554
+ return;
2555
+ }
2556
+ }
2557
+ }
2558
+ if (plugin.registeredDevices !== undefined)
2559
+ plugin.registeredDevices++;
2560
+ // Add the device to the DeviceManager
2561
+ this.devices.set(device);
2562
+ // Subscribe to the attributes changed event
2563
+ await this.subscribeAttributeChanged(plugin, device);
2564
+ this.log.info(`Added and registered bridged endpoint (${plugin.registeredDevices}) ${dev}${device.deviceName}${nf} (${dev}${device.id}${nf}) for plugin ${plg}${pluginName}${nf}`);
2565
+ }
2566
+ /**
2567
+ * Removes a MatterbridgeEndpoint from the specified plugin.
2568
+ *
2569
+ * @param {string} pluginName - The name of the plugin.
2570
+ * @param {MatterbridgeEndpoint} device - The device to remove as a bridged endpoint.
2571
+ * @returns {Promise<void>} A promise that resolves when the bridged endpoint has been removed.
2572
+ */
2573
+ async removeBridgedEndpoint(pluginName, device) {
2574
+ this.log.debug(`Removing bridged endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} (${zb}${device.name}${db}) for plugin ${plg}${pluginName}${db}`);
2575
+ // Check if the plugin is registered
2576
+ const plugin = this.plugins.get(pluginName);
2577
+ if (!plugin) {
2578
+ this.log.error(`Error removing bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.name}${er}) for plugin ${plg}${pluginName}${er}: plugin not found`);
2579
+ return;
2580
+ }
2581
+ if (device.mode === 'server') {
2582
+ this.log.info(`Removed mode server bridged endpoint(${plugin.registeredDevices}) ${dev}${device.deviceName}${nf} (${zb}${device.name}${nf}) for plugin ${plg}${pluginName}${nf}`);
2583
+ }
2584
+ else if (this.bridgeMode === 'bridge') {
2585
+ // Unregister and remove the device from the matterbridge aggregator node
2586
+ if (!this.aggregatorNode) {
2587
+ this.log.error(`Error removing bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.name}${er}) for plugin ${plg}${pluginName}${er}: aggregator node not found`);
2588
+ return;
2589
+ }
2590
+ await device.delete();
2591
+ this.log.info(`Removed bridged endpoint(${plugin.registeredDevices}) ${dev}${device.deviceName}${nf} (${zb}${device.name}${nf}) for plugin ${plg}${pluginName}${nf}`);
2592
+ if (plugin.registeredDevices !== undefined)
2593
+ plugin.registeredDevices--;
2594
+ }
2595
+ else if (this.bridgeMode === 'childbridge') {
2596
+ if (plugin.type === 'AccessoryPlatform') {
2597
+ // Nothing to do here since the server node has no aggregator node but only the device itself
2598
+ }
2599
+ else if (plugin.type === 'DynamicPlatform') {
2600
+ // Unregister and remove the device from the plugin aggregator node
2601
+ if (!plugin.aggregatorNode) {
2602
+ this.log.error(`Error removing bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.name}${er}) for plugin ${plg}${pluginName}${er}: aggregator node not found`);
2603
+ return;
2604
+ }
2605
+ await device.delete();
2606
+ }
2607
+ this.log.info(`Removed bridged endpoint(${plugin.registeredDevices}) ${dev}${device.deviceName}${nf} (${zb}${device.name}${nf}) for plugin ${plg}${pluginName}${nf}`);
2608
+ if (plugin.registeredDevices !== undefined)
2609
+ plugin.registeredDevices--;
2610
+ }
2611
+ // Remove the device from the DeviceManager
2612
+ this.devices.remove(device);
2613
+ }
2614
+ /**
2615
+ * Removes all bridged endpoints from the specified plugin.
2616
+ *
2617
+ * @param {string} pluginName - The name of the plugin.
2618
+ * @param {number} [delay] - The delay in milliseconds between removing each bridged endpoint (default: 0).
2619
+ * @returns {Promise<void>} A promise that resolves when all bridged endpoints have been removed.
2620
+ *
2621
+ * @remarks
2622
+ * This method iterates through all devices in the DeviceManager and removes each bridged endpoint associated with the specified plugin.
2623
+ * It also applies a delay between each removal if specified.
2624
+ * The delay is useful to allow the controllers to receive a single subscription for each device removed.
2625
+ */
2626
+ async removeAllBridgedEndpoints(pluginName, delay = 0) {
2627
+ const { wait } = await import('@matterbridge/utils');
2628
+ this.log.debug(`Removing all bridged endpoints for plugin ${plg}${pluginName}${db}${delay > 0 ? ` with delay ${delay} ms` : ''}`);
2629
+ for (const device of this.devices.array().filter((device) => device.plugin === pluginName)) {
2630
+ await this.removeBridgedEndpoint(pluginName, device);
2631
+ if (delay > 0)
2632
+ await wait(delay);
2633
+ }
2634
+ if (delay > 0)
2635
+ await wait(2000);
2636
+ }
2637
+ /**
2638
+ * Registers a virtual device.
2639
+ * Virtual devices are only supported in bridge mode and childbridge mode with a DynamicPlatform.
2640
+ *
2641
+ * The virtual device is created as an instance of `Endpoint` with the provided device type.
2642
+ * When the virtual device is turned on, the provided callback function is executed.
2643
+ * The onOff state of the virtual device always reverts to false when the device is turned on.
2644
+ *
2645
+ * @param { string } pluginName - The name of the plugin to register the virtual device under.
2646
+ * @param { string } name - The name of the virtual device.
2647
+ * @param { 'light' | 'outlet' | 'switch' | 'mounted_switch' } type - The type of the virtual device.
2648
+ * @param { () => Promise<void> } callback - The callback to call when the virtual device is turned on.
2649
+ *
2650
+ * @returns {Promise<boolean>} A promise that resolves to true if the virtual device was successfully registered, false otherwise.
2651
+ *
2652
+ * @remarks
2653
+ * The virtual devices don't show up in the device list of the frontend.
2654
+ * Type 'switch' is not supported by Alexa and 'mounted_switch' is not supported by Apple Home.
2655
+ */
2656
+ async addVirtualEndpoint(pluginName, name, type, callback) {
2657
+ this.log.debug(`Adding virtual endpoint ${plg}${pluginName}${db}:${dev}${name}${db}...`);
2658
+ // Check if the plugin is registered
2659
+ const plugin = this.plugins.get(pluginName);
2660
+ if (!plugin) {
2661
+ this.log.error(`Error adding virtual endpoint ${dev}${name}${er} for plugin ${plg}${pluginName}${er}: plugin not found`);
2662
+ return false;
2663
+ }
2664
+ let aggregator;
2665
+ if (this.bridgeMode === 'bridge') {
2666
+ aggregator = this.aggregatorNode;
2667
+ }
2668
+ else if (this.bridgeMode === 'childbridge' && plugin.type === 'DynamicPlatform') {
2669
+ aggregator = plugin.aggregatorNode;
2670
+ }
2671
+ if (aggregator) {
2672
+ if (aggregator.parts.has(name.replaceAll(' ', '') + ':' + type)) {
2673
+ this.log.error(`Virtual endpoint ${dev}${name}${er} already registered for plugin ${plg}${pluginName}${er}. Please use a different name.`);
2674
+ return false;
2675
+ }
2676
+ else {
2677
+ await addVirtualDevice(aggregator, name.slice(0, 32), type, callback);
2678
+ this.log.info(`Created virtual endpoint ${dev}${name}${nf} for plugin ${plg}${pluginName}${nf}`);
2679
+ return true;
2680
+ }
2681
+ }
2682
+ this.log.error(`Virtual endpoint ${dev}${name}${er} for plugin ${plg}${pluginName}${er} not created. Virtual endpoints are only supported in bridge mode and childbridge mode with a DynamicPlatform.`);
2683
+ return false;
2684
+ }
2685
+ /**
2686
+ * Subscribes to the attribute change event for the given device and plugin.
2687
+ * Specifically, it listens for changes in the 'reachable' attribute of the
2688
+ * BridgedDeviceBasicInformationServer cluster server of the bridged device or BasicInformationServer cluster server of server node.
2689
+ *
2690
+ * @param {Plugin} plugin - The plugin associated with the device.
2691
+ * @param {MatterbridgeEndpoint} device - The device to subscribe to attribute changes for.
2692
+ * @returns {Promise<void>} A promise that resolves when the subscription is set up.
2693
+ */
2694
+ async subscribeAttributeChanged(plugin, device) {
2695
+ if (!plugin || !device || !device.plugin || !device.serialNumber || !device.uniqueId || !device.maybeNumber)
2696
+ return;
2697
+ this.log.info(`Subscribing attributes for endpoint ${dev}${device.deviceName}${nf} (${dev}${device.id}${nf}) plugin ${plg}${plugin.name}${nf}`);
2698
+ // Subscribe to the reachable$Changed event of the BasicInformationServer cluster server of the server node in childbridge mode
2699
+ if (this.bridgeMode === 'childbridge' && plugin.type === 'AccessoryPlatform' && plugin.serverNode) {
2700
+ plugin.serverNode.eventsOf(BasicInformationServer).reachable$Changed?.on((reachable) => {
2701
+ this.log.info(`Accessory endpoint ${dev}${device.deviceName}${nf} (${dev}${device.id}${nf}) is ${reachable ? 'reachable' : 'unreachable'}`);
2702
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2703
+ this.frontend.wssSendAttributeChangedMessage(device.plugin, device.serialNumber, device.uniqueId, device.number, device.id, 'BasicInformation', 'reachable', reachable);
2704
+ });
2705
+ }
2706
+ const subscriptions = [
2707
+ { cluster: 'BridgedDeviceBasicInformation', attribute: 'reachable' },
2708
+ { cluster: 'OnOff', attribute: 'onOff' },
2709
+ { cluster: 'LevelControl', attribute: 'currentLevel' },
2710
+ { cluster: 'ColorControl', attribute: 'colorMode' },
2711
+ { cluster: 'ColorControl', attribute: 'currentHue' },
2712
+ { cluster: 'ColorControl', attribute: 'currentSaturation' },
2713
+ { cluster: 'ColorControl', attribute: 'currentX' },
2714
+ { cluster: 'ColorControl', attribute: 'currentY' },
2715
+ { cluster: 'ColorControl', attribute: 'colorTemperatureMireds' },
2716
+ { cluster: 'Thermostat', attribute: 'localTemperature' },
2717
+ { cluster: 'Thermostat', attribute: 'occupiedCoolingSetpoint' },
2718
+ { cluster: 'Thermostat', attribute: 'occupiedHeatingSetpoint' },
2719
+ { cluster: 'Thermostat', attribute: 'systemMode' },
2720
+ { cluster: 'Thermostat', attribute: 'thermostatRunningMode' },
2721
+ { cluster: 'Thermostat', attribute: 'thermostatRunningState' },
2722
+ { cluster: 'WindowCovering', attribute: 'operationalStatus' },
2723
+ { cluster: 'WindowCovering', attribute: 'currentPositionLiftPercent100ths' },
2724
+ { cluster: 'DoorLock', attribute: 'lockState' },
2725
+ { cluster: 'PumpConfigurationAndControl', attribute: 'pumpStatus' },
2726
+ { cluster: 'FanControl', attribute: 'fanMode' },
2727
+ { cluster: 'FanControl', attribute: 'fanModeSequence' },
2728
+ { cluster: 'FanControl', attribute: 'percentSetting' },
2729
+ { cluster: 'ModeSelect', attribute: 'currentMode' },
2730
+ { cluster: 'RvcRunMode', attribute: 'currentMode' },
2731
+ { cluster: 'RvcCleanMode', attribute: 'currentMode' },
2732
+ { cluster: 'RvcOperationalState', attribute: 'operationalState' },
2733
+ { cluster: 'RvcOperationalState', attribute: 'operationalError' },
2734
+ { cluster: 'ServiceArea', attribute: 'currentArea' },
2735
+ { cluster: 'AirQuality', attribute: 'airQuality' },
2736
+ { cluster: 'TotalVolatileOrganicCompoundsConcentrationMeasurement', attribute: 'measuredValue' },
2737
+ { cluster: 'BooleanState', attribute: 'stateValue' },
2738
+ { cluster: 'OccupancySensing', attribute: 'occupancy' },
2739
+ { cluster: 'IlluminanceMeasurement', attribute: 'measuredValue' },
2740
+ { cluster: 'TemperatureMeasurement', attribute: 'measuredValue' },
2741
+ { cluster: 'RelativeHumidityMeasurement', attribute: 'measuredValue' },
2742
+ { cluster: 'PressureMeasurement', attribute: 'measuredValue' },
2743
+ { cluster: 'FlowMeasurement', attribute: 'measuredValue' },
2744
+ { cluster: 'SmokeCoAlarm', attribute: 'smokeState' },
2745
+ { cluster: 'SmokeCoAlarm', attribute: 'coState' },
2746
+ ];
2747
+ for (const sub of subscriptions) {
2748
+ if (device.hasAttributeServer(sub.cluster, sub.attribute)) {
2749
+ this.log.debug(`Subscribing to endpoint ${or}${device.id}${db}:${or}${device.number}${db} attribute ${dev}${sub.cluster}${db}.${dev}${sub.attribute}${db} changes...`);
2750
+ await device.subscribeAttribute(sub.cluster, sub.attribute, (value) => {
2751
+ this.log.debug(`Bridged endpoint ${or}${device.id}${db}:${or}${device.number}${db} attribute ${dev}${sub.cluster}${db}.${dev}${sub.attribute}${db} changed to ${CYAN}${isValidObject(value) ? debugStringify(value) : value}${db}`);
2752
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2753
+ this.frontend.wssSendAttributeChangedMessage(device.plugin, device.serialNumber, device.uniqueId, device.number, device.id, sub.cluster, sub.attribute, value);
2754
+ });
2755
+ }
2756
+ for (const child of device.getChildEndpoints()) {
2757
+ if (child.hasAttributeServer(sub.cluster, sub.attribute)) {
2758
+ this.log.debug(`Subscribing to child endpoint ${or}${child.id}${db}:${or}${child.number}${db} attribute ${dev}${sub.cluster}${db}.${dev}${sub.attribute}${db} changes...`);
2759
+ await child.subscribeAttribute(sub.cluster, sub.attribute, (value) => {
2760
+ this.log.debug(`Bridged child endpoint ${or}${child.id}${db}:${or}${child.number}${db} attribute ${dev}${sub.cluster}${db}.${dev}${sub.attribute}${db} changed to ${CYAN}${isValidObject(value) ? debugStringify(value) : value}${db}`);
2761
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2762
+ this.frontend.wssSendAttributeChangedMessage(device.plugin, device.serialNumber, device.uniqueId, child.number, child.id, sub.cluster, sub.attribute, value);
2763
+ });
2764
+ }
2765
+ }
2766
+ }
2767
+ }
2768
+ /**
2769
+ * Sanitizes the fabric information by converting bigint properties to strings because `res.json` doesn't support bigint.
2770
+ *
2771
+ * @param {ExposedFabricInformation[]} fabricInfo - The array of exposed fabric information objects.
2772
+ * @returns {SanitizedExposedFabricInformation[]} An array of sanitized exposed fabric information objects.
2773
+ */
2774
+ sanitizeFabricInformations(fabricInfo) {
2775
+ return fabricInfo.map((info) => {
2776
+ return {
2777
+ fabricIndex: info.fabricIndex,
2778
+ fabricId: info.fabricId.toString(),
2779
+ nodeId: info.nodeId.toString(),
2780
+ rootNodeId: info.rootNodeId.toString(),
2781
+ rootVendorId: info.rootVendorId,
2782
+ rootVendorName: this.getVendorIdName(info.rootVendorId),
2783
+ label: info.label,
2784
+ };
2785
+ });
2786
+ }
2787
+ /**
2788
+ * Sanitizes the session information by converting bigint properties to strings because `res.json` doesn't support bigint.
2789
+ *
2790
+ * @param {SessionsBehavior.Session[]} sessions - The array of session information objects.
2791
+ * @returns {SanitizedSession[]} An array of sanitized session information objects.
2792
+ */
2793
+ sanitizeSessionInformation(sessions) {
2794
+ return sessions
2795
+ .filter((session) => session.isPeerActive)
2796
+ .map((session) => {
2797
+ return {
2798
+ name: session.name,
2799
+ nodeId: session.nodeId.toString(),
2800
+ peerNodeId: session.peerNodeId.toString(),
2801
+ fabric: session.fabric
2802
+ ? {
2803
+ fabricIndex: session.fabric.fabricIndex,
2804
+ fabricId: session.fabric.fabricId.toString(),
2805
+ nodeId: session.fabric.nodeId.toString(),
2806
+ rootNodeId: session.fabric.rootNodeId.toString(),
2807
+ rootVendorId: session.fabric.rootVendorId,
2808
+ rootVendorName: this.getVendorIdName(session.fabric.rootVendorId),
2809
+ label: session.fabric.label,
2810
+ }
2811
+ : undefined,
2812
+ isPeerActive: session.isPeerActive,
2813
+ lastInteractionTimestamp: session.lastInteractionTimestamp?.toString(),
2814
+ lastActiveTimestamp: session.lastActiveTimestamp?.toString(),
2815
+ numberOfActiveSubscriptions: session.numberOfActiveSubscriptions,
2816
+ };
2817
+ });
2818
+ }
2819
+ /**
2820
+ * Sets the reachability of the specified aggregator node bridged devices and trigger.
2821
+ *
2822
+ * @param {Endpoint<AggregatorEndpoint>} aggregatorNode - The aggregator node to set the reachability for.
2823
+ * @param {boolean} reachable - A boolean indicating the reachability status to set.
2824
+ */
2825
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
2826
+ async setAggregatorReachability(aggregatorNode, reachable) {
2827
+ /*
2828
+ for (const child of aggregatorNode.parts) {
2829
+ this.log.debug(`Setting reachability of ${(child as unknown as MatterbridgeEndpoint)?.deviceName} to ${reachable}`);
2830
+ await child.setStateOf(BridgedDeviceBasicInformationServer, { reachable });
2831
+ child.act((agent) => child.eventsOf(BridgedDeviceBasicInformationServer).reachableChanged.emit({ reachableNewValue: true }, agent.context));
2832
+ }
2833
+ */
2834
+ }
2835
+ getVendorIdName = (vendorId) => {
2836
+ if (!vendorId)
2837
+ return '';
2838
+ let vendorName = '(Unknown vendorId)';
2839
+ switch (vendorId) {
2840
+ case 4937:
2841
+ vendorName = '(AppleHome)';
2842
+ break;
2843
+ case 4996:
2844
+ vendorName = '(AppleKeyChain)';
2845
+ break;
2846
+ case 4362:
2847
+ vendorName = '(SmartThings)';
2848
+ break;
2849
+ case 4939:
2850
+ vendorName = '(HomeAssistant)';
2851
+ break;
2852
+ case 24582:
2853
+ vendorName = '(GoogleHome)';
2854
+ break;
2855
+ case 4631:
2856
+ vendorName = '(Alexa)';
2857
+ break;
2858
+ case 4701:
2859
+ vendorName = '(Tuya)';
2860
+ break;
2861
+ case 4718:
2862
+ vendorName = '(Xiaomi)';
2863
+ break;
2864
+ case 4742:
2865
+ vendorName = '(eWeLink)';
2866
+ break;
2867
+ case 5264:
2868
+ vendorName = '(Shelly)';
2869
+ break;
2870
+ case 0x1488:
2871
+ vendorName = '(ShortcutLabsFlic)';
2872
+ break;
2873
+ case 65521: // 0xFFF1
2874
+ vendorName = '(MatterTest)';
2875
+ break;
2876
+ }
2877
+ return vendorName;
2878
+ };
2879
+ }
2880
+ //# sourceMappingURL=matterbridge.js.map