@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,2605 @@
1
+ /**
2
+ * This file contains the class Frontend.
3
+ *
4
+ * @file frontend.ts
5
+ * @author Luca Liguori
6
+ * @created 2025-01-13
7
+ * @version 1.3.3
8
+ * @license Apache-2.0
9
+ *
10
+ * Copyright 2025, 2026, 2027 Luca Liguori.
11
+ *
12
+ * Licensed under the Apache License, Version 2.0 (the "License");
13
+ * you may not use this file except in compliance with the License.
14
+ * You may obtain a copy of the License at
15
+ *
16
+ * http://www.apache.org/licenses/LICENSE-2.0
17
+ *
18
+ * Unless required by applicable law or agreed to in writing, software
19
+ * distributed under the License is distributed on an "AS IS" BASIS,
20
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21
+ * See the License for the specific language governing permissions and
22
+ * limitations under the License.
23
+ */
24
+ /* eslint-disable-next-line no-console */ /* istanbul ignore next */
25
+ if (process.argv.includes('--loader') || process.argv.includes('-loader'))
26
+ console.log('\u001B[32mFrontend loaded.\u001B[40;0m');
27
+ // Node modules
28
+ import os from 'node:os';
29
+ import path from 'node:path';
30
+ import EventEmitter from 'node:events';
31
+ // AnsiLogger module
32
+ import { AnsiLogger, stringify, debugStringify, CYAN, db, er, nf, rs, UNDERLINE, UNDERLINEOFF, YELLOW, nt } from 'node-ansi-logger';
33
+ import { Logger, Diagnostic, LogDestination, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, Lifecycle } from '@matter/general';
34
+ import { DeviceAdvertiser, DeviceCommissioner, FabricManager } from '@matter/protocol';
35
+ import { FabricIndex } from '@matter/types/datatype';
36
+ import { CommissioningOptions } from '@matter/types/commissioning';
37
+ import { BridgedDeviceBasicInformation } from '@matter/types/clusters/bridged-device-basic-information';
38
+ import { PowerSource } from '@matter/types/clusters/power-source';
39
+ // @matterbridge
40
+ import { createZip, formatBytes, formatPercent, formatUptime, getParameter, hasParameter, inspectError, isValidArray, isValidBoolean, isValidNumber, isValidObject, isValidString, wait, withTimeout, } from '@matterbridge/utils';
41
+ import { MATTER_LOGGER_FILE, MATTER_STORAGE_NAME, MATTERBRIDGE_DIAGNOSTIC_FILE, MATTERBRIDGE_HISTORY_FILE, MATTERBRIDGE_LOGGER_FILE, NODE_STORAGE_DIR, plg, } from '@matterbridge/types';
42
+ import { BroadcastServer } from '@matterbridge/thread';
43
+ import { capitalizeFirstLetter, getAttribute } from './matterbridgeEndpointHelpers.js';
44
+ import { cliEmitter, lastOsCpuUsage, lastProcessCpuUsage } from './cliEmitter.js';
45
+ import { generateHistoryPage } from './cliHistory.js';
46
+ export class Frontend extends EventEmitter {
47
+ matterbridge;
48
+ log;
49
+ port = 8283;
50
+ listening = false;
51
+ storedPassword = undefined;
52
+ authClients = [];
53
+ expressApp;
54
+ httpServer;
55
+ httpsServer;
56
+ webSocketServer;
57
+ server;
58
+ debug = hasParameter('debug') || hasParameter('verbose');
59
+ verbose = hasParameter('verbose');
60
+ constructor(matterbridge) {
61
+ super();
62
+ this.matterbridge = matterbridge;
63
+ this.log = new AnsiLogger({
64
+ logName: 'Frontend',
65
+ logNameColor: '\x1b[38;5;97m',
66
+ logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */,
67
+ logLevel: hasParameter('debug') ? "debug" /* LogLevel.DEBUG */ : "info" /* LogLevel.INFO */,
68
+ });
69
+ this.server = new BroadcastServer('frontend', this.log);
70
+ this.server.on('broadcast_message', this.msgHandler.bind(this));
71
+ }
72
+ destroy() {
73
+ this.server.off('broadcast_message', this.msgHandler.bind(this));
74
+ this.server.close();
75
+ }
76
+ async msgHandler(msg) {
77
+ if (this.server.isWorkerRequest(msg)) {
78
+ // istanbul ignore else
79
+ if (this.verbose)
80
+ this.log.debug(`Received broadcast request ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}: ${debugStringify(msg)}${db}`);
81
+ switch (msg.type) {
82
+ case 'get_log_level':
83
+ this.server.respond({ ...msg, result: { logLevel: this.log.logLevel } });
84
+ break;
85
+ case 'set_log_level':
86
+ this.log.logLevel = msg.params.logLevel;
87
+ this.server.respond({ ...msg, result: { logLevel: this.log.logLevel } });
88
+ break;
89
+ case 'frontend_start':
90
+ await this.start(msg.params.port);
91
+ this.server.respond({ ...msg, result: { success: true } });
92
+ break;
93
+ case 'frontend_stop':
94
+ await this.stop();
95
+ this.server.respond({ ...msg, result: { success: true } });
96
+ break;
97
+ case 'frontend_refreshrequired':
98
+ this.wssSendRefreshRequired(msg.params.changed, msg.params.matter ? { matter: msg.params.matter } : undefined);
99
+ this.server.respond({ ...msg, result: { success: true } });
100
+ break;
101
+ case 'frontend_restartrequired':
102
+ this.wssSendRestartRequired(msg.params.snackbar, msg.params.fixed);
103
+ this.server.respond({ ...msg, result: { success: true } });
104
+ break;
105
+ case 'frontend_restartnotrequired':
106
+ this.wssSendRestartNotRequired(msg.params.snackbar);
107
+ this.server.respond({ ...msg, result: { success: true } });
108
+ break;
109
+ case 'frontend_updaterequired':
110
+ this.wssSendUpdateRequired(msg.params.devVersion);
111
+ this.server.respond({ ...msg, result: { success: true } });
112
+ break;
113
+ case 'frontend_snackbarmessage':
114
+ this.wssSendSnackbarMessage(msg.params.message, msg.params.timeout, msg.params.severity);
115
+ this.server.respond({ ...msg, result: { success: true } });
116
+ break;
117
+ case 'frontend_attributechanged':
118
+ this.wssSendAttributeChangedMessage(msg.params.plugin, msg.params.serialNumber, msg.params.uniqueId, msg.params.number, msg.params.id, msg.params.cluster, msg.params.attribute, msg.params.value);
119
+ this.server.respond({ ...msg, result: { success: true } });
120
+ break;
121
+ case 'frontend_logmessage':
122
+ this.wssSendLogMessage(msg.params.level, msg.params.time, msg.params.name, msg.params.message);
123
+ this.server.respond({ ...msg, result: { success: true } });
124
+ break;
125
+ case 'frontend_broadcast_message':
126
+ this.wssBroadcastMessage(msg.params.msg);
127
+ this.server.respond({ ...msg, result: { success: true } });
128
+ break;
129
+ default:
130
+ // istanbul ignore next
131
+ if (this.verbose)
132
+ this.log.debug(`Unknown broadcast request ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}`);
133
+ }
134
+ }
135
+ if (this.server.isWorkerResponse(msg) && msg.result) {
136
+ // istanbul ignore next
137
+ if (this.verbose)
138
+ this.log.debug(`Received broadcast response ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}: ${debugStringify(msg)}${db}`);
139
+ switch (msg.type) {
140
+ case 'plugins_install':
141
+ this.wssSendCloseSnackbarMessage(`Installing package ${msg.result.packageName}...`);
142
+ if (msg.result.success) {
143
+ this.wssSendRestartRequired(true, true);
144
+ this.wssSendRefreshRequired('plugins');
145
+ this.wssSendSnackbarMessage(`Installed package ${msg.result.packageName}`, 5, 'success');
146
+ }
147
+ else {
148
+ this.wssSendSnackbarMessage(`Package ${msg.result.packageName} not installed`, 10, 'error');
149
+ }
150
+ break;
151
+ case 'plugins_uninstall':
152
+ this.wssSendCloseSnackbarMessage(`Uninstalling package ${msg.result.packageName}...`);
153
+ if (msg.result.success) {
154
+ this.wssSendRestartRequired(true, true);
155
+ this.wssSendRefreshRequired('plugins');
156
+ this.wssSendSnackbarMessage(`Uninstalled package ${msg.result.packageName}`, 5, 'success');
157
+ }
158
+ else {
159
+ this.wssSendSnackbarMessage(`Package ${msg.result.packageName} not uninstalled`, 10, 'error');
160
+ }
161
+ break;
162
+ }
163
+ }
164
+ }
165
+ set logLevel(logLevel) {
166
+ this.log.logLevel = logLevel;
167
+ }
168
+ validateReq(req, res) {
169
+ if (req.ip && !this.authClients.includes(req.ip)) {
170
+ this.log.warn(`Warning blocked unauthorized access request ${req.originalUrl ?? req.url} from ${req.ip}`);
171
+ res.status(401).json({ error: 'Unauthorized' });
172
+ return false;
173
+ }
174
+ return true;
175
+ }
176
+ async start(port = 8283) {
177
+ this.port = port;
178
+ this.storedPassword = await this.matterbridge.nodeContext?.get('password', '');
179
+ this.log.debug(`Initializing the frontend ${hasParameter('ssl') ? 'https' : 'http'} server on port ${YELLOW}${this.port}${db}`);
180
+ // Initialize multer with the upload directory
181
+ const multer = await import('multer');
182
+ const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads'); // Is created by matterbridge initialize
183
+ const upload = multer.default({ dest: uploadDir });
184
+ // Create the express app that serves the frontend
185
+ const express = await import('express');
186
+ this.expressApp = express.default();
187
+ // Inject logging/debug wrapper for route/middleware registration
188
+ /*
189
+ const methods = ['get', 'post', 'put', 'delete', 'use'];
190
+ for (const method of methods) {
191
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
192
+ const original = (this.expressApp as any)[method].bind(this.expressApp);
193
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
194
+ (this.expressApp as any)[method] = (path: any, ...rest: any) => {
195
+ try {
196
+ console.log(`[DEBUG] Registering ${method.toUpperCase()} route:`, path);
197
+ return original(path, ...rest);
198
+ } catch (err) {
199
+ console.error(`[ERROR] Failed to register route: ${path}`);
200
+ throw err;
201
+ }
202
+ };
203
+ }
204
+ */
205
+ // Log all requests to the server for debugging
206
+ /*
207
+ this.expressApp.use((req, res, next) => {
208
+ this.log.debug(`***Received request on expressApp: ${req.method} ${req.url}`);
209
+ next();
210
+ });
211
+ */
212
+ // Serve static files from 'frontend/build' directory
213
+ this.expressApp.use(express.static(path.join(this.matterbridge.rootDirectory, 'apps', 'frontend', 'build')));
214
+ // Create a WebSocket server and attach it to the http or https server
215
+ this.log.debug(`Creating WebSocketServer...`);
216
+ const ws = await import('ws');
217
+ this.webSocketServer = new ws.WebSocketServer({ noServer: true });
218
+ this.emit('websocket_server_listening', hasParameter('ssl') ? 'wss' : 'ws');
219
+ this.webSocketServer.on('connection', (ws, request) => {
220
+ const clientIp = request.socket.remoteAddress;
221
+ // Set the global logger callback for the WebSocketServer
222
+ let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
223
+ // istanbul ignore else
224
+ if (this.matterbridge.getLogLevel() === "info" /* LogLevel.INFO */ || Logger.level === MatterLogLevel.INFO)
225
+ callbackLogLevel = "info" /* LogLevel.INFO */;
226
+ // istanbul ignore else
227
+ if (this.matterbridge.getLogLevel() === "debug" /* LogLevel.DEBUG */ || Logger.level === MatterLogLevel.DEBUG)
228
+ callbackLogLevel = "debug" /* LogLevel.DEBUG */;
229
+ AnsiLogger.setGlobalCallback(this.wssSendLogMessage.bind(this), callbackLogLevel);
230
+ this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
231
+ this.log.info(`WebSocketServer client "${clientIp}" connected to Matterbridge`);
232
+ ws.on('message', (message) => {
233
+ this.wsMessageHandler(ws, message);
234
+ });
235
+ ws.on('ping', () => {
236
+ this.log.debug('WebSocket client ping received');
237
+ ws.pong();
238
+ });
239
+ ws.on('pong', () => {
240
+ this.log.debug('WebSocket client pong received');
241
+ });
242
+ ws.on('close', () => {
243
+ this.log.info('WebSocket client disconnected');
244
+ // istanbul ignore else
245
+ if (this.webSocketServer?.clients.size === 0) {
246
+ AnsiLogger.setGlobalCallback(undefined);
247
+ this.log.debug('All WebSocket clients disconnected. WebSocketServer logger global callback removed');
248
+ this.authClients = [];
249
+ }
250
+ });
251
+ // istanbul ignore next
252
+ ws.on('error', (error) => {
253
+ // istanbul ignore next
254
+ this.log.error(`WebSocket client error: ${error}`);
255
+ });
256
+ });
257
+ this.webSocketServer.on('close', () => {
258
+ this.log.debug(`WebSocketServer closed`);
259
+ });
260
+ /* With { noServer: true } it never fires
261
+ this.webSocketServer.on('listening', () => {
262
+ this.log.info(`The WebSocketServer is listening`);
263
+ this.emit('websocket_server_listening', hasParameter('ssl') ? 'wss' : 'ws');
264
+ });
265
+ */
266
+ // istanbul ignore next
267
+ this.webSocketServer.on('error', (ws, error) => {
268
+ this.log.error(`WebSocketServer error: ${error}`);
269
+ });
270
+ if (!hasParameter('ssl')) {
271
+ // Create an HTTP server and attach the express app
272
+ const http = await import('node:http');
273
+ try {
274
+ this.log.debug(`Creating HTTP server...`);
275
+ this.httpServer = http.createServer(this.expressApp);
276
+ }
277
+ catch (error) {
278
+ this.log.error(`Failed to create HTTP server: ${error}`);
279
+ this.emit('server_error', error);
280
+ return;
281
+ }
282
+ // Listen on the specified port
283
+ if (hasParameter('ingress')) {
284
+ // We limit to all ipv4 addresses when running in ingress mode (Home Assistant add-on)
285
+ this.httpServer.listen(this.port, '0.0.0.0', () => {
286
+ this.log.info(`The frontend http server is listening on ${UNDERLINE}http://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
287
+ this.listening = true;
288
+ this.emit('server_listening', 'http', this.port, '0.0.0.0');
289
+ });
290
+ }
291
+ else {
292
+ // We listen to all available addresses
293
+ this.httpServer.listen(this.port, getParameter('bind'), () => {
294
+ const addr = this.httpServer?.address();
295
+ // istanbul ignore else
296
+ if (addr && typeof addr !== 'string') {
297
+ this.log.info(`The frontend http server is bound to ${addr.family} ${addr.address}:${addr.port}`);
298
+ }
299
+ // istanbul ignore else
300
+ if (this.matterbridge.systemInformation.ipv4Address !== '' && !getParameter('bind'))
301
+ this.log.info(`The frontend http server is listening on ${UNDERLINE}http://${this.matterbridge.systemInformation.ipv4Address}:${this.port}${UNDERLINEOFF}${rs}`);
302
+ // istanbul ignore else
303
+ if (this.matterbridge.systemInformation.ipv6Address !== '' && !getParameter('bind'))
304
+ this.log.info(`The frontend http server is listening on ${UNDERLINE}http://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
305
+ this.listening = true;
306
+ this.emit('server_listening', 'http', this.port);
307
+ });
308
+ }
309
+ this.httpServer.on('upgrade', async (req, socket, head) => {
310
+ try {
311
+ // Only proceed for real WebSocket upgrades
312
+ // istanbul ignore next cause is only a safety check
313
+ if ((req.headers.upgrade || '').toLowerCase() !== 'websocket') {
314
+ this.log.error(`WebSocket upgrade error: Invalid upgrade header ${req.headers.upgrade}`);
315
+ socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
316
+ return socket.destroy();
317
+ }
318
+ // Build a URL so we can read ?password=...
319
+ const url = new URL(req.url ?? '/', `http://${req.headers.host || 'localhost'}`);
320
+ // Validate WebSocket password
321
+ const password = url.searchParams.get('password') ?? '';
322
+ if (password !== this.storedPassword) {
323
+ this.log.error(`WebSocket upgrade error: Invalid password ${password ? '[redacted]' : '(empty)'}`);
324
+ socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
325
+ return socket.destroy();
326
+ }
327
+ // Complete the WebSocket handshake
328
+ this.log.debug(`WebSocket upgrade success host ${url.host} password ${password ? '[redacted]' : '(empty)'}`);
329
+ // istanbul ignore else
330
+ if (req.socket.remoteAddress)
331
+ this.authClients.push(req.socket.remoteAddress);
332
+ this.webSocketServer?.handleUpgrade(req, socket, head, (ws) => {
333
+ this.webSocketServer?.emit('connection', ws, req);
334
+ });
335
+ }
336
+ catch (err) {
337
+ /* istanbul ignore next: only triggered on unexpected internal error */
338
+ {
339
+ inspectError(this.log, 'WebSocket upgrade error:', err);
340
+ socket.write('HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n');
341
+ socket.destroy();
342
+ }
343
+ }
344
+ });
345
+ this.httpServer.on('error', (error) => {
346
+ this.log.error(`Frontend http server error listening on ${this.port}`);
347
+ switch (error.code) {
348
+ case 'EACCES':
349
+ this.log.error(`Port ${this.port} requires elevated privileges`);
350
+ break;
351
+ case 'EADDRINUSE':
352
+ this.log.error(`Port ${this.port} is already in use`);
353
+ break;
354
+ }
355
+ this.emit('server_error', error);
356
+ return;
357
+ });
358
+ }
359
+ else {
360
+ // SSL is enabled, load the certificate and the private key
361
+ let cert;
362
+ let key;
363
+ let ca;
364
+ let fullChain;
365
+ let pfx;
366
+ let passphrase;
367
+ let httpsServerOptions = {};
368
+ const fs = await import('node:fs');
369
+ if (fs.existsSync(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.p12'))) {
370
+ // Load the p12 certificate and the passphrase
371
+ try {
372
+ pfx = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.p12'));
373
+ this.log.info(`Loaded p12 certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.p12')}`);
374
+ }
375
+ catch (error) {
376
+ this.log.error(`Error reading p12 certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.p12')}: ${error}`);
377
+ this.emit('server_error', error);
378
+ return;
379
+ }
380
+ try {
381
+ passphrase = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pass'), 'utf8');
382
+ passphrase = passphrase.trim(); // Ensure no extra characters
383
+ this.log.info(`Loaded p12 passphrase file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pass')}`);
384
+ }
385
+ catch (error) {
386
+ this.log.error(`Error reading p12 passphrase file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pass')}: ${error}`);
387
+ this.emit('server_error', error);
388
+ return;
389
+ }
390
+ httpsServerOptions = { pfx, passphrase };
391
+ }
392
+ else {
393
+ // Load the SSL certificate, the private key and optionally the CA certificate. If the CA certificate is present, it will be used to create a full chain certificate.
394
+ try {
395
+ cert = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pem'), 'utf8');
396
+ this.log.info(`Loaded certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pem')}`);
397
+ }
398
+ catch (error) {
399
+ this.log.error(`Error reading certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pem')}: ${error}`);
400
+ this.emit('server_error', error);
401
+ return;
402
+ }
403
+ try {
404
+ key = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'key.pem'), 'utf8');
405
+ this.log.info(`Loaded key file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'key.pem')}`);
406
+ }
407
+ catch (error) {
408
+ this.log.error(`Error reading key file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'key.pem')}: ${error}`);
409
+ this.emit('server_error', error);
410
+ return;
411
+ }
412
+ try {
413
+ ca = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'ca.pem'), 'utf8');
414
+ fullChain = `${cert}\n${ca}`;
415
+ this.log.info(`Loaded CA certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'ca.pem')}`);
416
+ }
417
+ catch (error) {
418
+ this.log.info(`CA certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'ca.pem')} not loaded: ${error}`);
419
+ }
420
+ httpsServerOptions = { cert: fullChain ?? cert, key, ca };
421
+ }
422
+ if (hasParameter('mtls')) {
423
+ httpsServerOptions.requestCert = true; // Request client certificate
424
+ httpsServerOptions.rejectUnauthorized = true; // Require client certificate validation
425
+ }
426
+ // Create an HTTPS server with the SSL certificate and private key (ca is optional) and attach the express app
427
+ const https = await import('node:https');
428
+ try {
429
+ this.log.debug(`Creating HTTPS server...`);
430
+ this.httpsServer = https.createServer(httpsServerOptions, this.expressApp);
431
+ }
432
+ catch (error) {
433
+ this.log.error(`Failed to create HTTPS server: ${error}`);
434
+ this.emit('server_error', error);
435
+ return;
436
+ }
437
+ // Listen on the specified port
438
+ if (hasParameter('ingress')) {
439
+ // We limit to all ipv4 addresses when running in ingress mode (Home Assistant add-on)
440
+ this.httpsServer.listen(this.port, '0.0.0.0', () => {
441
+ this.log.info(`The frontend https server is listening on ${UNDERLINE}https://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
442
+ this.listening = true;
443
+ this.emit('server_listening', 'https', this.port, '0.0.0.0');
444
+ });
445
+ }
446
+ else {
447
+ // We listen to all available addresses
448
+ this.httpsServer.listen(this.port, getParameter('bind'), () => {
449
+ const addr = this.httpsServer?.address();
450
+ // istanbul ignore else
451
+ if (addr && typeof addr !== 'string') {
452
+ this.log.info(`The frontend https server is bound to ${addr.family} ${addr.address}:${addr.port}`);
453
+ }
454
+ // istanbul ignore else
455
+ if (this.matterbridge.systemInformation.ipv4Address !== '' && !getParameter('bind'))
456
+ this.log.info(`The frontend https server is listening on ${UNDERLINE}https://${this.matterbridge.systemInformation.ipv4Address}:${this.port}${UNDERLINEOFF}${rs}`);
457
+ // istanbul ignore else
458
+ if (this.matterbridge.systemInformation.ipv6Address !== '' && !getParameter('bind'))
459
+ this.log.info(`The frontend https server is listening on ${UNDERLINE}https://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
460
+ this.listening = true;
461
+ this.emit('server_listening', 'https', this.port);
462
+ });
463
+ }
464
+ this.httpsServer.on('upgrade', async (req, socket, head) => {
465
+ try {
466
+ // Only proceed for real WebSocket upgrades
467
+ // istanbul ignore next cause is only a safety check
468
+ if ((req.headers.upgrade || '').toLowerCase() !== 'websocket') {
469
+ socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
470
+ return socket.destroy();
471
+ }
472
+ // Build a URL so we can read ?password=...
473
+ const url = new URL(req.url ?? '/', `https://${req.headers.host || 'localhost'}`);
474
+ // Validate WebSocket password
475
+ const password = url.searchParams.get('password') ?? '';
476
+ if (password !== this.storedPassword) {
477
+ this.log.error(`WebSocket upgrade error: Invalid password ${password ? '[redacted]' : '(empty)'}`);
478
+ socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
479
+ return socket.destroy();
480
+ }
481
+ // Complete the WebSocket handshake
482
+ this.log.debug(`WebSocket upgrade success host ${url.host} password ${password ? '[redacted]' : '(empty)'}`);
483
+ // istanbul ignore else
484
+ if (req.socket.remoteAddress)
485
+ this.authClients.push(req.socket.remoteAddress);
486
+ this.webSocketServer?.handleUpgrade(req, socket, head, (ws) => {
487
+ this.webSocketServer?.emit('connection', ws, req);
488
+ });
489
+ }
490
+ catch (err) {
491
+ /* istanbul ignore next: only triggered on unexpected internal error */
492
+ {
493
+ inspectError(this.log, 'WebSocket upgrade error:', err);
494
+ socket.write('HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n');
495
+ socket.destroy();
496
+ }
497
+ }
498
+ });
499
+ this.httpsServer.on('error', (error) => {
500
+ this.log.error(`Frontend https server error listening on ${this.port}`);
501
+ switch (error.code) {
502
+ case 'EACCES':
503
+ this.log.error(`Port ${this.port} requires elevated privileges`);
504
+ break;
505
+ case 'EADDRINUSE':
506
+ this.log.error(`Port ${this.port} is already in use`);
507
+ break;
508
+ }
509
+ this.emit('server_error', error);
510
+ return;
511
+ });
512
+ }
513
+ // Subscribe to cli events
514
+ cliEmitter.removeAllListeners();
515
+ cliEmitter.on('uptime', (systemUptime, processUptime) => {
516
+ this.wssSendUptimeUpdate(systemUptime, processUptime);
517
+ });
518
+ cliEmitter.on('memory', (totalMememory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) => {
519
+ this.wssSendMemoryUpdate(totalMememory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers);
520
+ });
521
+ cliEmitter.on('cpu', (cpuUsage, processCpuUsage) => {
522
+ this.wssSendCpuUpdate(cpuUsage, processCpuUsage);
523
+ });
524
+ // Endpoint to validate login code
525
+ // curl -X POST "http://localhost:8283/api/login" -H "Content-Type: application/json" -d "{\"password\":\"Here\"}"
526
+ this.expressApp.post('/api/login', express.json(), async (req, res) => {
527
+ const { password } = req.body;
528
+ this.log.debug(`The frontend sent /api/login with password ${password ? '[redacted]' : '(empty)'}`);
529
+ if (this.storedPassword === '' || password === this.storedPassword) {
530
+ this.log.debug('/api/login password valid');
531
+ res.json({ valid: true });
532
+ if (req.ip)
533
+ this.authClients.push(req.ip);
534
+ }
535
+ else {
536
+ this.log.warn('/api/login error wrong password');
537
+ res.json({ valid: false });
538
+ }
539
+ });
540
+ // Endpoint to provide health check for docker
541
+ this.expressApp.get('/health', (req, res) => {
542
+ this.log.debug('Express received /health');
543
+ const healthStatus = {
544
+ status: 'ok', // Indicate service is healthy
545
+ uptime: process.uptime(), // Server uptime in seconds
546
+ timestamp: new Date().toISOString(), // Current timestamp
547
+ };
548
+ res.status(200).json(healthStatus);
549
+ });
550
+ // Endpoint to provide memory usage details
551
+ this.expressApp.get('/memory', async (req, res) => {
552
+ this.log.debug('Express received /memory');
553
+ // Memory usage from process
554
+ const memoryUsageRaw = process.memoryUsage();
555
+ const memoryUsage = {
556
+ rss: formatBytes(memoryUsageRaw.rss),
557
+ heapTotal: formatBytes(memoryUsageRaw.heapTotal),
558
+ heapUsed: formatBytes(memoryUsageRaw.heapUsed),
559
+ external: formatBytes(memoryUsageRaw.external),
560
+ arrayBuffers: formatBytes(memoryUsageRaw.arrayBuffers),
561
+ };
562
+ // V8 heap statistics
563
+ const { default: v8 } = await import('node:v8');
564
+ const heapStatsRaw = v8.getHeapStatistics();
565
+ const heapSpacesRaw = v8.getHeapSpaceStatistics();
566
+ // Format heapStats
567
+ const heapStats = Object.fromEntries(Object.entries(heapStatsRaw).map(([key, value]) => [key, formatBytes(value)]));
568
+ // Format heapSpaces
569
+ const heapSpaces = heapSpacesRaw.map((space) => ({
570
+ ...space,
571
+ space_size: formatBytes(space.space_size),
572
+ space_used_size: formatBytes(space.space_used_size),
573
+ space_available_size: formatBytes(space.space_available_size),
574
+ physical_space_size: formatBytes(space.physical_space_size),
575
+ }));
576
+ const { createRequire } = await import('node:module');
577
+ const require = createRequire(import.meta.url);
578
+ const cjsModules = Object.keys(require.cache).sort();
579
+ const memoryReport = {
580
+ memoryUsage,
581
+ heapStats,
582
+ heapSpaces,
583
+ cjsModules,
584
+ };
585
+ res.status(200).json(memoryReport);
586
+ });
587
+ // Endpoint to provide settings
588
+ this.expressApp.get('/api/settings', express.json(), async (req, res) => {
589
+ this.log.debug('The frontend sent /api/settings');
590
+ if (!this.validateReq(req, res))
591
+ return;
592
+ res.json(await this.getApiSettings());
593
+ });
594
+ // Endpoint to provide plugins
595
+ this.expressApp.get('/api/plugins', async (req, res) => {
596
+ this.log.debug('The frontend sent /api/plugins');
597
+ if (!this.validateReq(req, res))
598
+ return;
599
+ res.json(this.matterbridge.hasCleanupStarted ? [] : this.getPlugins());
600
+ });
601
+ // Endpoint to provide devices
602
+ this.expressApp.get('/api/devices', async (req, res) => {
603
+ this.log.debug('The frontend sent /api/devices');
604
+ if (!this.validateReq(req, res))
605
+ return;
606
+ res.json(this.matterbridge.hasCleanupStarted ? [] : this.getDevices());
607
+ });
608
+ // Endpoint to view the matterbridge log
609
+ this.expressApp.get('/api/view-mblog', async (req, res) => {
610
+ this.log.debug('The frontend sent /api/view-mblog');
611
+ if (!this.validateReq(req, res))
612
+ return;
613
+ try {
614
+ const fs = await import('node:fs');
615
+ const data = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE), 'utf8');
616
+ res.type('text/plain');
617
+ res.send(data);
618
+ }
619
+ catch (error) {
620
+ this.log.error(`Error reading matterbridge log file ${MATTERBRIDGE_LOGGER_FILE}: ${error instanceof Error ? error.message : error}`);
621
+ res.status(500).send('Error reading matterbridge log file. Please enable the matterbridge log on file in the settings.');
622
+ }
623
+ });
624
+ // Endpoint to view the matter.js log
625
+ this.expressApp.get('/api/view-mjlog', async (req, res) => {
626
+ this.log.debug('The frontend sent /api/view-mjlog');
627
+ if (!this.validateReq(req, res))
628
+ return;
629
+ try {
630
+ const fs = await import('node:fs');
631
+ const data = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, MATTER_LOGGER_FILE), 'utf8');
632
+ res.type('text/plain');
633
+ res.send(data);
634
+ }
635
+ catch (error) {
636
+ this.log.error(`Error reading matter log file ${MATTER_LOGGER_FILE}: ${error instanceof Error ? error.message : error}`);
637
+ res.status(500).send('Error reading matter log file. Please enable the matter log on file in the settings.');
638
+ }
639
+ });
640
+ // Endpoint to view the diagnostic.log
641
+ this.expressApp.get('/api/view-diagnostic', async (req, res) => {
642
+ this.log.debug('The frontend sent /api/view-diagnostic');
643
+ if (!this.validateReq(req, res))
644
+ return;
645
+ await this.generateDiagnostic();
646
+ try {
647
+ const fs = await import('node:fs');
648
+ const data = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_DIAGNOSTIC_FILE), 'utf8');
649
+ res.type('text/plain');
650
+ res.send(data.slice(29));
651
+ }
652
+ catch (error) {
653
+ // istanbul ignore next
654
+ this.log.error(`Error reading diagnostic log file ${MATTERBRIDGE_DIAGNOSTIC_FILE}: ${error instanceof Error ? error.message : error}`);
655
+ // istanbul ignore next
656
+ res.status(500).send('Error reading diagnostic log file.');
657
+ }
658
+ });
659
+ // Endpoint to download the diagnostic.log
660
+ this.expressApp.get('/api/download-diagnostic', async (req, res) => {
661
+ this.log.debug(`The frontend sent /api/download-diagnostic`);
662
+ if (!this.validateReq(req, res))
663
+ return;
664
+ await this.generateDiagnostic();
665
+ try {
666
+ const fs = await import('node:fs');
667
+ await fs.promises.access(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_DIAGNOSTIC_FILE), fs.constants.F_OK);
668
+ const data = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_DIAGNOSTIC_FILE), 'utf8');
669
+ await fs.promises.writeFile(path.join(os.tmpdir(), MATTERBRIDGE_DIAGNOSTIC_FILE), data, 'utf-8');
670
+ }
671
+ catch (error) {
672
+ // istanbul ignore next
673
+ this.log.debug(`Error in /api/download-diagnostic: ${error instanceof Error ? error.message : error}`);
674
+ }
675
+ res.type('text/plain');
676
+ res.download(path.join(os.tmpdir(), MATTERBRIDGE_DIAGNOSTIC_FILE), MATTERBRIDGE_DIAGNOSTIC_FILE, (error) => {
677
+ /* istanbul ignore if */
678
+ if (error) {
679
+ this.log.error(`Error downloading file ${MATTERBRIDGE_DIAGNOSTIC_FILE}: ${error instanceof Error ? error.message : error}`);
680
+ res.status(500).send('Error downloading the diagnostic log file');
681
+ }
682
+ });
683
+ });
684
+ // Endpoint to view the history.html
685
+ this.expressApp.get('/api/viewhistory', async (req, res) => {
686
+ this.log.debug('The frontend sent /api/viewhistory');
687
+ if (!this.validateReq(req, res))
688
+ return;
689
+ try {
690
+ const fs = await import('node:fs');
691
+ const data = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_HISTORY_FILE), 'utf8');
692
+ res.type('text/html');
693
+ res.send(data);
694
+ }
695
+ catch (error) {
696
+ this.log.error(`Error in /api/viewhistory reading history file ${MATTERBRIDGE_HISTORY_FILE}: ${error instanceof Error ? error.message : error}`);
697
+ res.status(500).send('Error reading history file.');
698
+ }
699
+ });
700
+ // Endpoint to download the history.html
701
+ this.expressApp.get('/api/downloadhistory', async (req, res) => {
702
+ this.log.debug(`The frontend sent /api/downloadhistory`);
703
+ if (!this.validateReq(req, res))
704
+ return;
705
+ try {
706
+ const fs = await import('node:fs');
707
+ await fs.promises.access(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_HISTORY_FILE), fs.constants.F_OK);
708
+ const data = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_HISTORY_FILE), 'utf8');
709
+ await fs.promises.writeFile(path.join(os.tmpdir(), MATTERBRIDGE_HISTORY_FILE), data, 'utf-8');
710
+ res.type('text/plain');
711
+ res.download(path.join(os.tmpdir(), MATTERBRIDGE_HISTORY_FILE), MATTERBRIDGE_HISTORY_FILE, (error) => {
712
+ /* istanbul ignore if */
713
+ if (error) {
714
+ this.log.error(`Error in /api/downloadhistory downloading history file ${MATTERBRIDGE_HISTORY_FILE}: ${error instanceof Error ? error.message : error}`);
715
+ res.status(500).send('Error downloading history file');
716
+ }
717
+ });
718
+ }
719
+ catch (error) {
720
+ this.log.error(`Error in /api/downloadhistory reading history file ${MATTERBRIDGE_HISTORY_FILE}: ${error instanceof Error ? error.message : error}`);
721
+ res.status(500).send('Error reading history file.');
722
+ }
723
+ });
724
+ // Endpoint to view the shelly log
725
+ this.expressApp.get('/api/shellyviewsystemlog', async (req, res) => {
726
+ this.log.debug('The frontend sent /api/shellyviewsystemlog');
727
+ if (!this.validateReq(req, res))
728
+ return;
729
+ try {
730
+ const fs = await import('node:fs');
731
+ const data = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'shelly.log'), 'utf8');
732
+ res.type('text/plain');
733
+ res.send(data);
734
+ }
735
+ catch (error) {
736
+ this.log.error(`Error reading shelly log file ${MATTERBRIDGE_LOGGER_FILE}: ${error instanceof Error ? error.message : error}`);
737
+ res.status(500).send('Error reading shelly log file. Please create the shelly system log before loading it.');
738
+ }
739
+ });
740
+ // Endpoint to download the matterbridge log
741
+ this.expressApp.get('/api/download-mblog', async (req, res) => {
742
+ this.log.debug(`The frontend sent /api/download-mblog ${path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE)}`);
743
+ if (!this.validateReq(req, res))
744
+ return;
745
+ const fs = await import('node:fs');
746
+ try {
747
+ await fs.promises.access(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE), fs.constants.F_OK);
748
+ const data = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE), 'utf8');
749
+ await fs.promises.writeFile(path.join(os.tmpdir(), MATTERBRIDGE_LOGGER_FILE), data, 'utf-8');
750
+ }
751
+ catch (error) {
752
+ await fs.promises.writeFile(path.join(os.tmpdir(), MATTERBRIDGE_LOGGER_FILE), 'Enable the matterbridge log on file in the settings to download the matterbridge log.', 'utf-8');
753
+ this.log.debug(`Error in /api/download-mblog: ${error instanceof Error ? error.message : error}`);
754
+ }
755
+ res.type('text/plain');
756
+ res.download(path.join(os.tmpdir(), MATTERBRIDGE_LOGGER_FILE), 'matterbridge.log', (error) => {
757
+ /* istanbul ignore if */
758
+ if (error) {
759
+ this.log.error(`Error downloading log file ${MATTERBRIDGE_LOGGER_FILE}: ${error instanceof Error ? error.message : error}`);
760
+ res.status(500).send('Error downloading the matterbridge log file');
761
+ }
762
+ });
763
+ });
764
+ // Endpoint to download the matter log
765
+ this.expressApp.get('/api/download-mjlog', async (req, res) => {
766
+ this.log.debug(`The frontend sent /api/download-mjlog ${path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE)}`);
767
+ if (!this.validateReq(req, res))
768
+ return;
769
+ const fs = await import('node:fs');
770
+ try {
771
+ await fs.promises.access(path.join(this.matterbridge.matterbridgeDirectory, MATTER_LOGGER_FILE), fs.constants.F_OK);
772
+ const data = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, MATTER_LOGGER_FILE), 'utf8');
773
+ await fs.promises.writeFile(path.join(os.tmpdir(), MATTER_LOGGER_FILE), data, 'utf-8');
774
+ }
775
+ catch (error) {
776
+ await fs.promises.writeFile(path.join(os.tmpdir(), MATTER_LOGGER_FILE), 'Enable the matter log on file in the settings to download the matter log.', 'utf-8');
777
+ this.log.debug(`Error in /api/download-mblog: ${error instanceof Error ? error.message : error}`);
778
+ }
779
+ res.type('text/plain');
780
+ res.download(path.join(os.tmpdir(), MATTER_LOGGER_FILE), 'matter.log', (error) => {
781
+ /* istanbul ignore if */
782
+ if (error) {
783
+ this.log.error(`Error downloading log file ${MATTER_LOGGER_FILE}: ${error instanceof Error ? error.message : error}`);
784
+ res.status(500).send('Error downloading the matter log file');
785
+ }
786
+ });
787
+ });
788
+ // Endpoint to download the shelly log
789
+ this.expressApp.get('/api/shellydownloadsystemlog', async (req, res) => {
790
+ this.log.debug('The frontend sent /api/shellydownloadsystemlog');
791
+ if (!this.validateReq(req, res))
792
+ return;
793
+ const fs = await import('node:fs');
794
+ try {
795
+ await fs.promises.access(path.join(this.matterbridge.matterbridgeDirectory, 'shelly.log'), fs.constants.F_OK);
796
+ const data = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'shelly.log'), 'utf8');
797
+ await fs.promises.writeFile(path.join(os.tmpdir(), 'shelly.log'), data, 'utf-8');
798
+ }
799
+ catch (error) {
800
+ await fs.promises.writeFile(path.join(os.tmpdir(), 'shelly.log'), 'Create the Shelly system log before downloading it.', 'utf-8');
801
+ this.log.debug(`Error in /api/shellydownloadsystemlog: ${error instanceof Error ? error.message : error}`);
802
+ }
803
+ res.type('text/plain');
804
+ res.download(path.join(os.tmpdir(), 'shelly.log'), 'shelly.log', (error) => {
805
+ /* istanbul ignore if */
806
+ if (error) {
807
+ this.log.error(`Error downloading Shelly system log file: ${error instanceof Error ? error.message : error}`);
808
+ res.status(500).send('Error downloading Shelly system log file');
809
+ }
810
+ });
811
+ });
812
+ // Endpoint to download the matterbridge storage directory
813
+ this.expressApp.get('/api/download-mbstorage', async (req, res) => {
814
+ this.log.debug('The frontend sent /api/download-mbstorage');
815
+ if (!this.validateReq(req, res))
816
+ return;
817
+ await createZip(path.join(os.tmpdir(), `matterbridge.${NODE_STORAGE_DIR}.zip`), path.join(this.matterbridge.matterbridgeDirectory, NODE_STORAGE_DIR));
818
+ res.download(path.join(os.tmpdir(), `matterbridge.${NODE_STORAGE_DIR}.zip`), `matterbridge.${NODE_STORAGE_DIR}.zip`, (error) => {
819
+ /* istanbul ignore if */
820
+ if (error) {
821
+ this.log.error(`Error downloading file ${`matterbridge.${NODE_STORAGE_DIR}.zip`}: ${error instanceof Error ? error.message : error}`);
822
+ res.status(500).send('Error downloading the matterbridge storage file');
823
+ }
824
+ });
825
+ });
826
+ // Endpoint to download the matter storage file
827
+ this.expressApp.get('/api/download-mjstorage', async (req, res) => {
828
+ this.log.debug('The frontend sent /api/download-mjstorage');
829
+ if (!this.validateReq(req, res))
830
+ return;
831
+ await createZip(path.join(os.tmpdir(), `matterbridge.${MATTER_STORAGE_NAME}.zip`), path.join(this.matterbridge.matterbridgeDirectory, MATTER_STORAGE_NAME));
832
+ res.download(path.join(os.tmpdir(), `matterbridge.${MATTER_STORAGE_NAME}.zip`), `matterbridge.${MATTER_STORAGE_NAME}.zip`, (error) => {
833
+ /* istanbul ignore if */
834
+ if (error) {
835
+ this.log.error(`Error downloading the matter storage matterbridge.${MATTER_STORAGE_NAME}.zip: ${error instanceof Error ? error.message : error}`);
836
+ res.status(500).send('Error downloading the matter storage zip file');
837
+ }
838
+ });
839
+ });
840
+ // Endpoint to download the matterbridge plugin directory
841
+ this.expressApp.get('/api/download-pluginstorage', async (req, res) => {
842
+ this.log.debug('The frontend sent /api/download-pluginstorage');
843
+ if (!this.validateReq(req, res))
844
+ return;
845
+ await createZip(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), this.matterbridge.matterbridgePluginDirectory);
846
+ res.download(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), `matterbridge.pluginstorage.zip`, (error) => {
847
+ /* istanbul ignore if */
848
+ if (error) {
849
+ this.log.error(`Error downloading file matterbridge.pluginstorage.zip: ${error instanceof Error ? error.message : error}`);
850
+ res.status(500).send('Error downloading the matterbridge plugin storage file');
851
+ }
852
+ });
853
+ });
854
+ // Endpoint to download the matterbridge plugin config files
855
+ this.expressApp.get('/api/download-pluginconfig', async (req, res) => {
856
+ this.log.debug('The frontend sent /api/download-pluginconfig');
857
+ if (!this.validateReq(req, res))
858
+ return;
859
+ await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
860
+ res.download(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), `matterbridge.pluginconfig.zip`, (error) => {
861
+ /* istanbul ignore if */
862
+ if (error) {
863
+ this.log.error(`Error downloading file matterbridge.pluginconfig.zip: ${error instanceof Error ? error.message : error}`);
864
+ res.status(500).send('Error downloading the matterbridge plugin config file');
865
+ }
866
+ });
867
+ });
868
+ // Endpoint to download the matterbridge backup (created with the backup command)
869
+ this.expressApp.get('/api/download-backup', async (req, res) => {
870
+ this.log.debug('The frontend sent /api/download-backup');
871
+ if (!this.validateReq(req, res))
872
+ return;
873
+ res.download(path.join(os.tmpdir(), `matterbridge.backup.zip`), `matterbridge.backup.zip`, (error) => {
874
+ /* istanbul ignore if */
875
+ if (error) {
876
+ this.log.error(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
877
+ res.status(500).send(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
878
+ }
879
+ });
880
+ });
881
+ // Endpoint to upload a package
882
+ this.expressApp.post('/api/uploadpackage', upload.single('file'), async (req, res) => {
883
+ if (!this.validateReq(req, res))
884
+ return;
885
+ const { filename } = req.body;
886
+ const file = req.file;
887
+ /* istanbul ignore if */
888
+ if (!file || !filename) {
889
+ this.log.error(`uploadpackage: invalid request: file and filename are required`);
890
+ res.status(400).send('Invalid request: file and filename are required');
891
+ return;
892
+ }
893
+ this.wssSendSnackbarMessage(`Installing package ${filename}. Please wait...`, 0);
894
+ // Define the path where the plugin file will be saved
895
+ const filePath = path.join(this.matterbridge.matterbridgeDirectory, 'uploads', filename);
896
+ try {
897
+ // Move the uploaded file to the specified path
898
+ const fs = await import('node:fs');
899
+ await fs.promises.rename(file.path, filePath);
900
+ this.log.info(`File ${plg}${filename}${nf} uploaded successfully`);
901
+ // Install the plugin package
902
+ if (filename.endsWith('.tgz')) {
903
+ const { spawnCommand } = await import('./spawn.js');
904
+ if (await spawnCommand('npm', ['install', '-g', filePath, '--omit=dev', '--verbose'], 'install', filename)) {
905
+ this.log.info(`Plugin package ${plg}${filename}${nf} installed successfully. Full restart required.`);
906
+ this.wssSendCloseSnackbarMessage(`Installing package ${filename}. Please wait...`);
907
+ this.wssSendSnackbarMessage(`Installed package ${filename}`, 10, 'success');
908
+ this.wssSendRestartRequired();
909
+ res.send(`Plugin package ${filename} uploaded and installed successfully`);
910
+ }
911
+ else {
912
+ this.log.error(`Error uploading or installing plugin package file ${plg}${filename}${er}`);
913
+ this.wssSendCloseSnackbarMessage(`Installing package ${filename}. Please wait...`);
914
+ this.wssSendSnackbarMessage(`Error uploading or installing plugin package ${filename}`, 10, 'error');
915
+ res.status(500).send(`Error uploading or installing plugin package ${filename}`);
916
+ }
917
+ }
918
+ else {
919
+ res.send(`File ${filename} uploaded successfully`);
920
+ }
921
+ }
922
+ catch (err) {
923
+ this.log.error(`Error uploading or installing plugin package file ${plg}${filename}${er}:`, err);
924
+ this.wssSendCloseSnackbarMessage(`Installing package ${filename}. Please wait...`);
925
+ this.wssSendSnackbarMessage(`Error uploading or installing plugin package ${filename}`, 10, 'error');
926
+ res.status(500).send(`Error uploading or installing plugin package ${filename}`);
927
+ }
928
+ });
929
+ // Fallback for routing (must be the last route)
930
+ this.expressApp.use((req, res) => {
931
+ const filePath = path.resolve(this.matterbridge.rootDirectory, 'apps', 'frontend', 'build');
932
+ this.log.debug(`The frontend sent ${req.url} method ${req.method}: sending index.html in ${filePath} as fallback`);
933
+ res.sendFile('index.html', { root: filePath });
934
+ });
935
+ this.log.debug(`Frontend initialized on port ${YELLOW}${this.port}${db} static ${UNDERLINE}${path.join(this.matterbridge.rootDirectory, 'apps', 'frontend', 'build')}${UNDERLINEOFF}${rs}`);
936
+ }
937
+ async stop() {
938
+ this.log.debug('Stopping the frontend...');
939
+ const ws = await import('ws');
940
+ // Remove listeners from the express app
941
+ if (this.expressApp) {
942
+ this.expressApp.removeAllListeners();
943
+ this.expressApp = undefined;
944
+ this.log.debug('Frontend app closed successfully');
945
+ }
946
+ // Close the WebSocket server
947
+ if (this.webSocketServer) {
948
+ this.log.debug('Closing WebSocket server...');
949
+ // Close all active connections
950
+ this.webSocketServer.clients.forEach((client) => {
951
+ if (client.readyState === ws.WebSocket.OPEN) {
952
+ client.close();
953
+ }
954
+ });
955
+ await withTimeout(new Promise((resolve) => {
956
+ this.webSocketServer?.close((error) => {
957
+ // istanbul ignore if
958
+ if (error) {
959
+ // istanbul ignore next
960
+ this.log.error(`Error closing WebSocket server: ${error}`);
961
+ }
962
+ else {
963
+ this.log.debug('WebSocket server closed successfully');
964
+ this.emit('websocket_server_stopped');
965
+ }
966
+ resolve();
967
+ });
968
+ }), 10000, false);
969
+ this.webSocketServer.removeAllListeners();
970
+ this.webSocketServer = undefined;
971
+ }
972
+ // Close the http server
973
+ if (this.httpServer) {
974
+ this.log.debug('Closing http server...');
975
+ /*
976
+ await withTimeout(
977
+ new Promise<void>((resolve) => {
978
+ this.httpServer?.close((error) => {
979
+ if (error) {
980
+ // istanbul ignore next
981
+ this.log.error(`Error closing http server: ${error}`);
982
+ } else {
983
+ this.log.debug('Http server closed successfully');
984
+ this.emit('server_stopped');
985
+ }
986
+ resolve();
987
+ });
988
+ }),
989
+ 5000,
990
+ false,
991
+ );
992
+ */
993
+ this.httpServer.close();
994
+ this.log.debug('Http server closed successfully');
995
+ this.listening = false;
996
+ this.emit('server_stopped');
997
+ this.httpServer.removeAllListeners();
998
+ this.httpServer = undefined;
999
+ this.log.debug('Frontend http server closed successfully');
1000
+ }
1001
+ // Close the https server
1002
+ if (this.httpsServer) {
1003
+ this.log.debug('Closing https server...');
1004
+ /*
1005
+ await withTimeout(
1006
+ new Promise<void>((resolve) => {
1007
+ this.httpsServer?.close((error) => {
1008
+ if (error) {
1009
+ // istanbul ignore next
1010
+ this.log.error(`Error closing https server: ${error}`);
1011
+ } else {
1012
+ this.log.debug('Https server closed successfully');
1013
+ this.emit('server_stopped');
1014
+ }
1015
+ resolve();
1016
+ });
1017
+ }),
1018
+ 5000,
1019
+ false,
1020
+ );
1021
+ */
1022
+ this.httpsServer.close();
1023
+ this.log.debug('Https server closed successfully');
1024
+ this.listening = false;
1025
+ this.emit('server_stopped');
1026
+ this.httpsServer.removeAllListeners();
1027
+ this.httpsServer = undefined;
1028
+ this.log.debug('Frontend https server closed successfully');
1029
+ }
1030
+ this.log.debug('Frontend stopped successfully');
1031
+ }
1032
+ /**
1033
+ * Retrieves the api settings data.
1034
+ *
1035
+ * @returns {Promise<{ matterbridgeInformation: MatterbridgeInformation, systemInformation: SystemInformation }>} A promise that resolve in the api settings object.
1036
+ */
1037
+ async getApiSettings() {
1038
+ // Update the variable system information properties
1039
+ this.matterbridge.systemInformation.totalMemory = formatBytes(os.totalmem());
1040
+ this.matterbridge.systemInformation.freeMemory = formatBytes(os.freemem());
1041
+ this.matterbridge.systemInformation.systemUptime = formatUptime(os.uptime());
1042
+ this.matterbridge.systemInformation.processUptime = formatUptime(Math.floor(process.uptime()));
1043
+ this.matterbridge.systemInformation.cpuUsage = formatPercent(lastOsCpuUsage);
1044
+ this.matterbridge.systemInformation.processCpuUsage = formatPercent(lastProcessCpuUsage);
1045
+ this.matterbridge.systemInformation.rss = formatBytes(process.memoryUsage().rss);
1046
+ this.matterbridge.systemInformation.heapTotal = formatBytes(process.memoryUsage().heapTotal);
1047
+ this.matterbridge.systemInformation.heapUsed = formatBytes(process.memoryUsage().heapUsed);
1048
+ // Create the matterbridge information
1049
+ const info = {
1050
+ homeDirectory: this.matterbridge.homeDirectory,
1051
+ rootDirectory: this.matterbridge.rootDirectory,
1052
+ matterbridgeDirectory: this.matterbridge.matterbridgeDirectory,
1053
+ matterbridgePluginDirectory: this.matterbridge.matterbridgePluginDirectory,
1054
+ matterbridgeCertDirectory: this.matterbridge.matterbridgeCertDirectory,
1055
+ globalModulesDirectory: this.matterbridge.globalModulesDirectory,
1056
+ matterbridgeVersion: this.matterbridge.matterbridgeVersion,
1057
+ matterbridgeLatestVersion: this.matterbridge.matterbridgeLatestVersion,
1058
+ matterbridgeDevVersion: this.matterbridge.matterbridgeDevVersion,
1059
+ frontendVersion: this.matterbridge.frontendVersion,
1060
+ bridgeMode: this.matterbridge.bridgeMode,
1061
+ restartMode: this.matterbridge.restartMode,
1062
+ virtualMode: this.matterbridge.virtualMode,
1063
+ profile: this.matterbridge.profile,
1064
+ readOnly: this.matterbridge.readOnly,
1065
+ shellyBoard: this.matterbridge.shellyBoard,
1066
+ shellySysUpdate: this.matterbridge.shellySysUpdate,
1067
+ shellyMainUpdate: this.matterbridge.shellyMainUpdate,
1068
+ loggerLevel: await this.matterbridge.getLogLevel(),
1069
+ fileLogger: this.matterbridge.fileLogger,
1070
+ matterLoggerLevel: Logger.level,
1071
+ matterFileLogger: this.matterbridge.matterFileLogger,
1072
+ matterMdnsInterface: this.matterbridge.mdnsInterface,
1073
+ matterIpv4Address: this.matterbridge.ipv4Address,
1074
+ matterIpv6Address: this.matterbridge.ipv6Address,
1075
+ matterPort: (await this.matterbridge.nodeContext?.get('matterport', 5540)) ?? 5540,
1076
+ matterDiscriminator: await this.matterbridge.nodeContext?.get('matterdiscriminator'),
1077
+ matterPasscode: await this.matterbridge.nodeContext?.get('matterpasscode'),
1078
+ restartRequired: this.matterbridge.restartRequired,
1079
+ fixedRestartRequired: this.matterbridge.fixedRestartRequired,
1080
+ updateRequired: this.matterbridge.updateRequired,
1081
+ };
1082
+ return { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: info };
1083
+ }
1084
+ /**
1085
+ * Retrieves the reachable attribute.
1086
+ *
1087
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint object.
1088
+ * @returns {boolean} The reachable attribute.
1089
+ */
1090
+ getReachability(device) {
1091
+ if (this.matterbridge.hasCleanupStarted)
1092
+ return false; // Skip if cleanup has started
1093
+ if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
1094
+ return false;
1095
+ if (device.hasClusterServer(BridgedDeviceBasicInformation.Cluster.id))
1096
+ return device.getAttribute(BridgedDeviceBasicInformation.Cluster.id, 'reachable');
1097
+ if (device.mode === 'server' && device.serverNode && device.serverNode.state.basicInformation.reachable !== undefined)
1098
+ return device.serverNode.state.basicInformation.reachable;
1099
+ if (this.matterbridge.bridgeMode === 'childbridge')
1100
+ return true;
1101
+ return false;
1102
+ }
1103
+ /**
1104
+ * Retrieves the power source attribute.
1105
+ *
1106
+ * @param {MatterbridgeEndpoint} endpoint - The MatterbridgeDevice to retrieve the power source from.
1107
+ * @returns {'ac' | 'dc' | 'ok' | 'warning' | 'critical' | undefined} The power source attribute.
1108
+ */
1109
+ getPowerSource(endpoint) {
1110
+ if (this.matterbridge.hasCleanupStarted)
1111
+ return undefined; // Skip if cleanup has started
1112
+ if (!endpoint.lifecycle.isReady || endpoint.construction.status !== Lifecycle.Status.Active)
1113
+ return undefined;
1114
+ const powerSource = (device) => {
1115
+ const featureMap = device.getAttribute(PowerSource.Cluster.id, 'featureMap');
1116
+ if (featureMap.wired) {
1117
+ const wiredCurrentType = device.getAttribute(PowerSource.Cluster.id, 'wiredCurrentType');
1118
+ return ['ac', 'dc'][wiredCurrentType];
1119
+ }
1120
+ if (featureMap.battery) {
1121
+ const batChargeLevel = device.getAttribute(PowerSource.Cluster.id, 'batChargeLevel');
1122
+ return ['ok', 'warning', 'critical'][batChargeLevel];
1123
+ }
1124
+ return;
1125
+ };
1126
+ // Root endpoint
1127
+ if (endpoint.hasClusterServer(PowerSource.Cluster.id))
1128
+ return powerSource(endpoint);
1129
+ // Child endpoints
1130
+ for (const child of endpoint.getChildEndpoints()) {
1131
+ // istanbul ignore else
1132
+ if (child.hasClusterServer(PowerSource.Cluster.id))
1133
+ return powerSource(child);
1134
+ }
1135
+ }
1136
+ /**
1137
+ * Retrieves the battery level attribute.
1138
+ *
1139
+ * @param {MatterbridgeEndpoint} endpoint - The MatterbridgeDevice to retrieve the power source from.
1140
+ * @returns {number | undefined} The battery level attribute.
1141
+ */
1142
+ getBatteryLevel(endpoint) {
1143
+ if (this.matterbridge.hasCleanupStarted)
1144
+ return undefined; // Skip if cleanup has started
1145
+ if (!endpoint.lifecycle.isReady || endpoint.construction.status !== Lifecycle.Status.Active)
1146
+ return undefined;
1147
+ const batteryLevel = (device) => {
1148
+ const featureMap = device.getAttribute(PowerSource.Cluster.id, 'featureMap');
1149
+ if (featureMap.battery) {
1150
+ const batChargeLevel = device.getAttribute(PowerSource.Cluster.id, 'batPercentRemaining');
1151
+ return isValidNumber(batChargeLevel) ? batChargeLevel / 2 : undefined;
1152
+ }
1153
+ return undefined;
1154
+ };
1155
+ // Root endpoint
1156
+ if (endpoint.hasClusterServer(PowerSource.Cluster.id))
1157
+ return batteryLevel(endpoint);
1158
+ // Child endpoints
1159
+ for (const child of endpoint.getChildEndpoints()) {
1160
+ // istanbul ignore else
1161
+ if (child.hasClusterServer(PowerSource.Cluster.id))
1162
+ return batteryLevel(child);
1163
+ }
1164
+ }
1165
+ /**
1166
+ * Retrieves the cluster text description from a given device.
1167
+ * The output is a string with the attributes description of the cluster servers in the device to show in the frontend.
1168
+ *
1169
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint to retrieve the cluster text from.
1170
+ * @returns {string} The attributes description of the cluster servers in the device.
1171
+ */
1172
+ getClusterTextFromDevice(device) {
1173
+ if (this.matterbridge.hasCleanupStarted)
1174
+ return ''; // Skip if cleanup has started
1175
+ // istanbul ignore else
1176
+ if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
1177
+ return '';
1178
+ const getUserLabel = (device) => {
1179
+ const labelList = getAttribute(device, 'userLabel', 'labelList');
1180
+ if (labelList) {
1181
+ const composed = labelList.find((entry) => entry.label === 'composed');
1182
+ if (composed)
1183
+ return 'Composed: ' + composed.value;
1184
+ }
1185
+ // istanbul ignore next cause is not reachable
1186
+ return '';
1187
+ };
1188
+ const getFixedLabel = (device) => {
1189
+ const labelList = getAttribute(device, 'fixedLabel', 'labelList');
1190
+ if (labelList) {
1191
+ const composed = labelList.find((entry) => entry.label === 'composed');
1192
+ if (composed)
1193
+ return 'Composed: ' + composed.value;
1194
+ }
1195
+ // istanbul ignore next cause is not reacheable
1196
+ return '';
1197
+ };
1198
+ let attributes = '';
1199
+ let supportedModes = [];
1200
+ device.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
1201
+ // console.log(`${device.deviceName} => Cluster: ${clusterName}-${clusterId} Attribute: ${attributeName}-${attributeId} Value(${typeof attributeValue}): ${attributeValue}`);
1202
+ if (typeof attributeValue === 'undefined' || attributeValue === undefined)
1203
+ return;
1204
+ if (clusterName === 'onOff' && attributeName === 'onOff')
1205
+ attributes += `OnOff: ${attributeValue} `;
1206
+ if (clusterName === 'switch' && attributeName === 'currentPosition')
1207
+ attributes += `Position: ${attributeValue} `;
1208
+ if (clusterName === 'windowCovering' && attributeName === 'currentPositionLiftPercent100ths' && isValidNumber(attributeValue, 0, 10000))
1209
+ attributes += `Cover position: ${attributeValue / 100}% `;
1210
+ if (clusterName === 'doorLock' && attributeName === 'lockState')
1211
+ attributes += `State: ${attributeValue === 1 ? 'Locked' : 'Not locked'} `;
1212
+ if (clusterName === 'thermostat' && attributeName === 'localTemperature' && isValidNumber(attributeValue))
1213
+ attributes += `Temperature: ${attributeValue / 100}°C `;
1214
+ if (clusterName === 'thermostat' && attributeName === 'occupiedHeatingSetpoint' && isValidNumber(attributeValue))
1215
+ attributes += `Heat to: ${attributeValue / 100}°C `;
1216
+ if (clusterName === 'thermostat' && attributeName === 'occupiedCoolingSetpoint' && isValidNumber(attributeValue))
1217
+ attributes += `Cool to: ${attributeValue / 100}°C `;
1218
+ const modeClusters = ['modeSelect', 'rvcRunMode', 'rvcCleanMode', 'laundryWasherMode', 'ovenMode', 'microwaveOvenMode'];
1219
+ if (modeClusters.includes(clusterName) && attributeName === 'supportedModes') {
1220
+ supportedModes = attributeValue;
1221
+ }
1222
+ if (modeClusters.includes(clusterName) && attributeName === 'currentMode') {
1223
+ const supportedMode = supportedModes.find((mode) => mode.mode === attributeValue);
1224
+ if (supportedMode)
1225
+ attributes += `Mode: ${supportedMode.label} `;
1226
+ }
1227
+ const operationalStateClusters = ['operationalState', 'rvcOperationalState'];
1228
+ if (operationalStateClusters.includes(clusterName) && attributeName === 'operationalState')
1229
+ attributes += `OpState: ${attributeValue} `;
1230
+ if (clusterName === 'pumpConfigurationAndControl' && attributeName === 'operationMode')
1231
+ attributes += `Mode: ${attributeValue} `;
1232
+ if (clusterName === 'valveConfigurationAndControl' && attributeName === 'currentState')
1233
+ attributes += `State: ${attributeValue} `;
1234
+ if (clusterName === 'levelControl' && attributeName === 'currentLevel')
1235
+ attributes += `Level: ${attributeValue} `;
1236
+ if (clusterName === 'colorControl' && attributeName === 'colorMode' && isValidNumber(attributeValue, 0, 2))
1237
+ attributes += `Mode: ${['HS', 'XY', 'CT'][attributeValue]} `;
1238
+ if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 0 && attributeName === 'currentHue' && isValidNumber(attributeValue))
1239
+ attributes += `Hue: ${Math.round(attributeValue)} `;
1240
+ if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 0 && attributeName === 'currentSaturation' && isValidNumber(attributeValue))
1241
+ attributes += `Saturation: ${Math.round(attributeValue)} `;
1242
+ if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 1 && attributeName === 'currentX' && isValidNumber(attributeValue))
1243
+ attributes += `X: ${Math.round(attributeValue / 655.36) / 100} `;
1244
+ if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 1 && attributeName === 'currentY' && isValidNumber(attributeValue))
1245
+ attributes += `Y: ${Math.round(attributeValue / 655.36) / 100} `;
1246
+ if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 2 && attributeName === 'colorTemperatureMireds' && isValidNumber(attributeValue))
1247
+ attributes += `ColorTemp: ${Math.round(attributeValue)} `;
1248
+ if (clusterName === 'booleanState' && attributeName === 'stateValue')
1249
+ attributes += `Contact: ${attributeValue} `;
1250
+ if (clusterName === 'booleanStateConfiguration' && attributeName === 'alarmsActive' && isValidObject(attributeValue))
1251
+ attributes += `Active alarms: ${stringify(attributeValue)} `;
1252
+ if (clusterName === 'smokeCoAlarm' && attributeName === 'smokeState')
1253
+ attributes += `Smoke: ${attributeValue} `;
1254
+ if (clusterName === 'smokeCoAlarm' && attributeName === 'coState')
1255
+ attributes += `Co: ${attributeValue} `;
1256
+ if (clusterName === 'fanControl' && attributeName === 'fanMode')
1257
+ attributes += `Mode: ${attributeValue} `;
1258
+ if (clusterName === 'fanControl' && attributeName === 'percentCurrent')
1259
+ attributes += `Percent: ${attributeValue} `;
1260
+ if (clusterName === 'fanControl' && attributeName === 'speedCurrent')
1261
+ attributes += `Speed: ${attributeValue} `;
1262
+ if (clusterName === 'occupancySensing' && attributeName === 'occupancy' && isValidObject(attributeValue, 1))
1263
+ attributes += `Occupancy: ${attributeValue.occupied} `;
1264
+ if (clusterName === 'illuminanceMeasurement' && attributeName === 'measuredValue' && isValidNumber(attributeValue))
1265
+ attributes += `Illuminance: ${Math.round(Math.max(Math.pow(10, attributeValue / 10000), 0))} `;
1266
+ if (clusterName === 'airQuality' && attributeName === 'airQuality')
1267
+ attributes += `Air quality: ${attributeValue} `;
1268
+ if (clusterName === 'totalVolatileOrganicCompoundsConcentrationMeasurement' && attributeName === 'measuredValue')
1269
+ attributes += `Voc: ${attributeValue} `;
1270
+ if (clusterName === 'pm1ConcentrationMeasurement' && attributeName === 'measuredValue')
1271
+ attributes += `Pm1: ${attributeValue} `;
1272
+ if (clusterName === 'pm25ConcentrationMeasurement' && attributeName === 'measuredValue')
1273
+ attributes += `Pm2.5: ${attributeValue} `;
1274
+ if (clusterName === 'pm10ConcentrationMeasurement' && attributeName === 'measuredValue')
1275
+ attributes += `Pm10: ${attributeValue} `;
1276
+ if (clusterName === 'formaldehydeConcentrationMeasurement' && attributeName === 'measuredValue')
1277
+ attributes += `CH₂O: ${attributeValue} `;
1278
+ if (clusterName === 'temperatureMeasurement' && attributeName === 'measuredValue' && isValidNumber(attributeValue))
1279
+ attributes += `Temperature: ${attributeValue / 100}°C `;
1280
+ if (clusterName === 'relativeHumidityMeasurement' && attributeName === 'measuredValue' && isValidNumber(attributeValue))
1281
+ attributes += `Humidity: ${attributeValue / 100}% `;
1282
+ if (clusterName === 'pressureMeasurement' && attributeName === 'measuredValue')
1283
+ attributes += `Pressure: ${attributeValue} `;
1284
+ if (clusterName === 'flowMeasurement' && attributeName === 'measuredValue')
1285
+ attributes += `Flow: ${attributeValue} `;
1286
+ if (clusterName === 'fixedLabel' && attributeName === 'labelList')
1287
+ attributes += `${getFixedLabel(device)} `;
1288
+ if (clusterName === 'userLabel' && attributeName === 'labelList')
1289
+ attributes += `${getUserLabel(device)} `;
1290
+ });
1291
+ // console.log(`${device.deviceName}.forEachAttribute: ${attributes}`);
1292
+ return attributes.trimStart().trimEnd();
1293
+ }
1294
+ /**
1295
+ * Retrieves the registered plugins sanitized for res.json().
1296
+ *
1297
+ * @returns {ApiPlugin[]} An array of BaseRegisteredPlugin.
1298
+ */
1299
+ getPlugins() {
1300
+ if (this.matterbridge.hasCleanupStarted)
1301
+ return []; // Skip if cleanup has started
1302
+ const plugins = [];
1303
+ for (const plugin of this.matterbridge.plugins.array()) {
1304
+ plugins.push({
1305
+ path: plugin.path,
1306
+ type: plugin.type,
1307
+ name: plugin.name,
1308
+ version: plugin.version,
1309
+ description: plugin.description,
1310
+ author: plugin.author,
1311
+ homepage: plugin.homepage,
1312
+ help: plugin.help,
1313
+ changelog: plugin.changelog,
1314
+ funding: plugin.funding,
1315
+ latestVersion: plugin.latestVersion,
1316
+ devVersion: plugin.devVersion,
1317
+ locked: plugin.locked,
1318
+ error: plugin.error,
1319
+ enabled: plugin.enabled,
1320
+ loaded: plugin.loaded,
1321
+ started: plugin.started,
1322
+ configured: plugin.configured,
1323
+ restartRequired: plugin.restartRequired,
1324
+ registeredDevices: plugin.registeredDevices,
1325
+ configJson: plugin.configJson,
1326
+ schemaJson: plugin.schemaJson,
1327
+ hasWhiteList: plugin.configJson?.whiteList !== undefined,
1328
+ hasBlackList: plugin.configJson?.blackList !== undefined,
1329
+ // Childbridge mode specific data
1330
+ matter: plugin.serverNode ? this.matterbridge.getServerNodeData(plugin.serverNode) : undefined,
1331
+ });
1332
+ }
1333
+ return plugins;
1334
+ }
1335
+ /**
1336
+ * Retrieves the devices from Matterbridge.
1337
+ *
1338
+ * @param {string} [pluginName] - The name of the plugin to filter devices by.
1339
+ * @returns {ApiDevice[]} An array of ApiDevices for the frontend.
1340
+ */
1341
+ getDevices(pluginName) {
1342
+ if (this.matterbridge.hasCleanupStarted)
1343
+ return []; // Skip if cleanup has started
1344
+ const devices = [];
1345
+ for (const device of this.matterbridge.devices.array()) {
1346
+ // Filter by pluginName if provided
1347
+ if (pluginName && pluginName !== device.plugin)
1348
+ continue;
1349
+ // Check if the device has the required properties
1350
+ if (!device.plugin || !device.deviceType || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId || !device.lifecycle.isReady)
1351
+ continue;
1352
+ devices.push({
1353
+ pluginName: device.plugin,
1354
+ type: device.name + ' (0x' + device.deviceType.toString(16).padStart(4, '0') + ')',
1355
+ endpoint: device.number,
1356
+ name: device.deviceName,
1357
+ serial: device.serialNumber,
1358
+ productUrl: device.productUrl,
1359
+ configUrl: device.configUrl,
1360
+ uniqueId: device.uniqueId,
1361
+ reachable: this.getReachability(device),
1362
+ powerSource: this.getPowerSource(device),
1363
+ batteryLevel: this.getBatteryLevel(device),
1364
+ matter: device.mode === 'server' && device.serverNode ? this.matterbridge.getServerNodeData(device.serverNode) : undefined,
1365
+ cluster: this.getClusterTextFromDevice(device),
1366
+ });
1367
+ }
1368
+ return devices;
1369
+ }
1370
+ /**
1371
+ * Retrieves the clusters from a given plugin and endpoint number.
1372
+ *
1373
+ * Response for /api/clusters
1374
+ *
1375
+ * @param {string} pluginName - The name of the plugin.
1376
+ * @param {number} endpointNumber - The endpoint number.
1377
+ * @returns {ApiClusters | undefined} A promise that resolves to the clusters or undefined if not found.
1378
+ */
1379
+ getClusters(pluginName, endpointNumber) {
1380
+ if (this.matterbridge.hasCleanupStarted)
1381
+ return; // Skip if cleanup has started
1382
+ const endpoint = this.matterbridge.devices.array().find((d) => d.plugin === pluginName && d.maybeNumber === endpointNumber);
1383
+ if (!endpoint || !endpoint.plugin || !endpoint.maybeNumber || !endpoint.maybeId || !endpoint.deviceName || !endpoint.serialNumber) {
1384
+ this.log.error(`getClusters: no device found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
1385
+ return;
1386
+ }
1387
+ // this.log.debug(`***getClusters: getting clusters for device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${endpointNumber}`);
1388
+ // Get the device types from the main endpoint
1389
+ const deviceTypes = [];
1390
+ const clusters = [];
1391
+ endpoint.state.descriptor.deviceTypeList.forEach((d) => {
1392
+ deviceTypes.push(d.deviceType);
1393
+ });
1394
+ // Get the clusters from the main endpoint
1395
+ endpoint.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
1396
+ if (typeof attributeValue === 'undefined' || attributeValue === undefined)
1397
+ return;
1398
+ // istanbul ignore if cause is not reachable without the EveHistory cluster
1399
+ if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
1400
+ return;
1401
+ // console.log(
1402
+ // `${idn}${endpoint.deviceName}${rs}${nf} => Cluster: ${CYAN}${clusterName} (0x${clusterId.toString(16).padStart(2, '0')})${nf} Attribute: ${CYAN}${attributeName} (0x${attributeId.toString(16).padStart(2, '0')})${nf} Value: ${YELLOW}${typeof attributeValue === 'object' ? stringify(attributeValue as object) : attributeValue}${nf}`,
1403
+ // );
1404
+ clusters.push({
1405
+ endpoint: endpoint.number.toString(),
1406
+ number: endpoint.number,
1407
+ id: 'main',
1408
+ deviceTypes,
1409
+ clusterName: capitalizeFirstLetter(clusterName),
1410
+ clusterId: '0x' + clusterId.toString(16).padStart(2, '0'),
1411
+ attributeName,
1412
+ attributeId: '0x' + attributeId.toString(16).padStart(2, '0'),
1413
+ attributeValue: typeof attributeValue === 'object' ? stringify(attributeValue) : attributeValue.toString(),
1414
+ attributeLocalValue: attributeValue,
1415
+ });
1416
+ });
1417
+ // Get the child endpoints
1418
+ const childEndpoints = endpoint.getChildEndpoints();
1419
+ // if (childEndpoints.length === 0) {
1420
+ // this.log.debug(`***getClusters: found ${childEndpoints.length} child endpoints for device ${endpoint.deviceName} plugin ${pluginName} and endpoint number ${endpointNumber}`);
1421
+ // }
1422
+ childEndpoints.forEach((childEndpoint) => {
1423
+ // istanbul ignore if cause is not reachable: should never happen but ...
1424
+ if (!childEndpoint.maybeId || !childEndpoint.maybeNumber) {
1425
+ this.log.error(`getClusters: no child endpoint found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
1426
+ return;
1427
+ }
1428
+ // this.log.debug(`***getClusters: getting clusters for child endpoint ${childEndpoint.id} of device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${childEndpoint.number}`);
1429
+ // Get the device types of the child endpoint
1430
+ const deviceTypes = [];
1431
+ childEndpoint.state.descriptor.deviceTypeList.forEach((d) => {
1432
+ deviceTypes.push(d.deviceType);
1433
+ });
1434
+ childEndpoint.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
1435
+ if (typeof attributeValue === 'undefined' || attributeValue === undefined)
1436
+ return;
1437
+ // istanbul ignore if cause is not reachable without the EveHistory cluster
1438
+ if (clusterName === 'EveHistory' &&
1439
+ ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
1440
+ return;
1441
+ // console.log(
1442
+ // `${idn}${childEndpoint.deviceName}${rs}${nf} => Cluster: ${CYAN}${clusterName} (0x${clusterId.toString(16).padStart(2, '0')})${nf} Attribute: ${CYAN}${attributeName} (0x${attributeId.toString(16).padStart(2, '0')})${nf} Value: ${YELLOW}${typeof attributeValue === 'object' ? stringify(attributeValue as object) : attributeValue}${nf}`,
1443
+ // );
1444
+ clusters.push({
1445
+ endpoint: childEndpoint.number.toString(),
1446
+ number: childEndpoint.number,
1447
+ id: childEndpoint.id,
1448
+ deviceTypes,
1449
+ clusterName: capitalizeFirstLetter(clusterName),
1450
+ clusterId: '0x' + clusterId.toString(16).padStart(2, '0'),
1451
+ attributeName,
1452
+ attributeId: '0x' + attributeId.toString(16).padStart(2, '0'),
1453
+ attributeValue: typeof attributeValue === 'object' ? stringify(attributeValue) : attributeValue.toString(),
1454
+ attributeLocalValue: attributeValue,
1455
+ });
1456
+ });
1457
+ });
1458
+ return { plugin: endpoint.plugin, deviceName: endpoint.deviceName, serialNumber: endpoint.serialNumber, number: endpoint.number, id: endpoint.id, deviceTypes, clusters };
1459
+ }
1460
+ async generateDiagnostic() {
1461
+ this.log.debug('Generating diagnostic...');
1462
+ const serverNodes = [];
1463
+ // istanbul ignore else
1464
+ if (this.matterbridge.bridgeMode === 'bridge') {
1465
+ if (this.matterbridge.serverNode)
1466
+ serverNodes.push(this.matterbridge.serverNode);
1467
+ }
1468
+ else if (this.matterbridge.bridgeMode === 'childbridge') {
1469
+ for (const plugin of this.matterbridge.plugins.array()) {
1470
+ if (plugin.serverNode)
1471
+ serverNodes.push(plugin.serverNode);
1472
+ }
1473
+ }
1474
+ // istanbul ignore next
1475
+ for (const device of this.matterbridge.devices.array()) {
1476
+ if (device.serverNode)
1477
+ serverNodes.push(device.serverNode);
1478
+ }
1479
+ const fs = await import('node:fs');
1480
+ if (fs.existsSync(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_DIAGNOSTIC_FILE)))
1481
+ fs.unlinkSync(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_DIAGNOSTIC_FILE));
1482
+ const diagnosticDestination = LogDestination({ name: 'diagnostic', level: MatterLogLevel.INFO, format: MatterLogFormat.formats.plain });
1483
+ diagnosticDestination.write = async (text, _message) => {
1484
+ await fs.promises.appendFile(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_DIAGNOSTIC_FILE), text + '\n', { encoding: 'utf8' });
1485
+ };
1486
+ Logger.destinations.diagnostic = diagnosticDestination;
1487
+ if (!diagnosticDestination.context) {
1488
+ diagnosticDestination.context = Diagnostic.Context();
1489
+ }
1490
+ diagnosticDestination.context.run(() => diagnosticDestination.add(Diagnostic.message({
1491
+ now: new Date(),
1492
+ facility: 'Server nodes:',
1493
+ level: MatterLogLevel.INFO,
1494
+ prefix: Logger.nestingLevel ? '⎸'.padEnd(Logger.nestingLevel * 2) : '',
1495
+ values: [...serverNodes],
1496
+ })));
1497
+ delete Logger.destinations.diagnostic;
1498
+ await wait(500); // Wait for the log to be written
1499
+ }
1500
+ /**
1501
+ * Handles incoming websocket api request messages from the Matterbridge frontend.
1502
+ *
1503
+ * @param {WebSocket} client - The websocket client that sent the message.
1504
+ * @param {WebSocket.RawData} message - The raw data of the message received from the client.
1505
+ * @returns {Promise<void>} A promise that resolves when the message has been handled.
1506
+ */
1507
+ async wsMessageHandler(client, message) {
1508
+ let data;
1509
+ const sendResponse = (data) => {
1510
+ if (client.readyState === client.OPEN) {
1511
+ if ('response' in data) {
1512
+ const { response, ...rest } = data;
1513
+ this.log.debug(`Sending api response message: ${debugStringify(rest)}`);
1514
+ }
1515
+ else if ('error' in data) {
1516
+ this.log.debug(`Sending api error message: ${debugStringify(data)}`);
1517
+ }
1518
+ else {
1519
+ this.log.debug(`Sending api response message: ${debugStringify(data)}`);
1520
+ }
1521
+ client.send(JSON.stringify(data));
1522
+ }
1523
+ else {
1524
+ // istanbul ignore next cause is only a safety check
1525
+ this.log.error('Cannot send api response, client not connected');
1526
+ }
1527
+ };
1528
+ try {
1529
+ data = JSON.parse(message.toString());
1530
+ if (!isValidNumber(data.id) ||
1531
+ !isValidString(data.dst) ||
1532
+ !isValidString(data.src) ||
1533
+ !isValidString(data.method) /* || !isValidObject(data.params)*/ ||
1534
+ data.dst !== 'Matterbridge') {
1535
+ this.log.error(`Invalid message from websocket client: ${debugStringify(data)}`);
1536
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Invalid message' });
1537
+ return;
1538
+ }
1539
+ this.log.debug(`Received message from websocket client: ${debugStringify(data)}`);
1540
+ if (data.method === 'ping') {
1541
+ sendResponse({ id: data.id, method: 'pong', src: 'Matterbridge', dst: data.src, success: true, response: 'pong' });
1542
+ return;
1543
+ }
1544
+ else if (data.method === '/api/login') {
1545
+ if (!this.matterbridge.nodeContext) {
1546
+ this.log.error('Login nodeContext not found');
1547
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Internal error: nodeContext not found' });
1548
+ return;
1549
+ }
1550
+ const storedPassword = await this.matterbridge.nodeContext.get('password', '');
1551
+ if (storedPassword === '' || storedPassword === data.params.password) {
1552
+ this.log.debug('Login password valid');
1553
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1554
+ return;
1555
+ }
1556
+ else {
1557
+ this.log.debug('Error wrong password');
1558
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong password' });
1559
+ return;
1560
+ }
1561
+ }
1562
+ else if (data.method === '/api/install') {
1563
+ if (isValidString(data.params.packageName, 12) && isValidBoolean(data.params.restart)) {
1564
+ this.wssSendSnackbarMessage(`Installing package ${data.params.packageName}...`, 0);
1565
+ this.server.request({ type: 'plugins_install', src: this.server.name, dst: 'plugins', params: { packageName: data.params.packageName } });
1566
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1567
+ }
1568
+ else {
1569
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter in /api/install' });
1570
+ }
1571
+ }
1572
+ else if (data.method === '/api/uninstall') {
1573
+ if (isValidString(data.params.packageName, 12)) {
1574
+ this.wssSendSnackbarMessage(`Uninstalling package ${data.params.packageName}...`, 0);
1575
+ this.server.request({ type: 'plugins_uninstall', src: this.server.name, dst: 'plugins', params: { packageName: data.params.packageName } });
1576
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1577
+ }
1578
+ else {
1579
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter packageName in /api/uninstall' });
1580
+ }
1581
+ }
1582
+ else if (data.method === '/api/addplugin') {
1583
+ const localData = data;
1584
+ if (!isValidString(data.params.pluginNameOrPath, 10)) {
1585
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter pluginNameOrPath in /api/addplugin' });
1586
+ this.wssSendSnackbarMessage(`Plugin ${data.params.pluginNameOrPath} not added`, 10, 'error');
1587
+ return;
1588
+ }
1589
+ this.wssSendSnackbarMessage(`Adding plugin ${data.params.pluginNameOrPath}...`, 5);
1590
+ this.log.debug(`Adding plugin ${data.params.pluginNameOrPath}...`);
1591
+ data.params.pluginNameOrPath = data.params.pluginNameOrPath.replace(/@.*$/, ''); // Remove @version if present
1592
+ /*
1593
+ const plugin = (await this.server.fetch({ type: 'plugins_add', src: this.server.name, dst: 'plugins', params: { nameOrPath: data.params.pluginNameOrPath } }, 5000)).response.plugin;
1594
+ if (plugin) {
1595
+ this.wssSendSnackbarMessage(`Added plugin ${data.params.pluginNameOrPath}`, 5, 'success');
1596
+ await this.server.fetch({ type: 'plugins_load', src: this.server.name, dst: 'plugins', params: { plugin: plugin.name } }, 5000);
1597
+ this.wssSendRestartRequired();
1598
+ this.wssSendRefreshRequired('plugins');
1599
+ this.wssSendRefreshRequired('devices');
1600
+ this.wssSendSnackbarMessage(`Loaded plugin ${localData.params.pluginNameOrPath}`, 5, 'success');
1601
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1602
+ } else {
1603
+ this.wssSendSnackbarMessage(`Plugin ${data.params.pluginNameOrPath} not added`, 10, 'error');
1604
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: `Plugin ${data.params.pluginNameOrPath} not added` });
1605
+ }
1606
+ */
1607
+ const plugin = await this.matterbridge.plugins.add(data.params.pluginNameOrPath);
1608
+ if (plugin) {
1609
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1610
+ this.wssSendSnackbarMessage(`Added plugin ${data.params.pluginNameOrPath}`, 5, 'success');
1611
+ this.matterbridge.plugins
1612
+ .load(plugin)
1613
+ .then(() => {
1614
+ this.wssSendRefreshRequired('plugins');
1615
+ this.wssSendRefreshRequired('devices');
1616
+ this.wssSendSnackbarMessage(`Loaded plugin ${localData.params.pluginNameOrPath}`, 5, 'success');
1617
+ return;
1618
+ })
1619
+ .catch(/* istanbul ignore next */ (_error) => { });
1620
+ }
1621
+ else {
1622
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: `Plugin ${data.params.pluginNameOrPath} not added` });
1623
+ this.wssSendSnackbarMessage(`Plugin ${data.params.pluginNameOrPath} not added`, 10, 'error');
1624
+ }
1625
+ }
1626
+ else if (data.method === '/api/removeplugin') {
1627
+ if (!isValidString(data.params.pluginName, 10) || !this.matterbridge.plugins.has(data.params.pluginName)) {
1628
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter pluginName in /api/removeplugin' });
1629
+ return;
1630
+ }
1631
+ this.wssSendSnackbarMessage(`Removing plugin ${data.params.pluginName}...`, 5);
1632
+ this.log.debug(`Removing plugin ${data.params.pluginName}...`);
1633
+ /*
1634
+ await this.server.fetch({ type: 'plugins_shutdown', src: this.server.name, dst: 'plugins', params: { plugin: data.params.pluginName, reason: 'The plugin has been removed.', removeAllDevices: true } }, 5000);
1635
+ await this.server.fetch({ type: 'plugins_remove', src: this.server.name, dst: 'plugins', params: { nameOrPath: data.params.pluginName } }, 5000);
1636
+ */
1637
+ const plugin = this.matterbridge.plugins.get(data.params.pluginName);
1638
+ await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true);
1639
+ await this.matterbridge.plugins.remove(data.params.pluginName);
1640
+ this.wssSendSnackbarMessage(`Removed plugin ${data.params.pluginName}`, 5, 'success');
1641
+ this.wssSendRefreshRequired('plugins');
1642
+ this.wssSendRefreshRequired('devices');
1643
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1644
+ }
1645
+ else if (data.method === '/api/enableplugin') {
1646
+ const localData = data;
1647
+ if (!isValidString(data.params.pluginName, 10) || !this.matterbridge.plugins.has(data.params.pluginName)) {
1648
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter pluginName in /api/enableplugin' });
1649
+ return;
1650
+ }
1651
+ const plugin = this.matterbridge.plugins.get(data.params.pluginName);
1652
+ plugin.locked = undefined;
1653
+ plugin.error = undefined;
1654
+ plugin.loaded = undefined;
1655
+ plugin.started = undefined;
1656
+ plugin.configured = undefined;
1657
+ plugin.platform = undefined;
1658
+ plugin.registeredDevices = undefined;
1659
+ plugin.matter = undefined;
1660
+ await this.matterbridge.plugins.enable(data.params.pluginName);
1661
+ this.wssSendSnackbarMessage(`Enabled plugin ${data.params.pluginName}`, 5, 'success');
1662
+ setImmediate(async () => {
1663
+ await this.matterbridge.plugins.load(plugin, true, 'The plugin has been enabled', true);
1664
+ // @ts-expect-error Accessing private method
1665
+ if (plugin.serverNode)
1666
+ await this.matterbridge.startServerNode(plugin.serverNode);
1667
+ for (const device of this.matterbridge.devices.array().filter((d) => d.plugin === plugin.name && d.serverNode))
1668
+ // @ts-expect-error Accessing private method
1669
+ await this.matterbridge.startServerNode(device.serverNode);
1670
+ this.wssSendSnackbarMessage(`Started plugin ${localData.params.pluginName}`, 5, 'success');
1671
+ this.wssSendRefreshRequired('plugins');
1672
+ this.wssSendRefreshRequired('devices');
1673
+ sendResponse({ id: localData.id, method: localData.method, src: 'Matterbridge', dst: localData.src, success: true });
1674
+ });
1675
+ }
1676
+ else if (data.method === '/api/disableplugin') {
1677
+ if (!isValidString(data.params.pluginName, 10) || !this.matterbridge.plugins.has(data.params.pluginName)) {
1678
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter pluginName in /api/disableplugin' });
1679
+ return;
1680
+ }
1681
+ const plugin = this.matterbridge.plugins.get(data.params.pluginName);
1682
+ // Stop server nodes devices first
1683
+ for (const device of this.matterbridge.devices.array().filter((d) => d.plugin === plugin.name && d.serverNode)) {
1684
+ // @ts-expect-error Accessing private method
1685
+ await this.matterbridge.stopServerNode(device.serverNode);
1686
+ device.serverNode = undefined;
1687
+ }
1688
+ // Then shutdown plugin removing devices, disable it and stop plugin server node
1689
+ await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been disabled.', true);
1690
+ await this.matterbridge.plugins.disable(data.params.pluginName);
1691
+ // @ts-expect-error Accessing private method
1692
+ if (plugin.serverNode)
1693
+ await this.matterbridge.stopServerNode(plugin.serverNode);
1694
+ plugin.serverNode = undefined;
1695
+ this.wssSendSnackbarMessage(`Disabled plugin ${data.params.pluginName}`, 5, 'success');
1696
+ this.wssSendRefreshRequired('plugins');
1697
+ this.wssSendRefreshRequired('devices');
1698
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1699
+ }
1700
+ else if (data.method === '/api/restartplugin') {
1701
+ if (!isValidString(data.params.pluginName, 10) || !this.matterbridge.plugins.has(data.params.pluginName)) {
1702
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter pluginName in /api/restartplugin' });
1703
+ return;
1704
+ }
1705
+ this.wssSendSnackbarMessage(`Restarting plugin ${data.params.pluginName}`, 5, 'info');
1706
+ const plugin = this.matterbridge.plugins.get(data.params.pluginName);
1707
+ await this.matterbridge.plugins.shutdown(plugin, 'The plugin is restarting.', false, true);
1708
+ // Stop server nodes
1709
+ if (plugin.serverNode) {
1710
+ // @ts-expect-error Accessing private method
1711
+ await this.matterbridge.stopServerNode(plugin.serverNode);
1712
+ plugin.serverNode = undefined;
1713
+ }
1714
+ for (const device of this.matterbridge.devices.array().filter((d) => d.plugin === plugin.name)) {
1715
+ // @ts-expect-error Accessing private method
1716
+ if (device.serverNode)
1717
+ await this.matterbridge.stopServerNode(device.serverNode);
1718
+ device.serverNode = undefined;
1719
+ this.log.debug(`Removing device ${device.deviceName} from plugin ${plugin.name}`);
1720
+ this.matterbridge.devices.remove(device);
1721
+ }
1722
+ // @ts-expect-error Accessing private method
1723
+ if (plugin.type === 'DynamicPlatform' && !plugin.locked)
1724
+ await this.matterbridge.createDynamicPlugin(plugin);
1725
+ await this.matterbridge.plugins.load(plugin, true, 'The plugin has been restarted', true);
1726
+ plugin.restartRequired = false; // Reset plugin restartRequired
1727
+ let needRestart = 0;
1728
+ for (const plugin of this.matterbridge.plugins) {
1729
+ if (plugin.restartRequired)
1730
+ needRestart++;
1731
+ }
1732
+ if (needRestart === 0)
1733
+ this.wssSendRestartNotRequired(true); // Reset global restart required message
1734
+ // Start server nodes
1735
+ // @ts-expect-error Accessing private method
1736
+ if (plugin.serverNode)
1737
+ await this.matterbridge.startServerNode(plugin.serverNode);
1738
+ // @ts-expect-error Accessing private method
1739
+ for (const device of this.matterbridge.devices.array().filter((d) => d.plugin === plugin.name && d.serverNode))
1740
+ await this.matterbridge.startServerNode(device.serverNode);
1741
+ this.wssSendSnackbarMessage(`Restarted plugin ${data.params.pluginName}`, 5, 'success');
1742
+ this.wssSendRefreshRequired('plugins');
1743
+ this.wssSendRefreshRequired('devices');
1744
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1745
+ }
1746
+ else if (data.method === '/api/savepluginconfig') {
1747
+ if (!isValidString(data.params.pluginName, 10) || !this.matterbridge.plugins.has(data.params.pluginName)) {
1748
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter pluginName in /api/savepluginconfig' });
1749
+ return;
1750
+ }
1751
+ if (!isValidObject(data.params.formData, 5)) {
1752
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter formData in /api/savepluginconfig' });
1753
+ return;
1754
+ }
1755
+ this.log.info(`Saving config for plugin ${plg}${data.params.pluginName}${nf}...`);
1756
+ const plugin = this.matterbridge.plugins.get(data.params.pluginName);
1757
+ if (plugin) {
1758
+ this.matterbridge.plugins.saveConfigFromJson(plugin, data.params.formData, true);
1759
+ this.wssSendSnackbarMessage(`Saved config for plugin ${data.params.pluginName}`);
1760
+ this.wssSendRestartRequired();
1761
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1762
+ }
1763
+ }
1764
+ else if (data.method === '/api/checkupdates') {
1765
+ const { createESMWorker } = await import('@matterbridge/thread');
1766
+ createESMWorker('CheckUpdates', this.matterbridge.resolveWorkerDistFilePath('workerCheckUpdates.js'));
1767
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1768
+ }
1769
+ else if (data.method === '/api/shellysysupdate') {
1770
+ /*
1771
+ const { triggerShellySysUpdate } = await import('./shelly.js');
1772
+ triggerShellySysUpdate(this.matterbridge);
1773
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1774
+ */
1775
+ }
1776
+ else if (data.method === '/api/shellymainupdate') {
1777
+ /*
1778
+ const { triggerShellyMainUpdate } = await import('./shelly.js');
1779
+ triggerShellyMainUpdate(this.matterbridge);
1780
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1781
+ */
1782
+ }
1783
+ else if (data.method === '/api/shellycreatesystemlog') {
1784
+ /*
1785
+ const { createShellySystemLog } = await import('./shelly.js');
1786
+ createShellySystemLog(this.matterbridge);
1787
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1788
+ */
1789
+ }
1790
+ else if (data.method === '/api/shellynetconfig') {
1791
+ /*
1792
+ this.log.debug('/api/shellynetconfig:', data.params);
1793
+ const { triggerShellyChangeIp } = await import('./shelly.js');
1794
+ triggerShellyChangeIp(this.matterbridge, data.params as { type: 'static' | 'dhcp'; ip: string; subnet: string; gateway: string; dns: string });
1795
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1796
+ */
1797
+ }
1798
+ else if (data.method === '/api/softreset') {
1799
+ /*
1800
+ const { triggerShellySoftReset } = await import('./shelly.js');
1801
+ triggerShellySoftReset(this.matterbridge);
1802
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1803
+ */
1804
+ }
1805
+ else if (data.method === '/api/hardreset') {
1806
+ /*
1807
+ const { triggerShellyHardReset } = await import('./shelly.js');
1808
+ triggerShellyHardReset(this.matterbridge);
1809
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1810
+ */
1811
+ }
1812
+ else if (data.method === '/api/reboot') {
1813
+ /*
1814
+ const { triggerShellyReboot } = await import('./shelly.js');
1815
+ triggerShellyReboot(this.matterbridge);
1816
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1817
+ */
1818
+ }
1819
+ else if (data.method === '/api/restart') {
1820
+ this.wssSendSnackbarMessage(`Restarting matterbridge...`, 0);
1821
+ await this.matterbridge.restartProcess();
1822
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1823
+ }
1824
+ else if (data.method === '/api/shutdown') {
1825
+ this.wssSendSnackbarMessage(`Shutting down matterbridge...`, 0);
1826
+ await this.matterbridge.shutdownProcess();
1827
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1828
+ }
1829
+ else if (data.method === '/api/create-backup') {
1830
+ this.wssSendSnackbarMessage('Creating backup...', 0);
1831
+ this.log.notice(`Creating the backup...`);
1832
+ await createZip(path.join(os.tmpdir(), `matterbridge.backup.zip`), path.join(this.matterbridge.matterbridgeDirectory), path.join(this.matterbridge.matterbridgePluginDirectory), path.join(this.matterbridge.matterbridgeCertDirectory));
1833
+ this.log.notice(`Backup ready to be downloaded.`);
1834
+ this.wssSendCloseSnackbarMessage('Creating backup...');
1835
+ this.wssSendSnackbarMessage('Backup ready to be downloaded', 10);
1836
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1837
+ }
1838
+ else if (data.method === '/api/unregister') {
1839
+ this.wssSendSnackbarMessage('Unregistering all bridged devices...', 0);
1840
+ await this.matterbridge.unregisterAndShutdownProcess();
1841
+ this.wssSendCloseSnackbarMessage('Unregistering all bridged devices...');
1842
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1843
+ }
1844
+ else if (data.method === '/api/reset') {
1845
+ this.wssSendSnackbarMessage('Resetting matterbridge commissioning...', 10);
1846
+ await this.matterbridge.shutdownProcessAndReset();
1847
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1848
+ }
1849
+ else if (data.method === '/api/factoryreset') {
1850
+ this.wssSendSnackbarMessage('Factory reset of matterbridge...', 10);
1851
+ await this.matterbridge.shutdownProcessAndFactoryReset();
1852
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1853
+ }
1854
+ else if (data.method === '/api/viewhistorypage') {
1855
+ generateHistoryPage({ outputPath: path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_HISTORY_FILE), pageTitle: `Matterbridge Cpu & Memory History` });
1856
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1857
+ }
1858
+ else if (data.method === '/api/downloadhistorypage') {
1859
+ generateHistoryPage({ outputPath: path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_HISTORY_FILE), pageTitle: `Matterbridge Cpu & Memory History` });
1860
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1861
+ }
1862
+ else if (data.method === '/api/matter') {
1863
+ const localData = data;
1864
+ if (!isValidString(data.params.id)) {
1865
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter id in /api/matter' });
1866
+ return;
1867
+ }
1868
+ let serverNode;
1869
+ if (data.params.id === 'Matterbridge')
1870
+ serverNode = this.matterbridge.serverNode;
1871
+ else
1872
+ serverNode =
1873
+ this.matterbridge.plugins.array().find((p) => p.serverNode && p.serverNode.id === localData.params.id)?.serverNode ||
1874
+ this.matterbridge.devices.array().find((d) => d.serverNode && d.serverNode.id === localData.params.id)?.serverNode;
1875
+ if (!serverNode) {
1876
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: `Unknown server node id ${localData.params.id} in /api/matter` });
1877
+ return;
1878
+ }
1879
+ const matter = this.matterbridge.getServerNodeData(serverNode);
1880
+ this.log.debug(`*Server node ${serverNode.id}: commissioned ${serverNode.state.commissioning.commissioned} upTime ${serverNode.state.generalDiagnostics.upTime}.`);
1881
+ if (data.params.server) {
1882
+ this.log.debug(`*Sending data for node ${data.params.id}`);
1883
+ this.wssSendRefreshRequired('matter', { matter: { ...matter } });
1884
+ }
1885
+ if (data.params.startCommission) {
1886
+ await serverNode.env.get(DeviceCommissioner)?.allowBasicCommissioning();
1887
+ this.matterbridge.advertisingNodes.set(serverNode.id, Date.now());
1888
+ this.log.debug(`*Allow commissioning has been sent for node ${data.params.id}`);
1889
+ this.wssSendRefreshRequired('matter', { matter: { ...matter, advertiseTime: Date.now(), advertising: true } });
1890
+ }
1891
+ if (data.params.stopCommission) {
1892
+ await serverNode.env.get(DeviceCommissioner)?.endCommissioning();
1893
+ this.matterbridge.advertisingNodes.delete(serverNode.id);
1894
+ this.log.debug(`*End commissioning has been sent for node ${data.params.id}`);
1895
+ this.wssSendRefreshRequired('matter', { matter: { ...matter, advertiseTime: 0, advertising: false } });
1896
+ }
1897
+ if (data.params.advertise) {
1898
+ serverNode.env.get(DeviceAdvertiser)?.restartAdvertisement();
1899
+ this.log.debug(`*Advertising has been sent for node ${data.params.id}`);
1900
+ this.wssSendRefreshRequired('matter', { matter: { ...matter, advertising: true } });
1901
+ }
1902
+ if (data.params.removeFabric) {
1903
+ const fabricIndex = FabricIndex(data.params.removeFabric);
1904
+ const fabricManager = serverNode.env.get(FabricManager);
1905
+ if (fabricManager.has(fabricIndex))
1906
+ await fabricManager.for(fabricIndex).leave();
1907
+ this.log.debug(`*Removed fabric index ${data.params.removeFabric} for node ${data.params.id}`);
1908
+ this.wssSendRefreshRequired('matter', { matter: { ...matter } });
1909
+ }
1910
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: matter });
1911
+ }
1912
+ else if (data.method === '/api/settings') {
1913
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: await this.getApiSettings() });
1914
+ }
1915
+ else if (data.method === '/api/plugins') {
1916
+ const plugins = this.matterbridge.hasCleanupStarted ? [] : this.getPlugins();
1917
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: plugins });
1918
+ }
1919
+ else if (data.method === '/api/devices') {
1920
+ const devices = this.matterbridge.hasCleanupStarted ? [] : this.getDevices(isValidString(data.params.pluginName) ? data.params.pluginName : undefined);
1921
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: devices });
1922
+ }
1923
+ else if (data.method === '/api/clusters') {
1924
+ if (!isValidString(data.params.plugin, 10)) {
1925
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter plugin in /api/clusters' });
1926
+ return;
1927
+ }
1928
+ if (!isValidNumber(data.params.endpoint, 1)) {
1929
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter endpoint in /api/clusters' });
1930
+ return;
1931
+ }
1932
+ const response = this.getClusters(data.params.plugin, data.params.endpoint);
1933
+ if (response) {
1934
+ sendResponse({
1935
+ id: data.id,
1936
+ method: data.method,
1937
+ src: 'Matterbridge',
1938
+ dst: data.src,
1939
+ success: true,
1940
+ response,
1941
+ });
1942
+ }
1943
+ else {
1944
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Endpoint not found in /api/clusters' });
1945
+ }
1946
+ }
1947
+ else if (data.method === '/api/select/devices') {
1948
+ if (!isValidString(data.params.plugin, 10)) {
1949
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter plugin in /api/select/devices' });
1950
+ return;
1951
+ }
1952
+ const plugin = this.matterbridge.plugins.get(data.params.plugin);
1953
+ if (!plugin) {
1954
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Plugin not found in /api/select/devices' });
1955
+ return;
1956
+ }
1957
+ // istanbul ignore next
1958
+ const selectDeviceValues = !plugin.platform ? [] : plugin.platform.getSelectDevices().sort((keyA, keyB) => keyA.name.localeCompare(keyB.name));
1959
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: selectDeviceValues });
1960
+ }
1961
+ else if (data.method === '/api/select/entities') {
1962
+ if (!isValidString(data.params.plugin, 10)) {
1963
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter plugin in /api/select/entities' });
1964
+ return;
1965
+ }
1966
+ const plugin = this.matterbridge.plugins.get(data.params.plugin);
1967
+ if (!plugin) {
1968
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Plugin not found in /api/select/entities' });
1969
+ return;
1970
+ }
1971
+ // istanbul ignore next
1972
+ const selectEntityValues = !plugin.platform ? [] : plugin.platform.getSelectEntities().sort((keyA, keyB) => keyA.name.localeCompare(keyB.name));
1973
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: selectEntityValues });
1974
+ }
1975
+ else if (data.method === '/api/action') {
1976
+ const localData = data;
1977
+ if (!isValidString(data.params.plugin, 5) || !isValidString(data.params.action, 1)) {
1978
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter in /api/action' });
1979
+ return;
1980
+ }
1981
+ const plugin = this.matterbridge.plugins.get(data.params.plugin);
1982
+ if (!plugin) {
1983
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Plugin not found in /api/action' });
1984
+ return;
1985
+ }
1986
+ this.log.notice(`Action ${CYAN}${data.params.action}${nt}${data.params.value ? ' with ' + CYAN + data.params.value + nt : ''} for plugin ${CYAN}${plugin.name}${nt}`);
1987
+ plugin.platform
1988
+ ?.onAction(data.params.action, data.params.value, data.params.id, data.params.formData)
1989
+ .then(() => {
1990
+ sendResponse({ id: localData.id, method: localData.method, src: 'Matterbridge', dst: localData.src, success: true });
1991
+ return;
1992
+ })
1993
+ .catch((error) => {
1994
+ this.log.error(`Error in plugin ${plugin.name} action ${localData.params.action}: ${error}`);
1995
+ sendResponse({
1996
+ id: data.id,
1997
+ method: data.method,
1998
+ src: 'Matterbridge',
1999
+ dst: data.src,
2000
+ error: `Error in plugin ${plugin.name} action ${localData.params.action}: ${error}`,
2001
+ });
2002
+ });
2003
+ }
2004
+ else if (data.method === '/api/config') {
2005
+ if (!isValidString(data.params.name, 5) || data.params.value === undefined) {
2006
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter in /api/config' });
2007
+ return;
2008
+ }
2009
+ this.log.debug(`Received /api/config name ${CYAN}${data.params.name}${db} value ${CYAN}${data.params.value}${db}`);
2010
+ switch (data.params.name) {
2011
+ case 'setpassword':
2012
+ if (isValidString(data.params.value)) {
2013
+ await this.matterbridge.nodeContext?.set('password', data.params.value);
2014
+ this.storedPassword = data.params.value;
2015
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
2016
+ }
2017
+ break;
2018
+ case 'setbridgemode':
2019
+ if (isValidString(data.params.value) && ['bridge', 'childbridge'].includes(data.params.value)) {
2020
+ await this.matterbridge.nodeContext?.set('bridgeMode', data.params.value);
2021
+ this.wssSendRestartRequired();
2022
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
2023
+ }
2024
+ break;
2025
+ case 'setmbloglevel':
2026
+ if (isValidString(data.params.value, 4)) {
2027
+ this.log.debug('Matterbridge logger level:', data.params.value);
2028
+ if (data.params.value === 'Debug') {
2029
+ await this.matterbridge.setLogLevel("debug" /* LogLevel.DEBUG */);
2030
+ }
2031
+ else if (data.params.value === 'Info') {
2032
+ await this.matterbridge.setLogLevel("info" /* LogLevel.INFO */);
2033
+ }
2034
+ else if (data.params.value === 'Notice') {
2035
+ await this.matterbridge.setLogLevel("notice" /* LogLevel.NOTICE */);
2036
+ }
2037
+ else if (data.params.value === 'Warn') {
2038
+ await this.matterbridge.setLogLevel("warn" /* LogLevel.WARN */);
2039
+ }
2040
+ else if (data.params.value === 'Error') {
2041
+ await this.matterbridge.setLogLevel("error" /* LogLevel.ERROR */);
2042
+ }
2043
+ else if (data.params.value === 'Fatal') {
2044
+ await this.matterbridge.setLogLevel("fatal" /* LogLevel.FATAL */);
2045
+ }
2046
+ await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
2047
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
2048
+ }
2049
+ break;
2050
+ case 'setmblogfile':
2051
+ if (isValidBoolean(data.params.value)) {
2052
+ this.log.debug('Matterbridge file log:', data.params.value);
2053
+ this.matterbridge.fileLogger = data.params.value;
2054
+ await this.matterbridge.nodeContext?.set('matterbridgeFileLog', data.params.value);
2055
+ // Create the file logger for matterbridge
2056
+ if (data.params.value)
2057
+ AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE), await this.matterbridge.getLogLevel(), true);
2058
+ else
2059
+ AnsiLogger.setGlobalLogfile(undefined);
2060
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
2061
+ }
2062
+ break;
2063
+ case 'setmjloglevel':
2064
+ if (isValidString(data.params.value, 4)) {
2065
+ this.log.debug('Matter logger level:', data.params.value);
2066
+ if (data.params.value === 'Debug') {
2067
+ Logger.level = MatterLogLevel.DEBUG;
2068
+ }
2069
+ else if (data.params.value === 'Info') {
2070
+ Logger.level = MatterLogLevel.INFO;
2071
+ }
2072
+ else if (data.params.value === 'Notice') {
2073
+ Logger.level = MatterLogLevel.NOTICE;
2074
+ }
2075
+ else if (data.params.value === 'Warn') {
2076
+ Logger.level = MatterLogLevel.WARN;
2077
+ }
2078
+ else if (data.params.value === 'Error') {
2079
+ Logger.level = MatterLogLevel.ERROR;
2080
+ }
2081
+ else if (data.params.value === 'Fatal') {
2082
+ Logger.level = MatterLogLevel.FATAL;
2083
+ }
2084
+ this.matterbridge.matterLogLevel = MatterLogLevel.names[Logger.level];
2085
+ // Set the global logger callback for the WebSocketServer to the common minimum logLevel
2086
+ let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
2087
+ if (this.matterbridge.getLogLevel() === "info" /* LogLevel.INFO */ || Logger.level === MatterLogLevel.INFO)
2088
+ callbackLogLevel = "info" /* LogLevel.INFO */;
2089
+ if (this.matterbridge.getLogLevel() === "debug" /* LogLevel.DEBUG */ || Logger.level === MatterLogLevel.DEBUG)
2090
+ callbackLogLevel = "debug" /* LogLevel.DEBUG */;
2091
+ AnsiLogger.setGlobalCallbackLevel(callbackLogLevel);
2092
+ this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
2093
+ await this.matterbridge.nodeContext?.set('matterLogLevel', Logger.level);
2094
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
2095
+ }
2096
+ break;
2097
+ case 'setmjlogfile':
2098
+ if (isValidBoolean(data.params.value)) {
2099
+ this.log.debug('Matter file log:', data.params.value);
2100
+ this.matterbridge.matterFileLogger = data.params.value;
2101
+ await this.matterbridge.nodeContext?.set('matterFileLog', data.params.value);
2102
+ if (data.params.value) {
2103
+ this.matterbridge.matterLog.logFilePath = path.join(this.matterbridge.matterbridgeDirectory, MATTER_LOGGER_FILE);
2104
+ }
2105
+ else {
2106
+ this.matterbridge.matterLog.logFilePath = undefined;
2107
+ }
2108
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
2109
+ }
2110
+ break;
2111
+ case 'setmdnsinterface':
2112
+ if (isValidString(data.params.value)) {
2113
+ this.log.debug(`Matter.js mdns interface: ${data.params.value === '' ? 'all interfaces' : data.params.value}`);
2114
+ this.matterbridge.mdnsInterface = data.params.value === '' ? undefined : data.params.value;
2115
+ await this.matterbridge.nodeContext?.set('mattermdnsinterface', data.params.value);
2116
+ this.wssSendRestartRequired();
2117
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
2118
+ this.wssSendSnackbarMessage(`Mdns interface changed to ${data.params.value === '' ? 'all interfaces' : data.params.value}`);
2119
+ }
2120
+ break;
2121
+ case 'setipv4address':
2122
+ if (isValidString(data.params.value)) {
2123
+ this.log.debug(`Matter.js ipv4 address: ${data.params.value === '' ? 'all ipv4 addresses' : data.params.value}`);
2124
+ this.matterbridge.ipv4Address = data.params.value === '' ? undefined : data.params.value;
2125
+ await this.matterbridge.nodeContext?.set('matteripv4address', data.params.value);
2126
+ this.wssSendRestartRequired();
2127
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
2128
+ this.wssSendSnackbarMessage(`IPv4 address changed to ${data.params.value === '' ? 'all ipv4 addresses' : data.params.value}`);
2129
+ }
2130
+ break;
2131
+ case 'setipv6address':
2132
+ if (isValidString(data.params.value)) {
2133
+ this.log.debug(`Matter.js ipv6 address: ${data.params.value === '' ? 'all ipv6 addresses' : data.params.value}`);
2134
+ this.matterbridge.ipv6Address = data.params.value === '' ? undefined : data.params.value;
2135
+ await this.matterbridge.nodeContext?.set('matteripv6address', data.params.value);
2136
+ this.wssSendRestartRequired();
2137
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
2138
+ this.wssSendSnackbarMessage(`IPv6 address changed to ${data.params.value === '' ? 'all ipv6 addresses' : data.params.value}`);
2139
+ }
2140
+ break;
2141
+ case 'setmatterport':
2142
+ // eslint-disable-next-line no-case-declarations
2143
+ const port = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
2144
+ if (isValidNumber(port, 5540, 5600)) {
2145
+ this.log.debug(`Set matter commissioning port to ${CYAN}${port}${db}`);
2146
+ this.matterbridge.port = port;
2147
+ await this.matterbridge.nodeContext?.set('matterport', port);
2148
+ this.wssSendRestartRequired();
2149
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
2150
+ this.wssSendSnackbarMessage(`Matter port changed to ${port}`);
2151
+ }
2152
+ else {
2153
+ this.log.debug(`Reset matter commissioning port to ${CYAN}5540${db}`);
2154
+ this.matterbridge.port = 5540;
2155
+ await this.matterbridge.nodeContext?.set('matterport', 5540);
2156
+ this.wssSendRestartRequired();
2157
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Invalid value: reset matter commissioning port to default 5540' });
2158
+ this.wssSendSnackbarMessage(`Matter port reset to default 5540`, undefined, 'error');
2159
+ }
2160
+ break;
2161
+ case 'setmatterdiscriminator':
2162
+ // eslint-disable-next-line no-case-declarations
2163
+ const discriminator = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
2164
+ if (isValidNumber(discriminator, 0, 4095)) {
2165
+ this.log.debug(`Set matter commissioning discriminator to ${CYAN}${discriminator}${db}`);
2166
+ this.matterbridge.discriminator = discriminator;
2167
+ await this.matterbridge.nodeContext?.set('matterdiscriminator', discriminator);
2168
+ this.wssSendRestartRequired();
2169
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
2170
+ this.wssSendSnackbarMessage(`Matter discriminator changed to ${discriminator}`);
2171
+ }
2172
+ else {
2173
+ this.log.debug(`Reset matter commissioning discriminator to ${CYAN}undefined${db}`);
2174
+ this.matterbridge.discriminator = undefined;
2175
+ await this.matterbridge.nodeContext?.remove('matterdiscriminator');
2176
+ this.wssSendRestartRequired();
2177
+ sendResponse({
2178
+ id: data.id,
2179
+ method: data.method,
2180
+ src: 'Matterbridge',
2181
+ dst: data.src,
2182
+ error: 'Invalid value: reset matter commissioning discriminator to default undefined',
2183
+ });
2184
+ this.wssSendSnackbarMessage(`Matter discriminator reset to default`, undefined, 'error');
2185
+ }
2186
+ break;
2187
+ case 'setmatterpasscode':
2188
+ // eslint-disable-next-line no-case-declarations
2189
+ const passcode = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
2190
+ if (isValidNumber(passcode, 1, 99999998) && CommissioningOptions.FORBIDDEN_PASSCODES.includes(passcode) === false) {
2191
+ this.matterbridge.passcode = passcode;
2192
+ this.log.debug(`Set matter commissioning passcode to ${CYAN}${passcode}${db}`);
2193
+ await this.matterbridge.nodeContext?.set('matterpasscode', passcode);
2194
+ this.wssSendRestartRequired();
2195
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
2196
+ this.wssSendSnackbarMessage(`Matter passcode changed to ${passcode}`);
2197
+ }
2198
+ else {
2199
+ this.log.debug(`Reset matter commissioning passcode to ${CYAN}undefined${db}`);
2200
+ this.matterbridge.passcode = undefined;
2201
+ await this.matterbridge.nodeContext?.remove('matterpasscode');
2202
+ this.wssSendRestartRequired();
2203
+ sendResponse({
2204
+ id: data.id,
2205
+ method: data.method,
2206
+ src: 'Matterbridge',
2207
+ dst: data.src,
2208
+ error: 'Invalid value: reset matter commissioning passcode to default undefined',
2209
+ });
2210
+ this.wssSendSnackbarMessage(`Matter passcode reset to default`, undefined, 'error');
2211
+ }
2212
+ break;
2213
+ case 'setvirtualmode':
2214
+ if (isValidString(data.params.value, 1) && ['disabled', 'light', 'outlet', 'switch', 'mounted_switch'].includes(data.params.value)) {
2215
+ this.matterbridge.virtualMode = data.params.value;
2216
+ this.log.debug(`Set matterbridge virtual mode to ${CYAN}${data.params.value}${db}`);
2217
+ await this.matterbridge.nodeContext?.set('virtualmode', data.params.value);
2218
+ this.wssSendRestartRequired();
2219
+ }
2220
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
2221
+ break;
2222
+ default:
2223
+ this.log.warn(`Unknown parameter ${data.params.name} in /api/config`);
2224
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: `Unknown parameter ${data.params.name} in /api/config` });
2225
+ }
2226
+ }
2227
+ else if (data.method === '/api/command') {
2228
+ const localData = data;
2229
+ if (!isValidString(data.params.command, 5)) {
2230
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter command in /api/command' });
2231
+ return;
2232
+ }
2233
+ if (data.params.command === 'selectdevice' && isValidString(data.params.plugin, 10) && isValidString(data.params.serial, 1) && isValidString(data.params.name, 1)) {
2234
+ const plugin = this.matterbridge.plugins.get(data.params.plugin);
2235
+ if (!plugin) {
2236
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Plugin not found in /api/command' });
2237
+ return;
2238
+ }
2239
+ const config = plugin.configJson;
2240
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2241
+ const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
2242
+ // this.log.debug(`SelectDevice(selectMode ${select}) data ${debugStringify(data)}`);
2243
+ if (select === 'serial')
2244
+ this.log.info(`Selected device serial ${data.params.serial}`);
2245
+ if (select === 'name')
2246
+ this.log.info(`Selected device name ${data.params.name}`);
2247
+ if (config && select && (select === 'serial' || select === 'name')) {
2248
+ // Remove postfix from the serial if it exists
2249
+ if (config.postfix) {
2250
+ data.params.serial = data.params.serial.replace('-' + config.postfix, '');
2251
+ }
2252
+ // Add the serial to the whiteList if the whiteList exists and the serial or name is not already in it
2253
+ if (isValidArray(config.whiteList, 1)) {
2254
+ if (select === 'serial' && !config.whiteList.includes(data.params.serial)) {
2255
+ config.whiteList.push(data.params.serial);
2256
+ }
2257
+ else if (select === 'name' && !config.whiteList.includes(data.params.name)) {
2258
+ config.whiteList.push(data.params.name);
2259
+ }
2260
+ }
2261
+ // Remove the serial from the blackList if the blackList exists and the serial or name is in it
2262
+ if (isValidArray(config.blackList, 1)) {
2263
+ if (select === 'serial' && config.blackList.includes(data.params.serial)) {
2264
+ config.blackList = config.blackList.filter((item) => item !== localData.params.serial);
2265
+ }
2266
+ else if (select === 'name' && config.blackList.includes(data.params.name)) {
2267
+ config.blackList = config.blackList.filter((item) => item !== localData.params.name);
2268
+ }
2269
+ }
2270
+ if (plugin.platform)
2271
+ plugin.platform.config = config;
2272
+ plugin.configJson = config;
2273
+ await this.matterbridge.plugins.saveConfigFromPlugin(plugin, true);
2274
+ this.wssSendRestartRequired(false);
2275
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
2276
+ }
2277
+ else {
2278
+ this.log.error(`SelectDevice: select ${select} not supported`);
2279
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: `SelectDevice: select ${select} not supported` });
2280
+ }
2281
+ }
2282
+ else if (data.params.command === 'unselectdevice' &&
2283
+ isValidString(data.params.plugin, 10) &&
2284
+ isValidString(data.params.serial, 1) &&
2285
+ isValidString(data.params.name, 1)) {
2286
+ const plugin = this.matterbridge.plugins.get(data.params.plugin);
2287
+ if (!plugin) {
2288
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Plugin not found in /api/command' });
2289
+ return;
2290
+ }
2291
+ const config = plugin.configJson;
2292
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2293
+ const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
2294
+ // this.log.debug(`UnselectDevice(selectMode ${select}) data ${debugStringify(data)}`);
2295
+ if (select === 'serial')
2296
+ this.log.info(`Unselected device serial ${data.params.serial}`);
2297
+ if (select === 'name')
2298
+ this.log.info(`Unselected device name ${data.params.name}`);
2299
+ if (config && select && (select === 'serial' || select === 'name')) {
2300
+ if (config.postfix) {
2301
+ data.params.serial = data.params.serial.replace('-' + config.postfix, '');
2302
+ }
2303
+ // Remove the serial from the whiteList if the whiteList exists and the serial is in it
2304
+ if (isValidArray(config.whiteList, 1)) {
2305
+ if (select === 'serial' && config.whiteList.includes(data.params.serial)) {
2306
+ config.whiteList = config.whiteList.filter((item) => item !== localData.params.serial);
2307
+ }
2308
+ else if (select === 'name' && config.whiteList.includes(data.params.name)) {
2309
+ config.whiteList = config.whiteList.filter((item) => item !== localData.params.name);
2310
+ }
2311
+ }
2312
+ // Add the serial to the blackList
2313
+ if (isValidArray(config.blackList)) {
2314
+ if (select === 'serial' && !config.blackList.includes(data.params.serial)) {
2315
+ config.blackList.push(data.params.serial);
2316
+ }
2317
+ else if (select === 'name' && !config.blackList.includes(data.params.name)) {
2318
+ config.blackList.push(data.params.name);
2319
+ }
2320
+ }
2321
+ if (plugin.platform)
2322
+ plugin.platform.config = config;
2323
+ plugin.configJson = config;
2324
+ await this.matterbridge.plugins.saveConfigFromPlugin(plugin, true);
2325
+ this.wssSendRestartRequired(false);
2326
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
2327
+ }
2328
+ else {
2329
+ this.log.error(`SelectDevice: select ${select} not supported`);
2330
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: `SelectDevice: select ${select} not supported` });
2331
+ }
2332
+ }
2333
+ }
2334
+ else {
2335
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2336
+ const localData = data;
2337
+ this.log.error(`Invalid method from websocket client: ${debugStringify(localData)}`);
2338
+ sendResponse({ id: localData.id, method: localData.method, src: 'Matterbridge', dst: localData.src, error: 'Invalid method' });
2339
+ }
2340
+ }
2341
+ catch (error) {
2342
+ inspectError(this.log, `Error processing message "${message}" from websocket client`, error);
2343
+ }
2344
+ }
2345
+ /**
2346
+ * Sends a WebSocket log message to all connected clients. The function is called by AnsiLogger.setGlobalCallback.
2347
+ *
2348
+ * @param {string} level - The logger level of the message: debug info notice warn error fatal...
2349
+ * @param {string} time - The time string of the message
2350
+ * @param {string} name - The logger name of the message
2351
+ * @param {string} message - The content of the message.
2352
+ *
2353
+ * @remarks
2354
+ * The function removes ANSI escape codes, leading asterisks, non-printable characters, and replaces all occurrences of \t and \n.
2355
+ * It also replaces all occurrences of \" with " and angle-brackets with &lt; and &gt;.
2356
+ * The function sends the message to all connected clients.
2357
+ */
2358
+ wssSendLogMessage(level, time, name, message) {
2359
+ if (!this.listening || this.webSocketServer?.clients.size === 0)
2360
+ return;
2361
+ if (!level || !time || !name || !message)
2362
+ return;
2363
+ // Remove ANSI escape codes from the message
2364
+ // eslint-disable-next-line no-control-regex
2365
+ message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
2366
+ // Remove leading asterisks from the message
2367
+ message = message.replace(/^\*+/, '');
2368
+ // Replace all occurrences of \t and \n
2369
+ message = message.replace(/[\t\n]/g, '');
2370
+ // Remove non-printable characters
2371
+ // eslint-disable-next-line no-control-regex
2372
+ message = message.replace(/[\x00-\x1F\x7F]/g, '');
2373
+ // Replace all occurrences of \" with "
2374
+ message = message.replace(/\\"/g, '"');
2375
+ // Define the maximum allowed length for continuous characters without a space
2376
+ const maxContinuousLength = 100;
2377
+ const keepStartLength = 20;
2378
+ const keepEndLength = 20;
2379
+ // Split the message into words
2380
+ if (level !== 'spawn') {
2381
+ message = message
2382
+ .split(' ')
2383
+ .map((word) => {
2384
+ // If the word length exceeds the max continuous length, insert spaces and truncate
2385
+ if (word.length > maxContinuousLength) {
2386
+ return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
2387
+ }
2388
+ return word;
2389
+ })
2390
+ .join(' ');
2391
+ }
2392
+ // Send the message to all connected clients
2393
+ this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'log', success: true, response: { level, time, name, message } });
2394
+ }
2395
+ /**
2396
+ * Sends a need to refresh WebSocket message to all connected clients.
2397
+ *
2398
+ * @param {string} changed - The changed value.
2399
+ * @param {Record<string, unknown>} params - Additional parameters to send with the message.
2400
+ * possible values for changed:
2401
+ * - 'settings' (when the bridge has started in bridge mode or childbridge mode and when update finds a new version)
2402
+ * - 'plugins'
2403
+ * - 'devices'
2404
+ * - 'matter' with param 'matter' (QRDiv component)
2405
+ * @param {ApiMatter} params.matter - The matter device that has changed. Required if changed is 'matter'.
2406
+ */
2407
+ wssSendRefreshRequired(changed, params) {
2408
+ if (!this.listening || this.webSocketServer?.clients.size === 0)
2409
+ return;
2410
+ this.log.debug('Sending a refresh required message to all connected clients');
2411
+ // Send the message to all connected clients
2412
+ this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', success: true, response: { changed, ...params } });
2413
+ }
2414
+ /**
2415
+ * Sends a need to restart WebSocket message to all connected clients.
2416
+ *
2417
+ * @param {boolean} snackbar - If true, a snackbar message will be sent to all connected clients. Default is true.
2418
+ * @param {boolean} fixed - If true, the restart is fixed and will not be reset by plugin restarts. Default is false.
2419
+ */
2420
+ wssSendRestartRequired(snackbar = true, fixed = false) {
2421
+ if (!this.listening || this.webSocketServer?.clients.size === 0)
2422
+ return;
2423
+ this.log.debug('Sending a restart required message to all connected clients');
2424
+ this.matterbridge.restartRequired = true;
2425
+ this.matterbridge.fixedRestartRequired = fixed;
2426
+ if (snackbar === true)
2427
+ this.wssSendSnackbarMessage(`Restart required`, 0);
2428
+ // Send the message to all connected clients
2429
+ this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', success: true, response: { fixed } });
2430
+ }
2431
+ /**
2432
+ * Sends a no need to restart WebSocket message to all connected clients.
2433
+ *
2434
+ * @param {boolean} snackbar - If true, the snackbar message will be cleared from all connected clients. Default is true.
2435
+ */
2436
+ wssSendRestartNotRequired(snackbar = true) {
2437
+ if (!this.listening || this.webSocketServer?.clients.size === 0)
2438
+ return;
2439
+ this.log.debug('Sending a restart not required message to all connected clients');
2440
+ this.matterbridge.restartRequired = false;
2441
+ if (snackbar === true)
2442
+ this.wssSendCloseSnackbarMessage(`Restart required`);
2443
+ // Send the message to all connected clients
2444
+ this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_not_required', success: true });
2445
+ }
2446
+ /**
2447
+ * Sends a need to update WebSocket message to all connected clients.
2448
+ *
2449
+ * @param {boolean} devVersion - If true, the update is for a development version. Default is false.
2450
+ */
2451
+ wssSendUpdateRequired(devVersion = false) {
2452
+ if (!this.listening || this.webSocketServer?.clients.size === 0)
2453
+ return;
2454
+ this.log.debug('Sending an update required message to all connected clients');
2455
+ this.matterbridge.updateRequired = true;
2456
+ // Send the message to all connected clients
2457
+ this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'update_required', success: true, response: { devVersion } });
2458
+ }
2459
+ /**
2460
+ * Sends a cpu update message to all connected clients.
2461
+ *
2462
+ * @param {number} cpuUsage - The CPU usage percentage to send.
2463
+ * @param {number} processCpuUsage - The CPU usage percentage of the process to send.
2464
+ */
2465
+ wssSendCpuUpdate(cpuUsage, processCpuUsage) {
2466
+ if (!this.listening || this.webSocketServer?.clients.size === 0)
2467
+ return;
2468
+ // istanbul ignore else
2469
+ if (hasParameter('debug'))
2470
+ this.log.debug('Sending a cpu update message to all connected clients');
2471
+ // Send the message to all connected clients
2472
+ this.wssBroadcastMessage({
2473
+ id: 0,
2474
+ src: 'Matterbridge',
2475
+ dst: 'Frontend',
2476
+ method: 'cpu_update',
2477
+ success: true,
2478
+ response: { cpuUsage: Math.round(cpuUsage * 100) / 100, processCpuUsage: Math.round(processCpuUsage * 100) / 100 },
2479
+ });
2480
+ }
2481
+ /**
2482
+ * Sends a memory update message to all connected clients.
2483
+ *
2484
+ * @param {string} totalMemory - The total memory in bytes.
2485
+ * @param {string} freeMemory - The free memory in bytes.
2486
+ * @param {string} rss - The resident set size in bytes.
2487
+ * @param {string} heapTotal - The total heap memory in bytes.
2488
+ * @param {string} heapUsed - The used heap memory in bytes.
2489
+ * @param {string} external - The external memory in bytes.
2490
+ * @param {string} arrayBuffers - The array buffers memory in bytes.
2491
+ */
2492
+ wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
2493
+ if (!this.listening || this.webSocketServer?.clients.size === 0)
2494
+ return;
2495
+ // istanbul ignore else
2496
+ if (hasParameter('debug'))
2497
+ this.log.debug('Sending a memory update message to all connected clients');
2498
+ // Send the message to all connected clients
2499
+ this.wssBroadcastMessage({
2500
+ id: 0,
2501
+ src: 'Matterbridge',
2502
+ dst: 'Frontend',
2503
+ method: 'memory_update',
2504
+ success: true,
2505
+ response: { totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers },
2506
+ });
2507
+ }
2508
+ /**
2509
+ * Sends an uptime update message to all connected clients.
2510
+ *
2511
+ * @param {string} systemUptime - The system uptime in a human-readable format.
2512
+ * @param {string} processUptime - The process uptime in a human-readable format.
2513
+ */
2514
+ wssSendUptimeUpdate(systemUptime, processUptime) {
2515
+ if (!this.listening || this.webSocketServer?.clients.size === 0)
2516
+ return;
2517
+ // istanbul ignore else
2518
+ if (hasParameter('debug'))
2519
+ this.log.debug('Sending a uptime update message to all connected clients');
2520
+ // Send the message to all connected clients
2521
+ this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', success: true, response: { systemUptime, processUptime } });
2522
+ }
2523
+ /**
2524
+ * Sends an open snackbar message to all connected clients.
2525
+ *
2526
+ * @param {string} message - The message to send.
2527
+ * @param {number} timeout - The timeout in seconds for the snackbar message. Default is 5 seconds.
2528
+ * @param {'info' | 'warning' | 'error' | 'success'} severity - The severity of the message.
2529
+ * possible values are: 'info', 'warning', 'error', 'success'. Default is 'info'.
2530
+ *
2531
+ * @remarks
2532
+ * If timeout is 0, the snackbar message will be displayed until closed by the user.
2533
+ */
2534
+ wssSendSnackbarMessage(message, timeout = 5, severity = 'info') {
2535
+ if (!this.listening || this.webSocketServer?.clients.size === 0)
2536
+ return;
2537
+ this.log.debug('Sending a snackbar message to all connected clients');
2538
+ // Send the message to all connected clients
2539
+ this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'snackbar', success: true, response: { message, timeout, severity } });
2540
+ }
2541
+ /**
2542
+ * Sends a close snackbar message to all connected clients.
2543
+ * It will close the snackbar message with the same message and timeout = 0.
2544
+ *
2545
+ * @param {string} message - The message to send.
2546
+ */
2547
+ wssSendCloseSnackbarMessage(message) {
2548
+ if (!this.listening || this.webSocketServer?.clients.size === 0)
2549
+ return;
2550
+ this.log.debug('Sending a close snackbar message to all connected clients');
2551
+ // Send the message to all connected clients
2552
+ this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'close_snackbar', success: true, response: { message } });
2553
+ }
2554
+ /**
2555
+ * Sends an attribute update message to all connected WebSocket clients.
2556
+ *
2557
+ * @param {string | undefined} plugin - The name of the plugin.
2558
+ * @param {string | undefined} serialNumber - The serial number of the device.
2559
+ * @param {string | undefined} uniqueId - The unique identifier of the device.
2560
+ * @param {EndpointNumber} number - The endpoint number where the attribute belongs.
2561
+ * @param {string} id - The endpoint id where the attribute belongs.
2562
+ * @param {string} cluster - The cluster name where the attribute belongs.
2563
+ * @param {string} attribute - The name of the attribute that changed.
2564
+ * @param {number | string | boolean} value - The new value of the attribute.
2565
+ *
2566
+ * @remarks
2567
+ * This method logs a debug message and sends a JSON-formatted message to all connected WebSocket clients
2568
+ * with the updated attribute information.
2569
+ */
2570
+ wssSendAttributeChangedMessage(plugin, serialNumber, uniqueId, number, id, cluster, attribute, value) {
2571
+ if (!this.listening || this.webSocketServer?.clients.size === 0)
2572
+ return;
2573
+ this.log.debug('Sending an attribute update message to all connected clients');
2574
+ // Send the message to all connected clients
2575
+ this.wssBroadcastMessage({
2576
+ id: 0,
2577
+ src: 'Matterbridge',
2578
+ dst: 'Frontend',
2579
+ method: 'state_update',
2580
+ success: true,
2581
+ response: { plugin, serialNumber, uniqueId, number, id, cluster, attribute, value },
2582
+ });
2583
+ }
2584
+ /**
2585
+ * Sends a message to all connected clients.
2586
+ * This is an helper function to send a broadcast message to all connected clients.
2587
+ *
2588
+ * @param {WsMessageBroadcast} msg - The message to send.
2589
+ */
2590
+ wssBroadcastMessage(msg) {
2591
+ if (!this.listening || this.webSocketServer?.clients.size === 0)
2592
+ return;
2593
+ // Send the message to all connected clients
2594
+ const stringifiedMsg = JSON.stringify(msg);
2595
+ if (msg.method !== 'log')
2596
+ this.log.debug(`Sending a broadcast message: ${debugStringify(msg)}`);
2597
+ this.webSocketServer?.clients.forEach((client) => {
2598
+ // istanbul ignore else
2599
+ if (client.readyState === client.OPEN) {
2600
+ client.send(stringifiedMsg);
2601
+ }
2602
+ });
2603
+ }
2604
+ }
2605
+ //# sourceMappingURL=frontend.js.map