homebridge 2.0.0-beta.35 → 2.0.0-beta.37

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 (280) hide show
  1. package/dist/api.d.ts +74 -32
  2. package/dist/api.d.ts.map +1 -1
  3. package/dist/api.js +48 -46
  4. package/dist/api.js.map +1 -1
  5. package/dist/api.spec.d.ts +2 -0
  6. package/dist/api.spec.d.ts.map +1 -0
  7. package/dist/api.spec.js +413 -0
  8. package/dist/api.spec.js.map +1 -0
  9. package/dist/childBridgeFork.d.ts +1 -18
  10. package/dist/childBridgeFork.d.ts.map +1 -1
  11. package/dist/childBridgeFork.js +21 -258
  12. package/dist/childBridgeFork.js.map +1 -1
  13. package/dist/index.d.ts +10 -10
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +3 -3
  16. package/dist/index.js.map +1 -1
  17. package/dist/logger.spec.d.ts +2 -0
  18. package/dist/logger.spec.d.ts.map +1 -0
  19. package/dist/logger.spec.js +95 -0
  20. package/dist/logger.spec.js.map +1 -0
  21. package/dist/matter/ChildBridgeMatterManager.d.ts +96 -0
  22. package/dist/matter/ChildBridgeMatterManager.d.ts.map +1 -0
  23. package/dist/matter/ChildBridgeMatterManager.js +397 -0
  24. package/dist/matter/ChildBridgeMatterManager.js.map +1 -0
  25. package/dist/matter/ExternalMatterAccessoryPublisher.d.ts +48 -0
  26. package/dist/matter/ExternalMatterAccessoryPublisher.d.ts.map +1 -0
  27. package/dist/matter/ExternalMatterAccessoryPublisher.js +73 -0
  28. package/dist/matter/ExternalMatterAccessoryPublisher.js.map +1 -0
  29. package/dist/matter/ExternalMatterAccessoryPublisher.spec.d.ts +2 -0
  30. package/dist/matter/ExternalMatterAccessoryPublisher.spec.d.ts.map +1 -0
  31. package/dist/matter/ExternalMatterAccessoryPublisher.spec.js +293 -0
  32. package/dist/matter/ExternalMatterAccessoryPublisher.spec.js.map +1 -0
  33. package/dist/matter/{matterServer.d.ts → MatterAPIImpl.d.ts} +203 -367
  34. package/dist/matter/MatterAPIImpl.d.ts.map +1 -0
  35. package/dist/matter/MatterAPIImpl.js +305 -0
  36. package/dist/matter/MatterAPIImpl.js.map +1 -0
  37. package/dist/matter/MatterBridgeManager.d.ts +91 -0
  38. package/dist/matter/MatterBridgeManager.d.ts.map +1 -0
  39. package/dist/matter/MatterBridgeManager.js +426 -0
  40. package/dist/matter/MatterBridgeManager.js.map +1 -0
  41. package/dist/matter/MatterConfigCollector.d.ts +26 -0
  42. package/dist/matter/MatterConfigCollector.d.ts.map +1 -0
  43. package/dist/matter/MatterConfigCollector.js +78 -0
  44. package/dist/matter/MatterConfigCollector.js.map +1 -0
  45. package/dist/matter/{matterAccessoryCache.d.ts → accessoryCache.d.ts} +12 -3
  46. package/dist/matter/accessoryCache.d.ts.map +1 -0
  47. package/dist/matter/{matterAccessoryCache.js → accessoryCache.js} +32 -10
  48. package/dist/matter/accessoryCache.js.map +1 -0
  49. package/dist/matter/accessoryCache.spec.d.ts +2 -0
  50. package/dist/matter/accessoryCache.spec.d.ts.map +1 -0
  51. package/dist/matter/accessoryCache.spec.js +452 -0
  52. package/dist/matter/accessoryCache.spec.js.map +1 -0
  53. package/dist/matter/behaviors/BehaviorRegistry.d.ts +65 -0
  54. package/dist/matter/behaviors/BehaviorRegistry.d.ts.map +1 -0
  55. package/dist/matter/behaviors/BehaviorRegistry.js +139 -0
  56. package/dist/matter/behaviors/BehaviorRegistry.js.map +1 -0
  57. package/dist/matter/behaviors/BehaviorRegistry.spec.d.ts +2 -0
  58. package/dist/matter/behaviors/BehaviorRegistry.spec.d.ts.map +1 -0
  59. package/dist/matter/behaviors/BehaviorRegistry.spec.js +307 -0
  60. package/dist/matter/behaviors/BehaviorRegistry.spec.js.map +1 -0
  61. package/dist/matter/behaviors/ColorControlBehavior.d.ts +64 -0
  62. package/dist/matter/behaviors/ColorControlBehavior.d.ts.map +1 -0
  63. package/dist/matter/behaviors/ColorControlBehavior.js +160 -0
  64. package/dist/matter/behaviors/ColorControlBehavior.js.map +1 -0
  65. package/dist/matter/behaviors/ColorControlBehavior.spec.d.ts +2 -0
  66. package/dist/matter/behaviors/ColorControlBehavior.spec.d.ts.map +1 -0
  67. package/dist/matter/behaviors/ColorControlBehavior.spec.js +29 -0
  68. package/dist/matter/behaviors/ColorControlBehavior.spec.js.map +1 -0
  69. package/dist/matter/behaviors/DoorLockBehavior.d.ts +21 -0
  70. package/dist/matter/behaviors/DoorLockBehavior.d.ts.map +1 -0
  71. package/dist/matter/behaviors/DoorLockBehavior.js +49 -0
  72. package/dist/matter/behaviors/DoorLockBehavior.js.map +1 -0
  73. package/dist/matter/behaviors/DoorLockBehavior.spec.d.ts +2 -0
  74. package/dist/matter/behaviors/DoorLockBehavior.spec.d.ts.map +1 -0
  75. package/dist/matter/behaviors/DoorLockBehavior.spec.js +108 -0
  76. package/dist/matter/behaviors/DoorLockBehavior.spec.js.map +1 -0
  77. package/dist/matter/behaviors/FanControlBehavior.d.ts +20 -0
  78. package/dist/matter/behaviors/FanControlBehavior.d.ts.map +1 -0
  79. package/dist/matter/behaviors/FanControlBehavior.js +45 -0
  80. package/dist/matter/behaviors/FanControlBehavior.js.map +1 -0
  81. package/dist/matter/behaviors/FanControlBehavior.spec.d.ts +2 -0
  82. package/dist/matter/behaviors/FanControlBehavior.spec.d.ts.map +1 -0
  83. package/dist/matter/behaviors/FanControlBehavior.spec.js +23 -0
  84. package/dist/matter/behaviors/FanControlBehavior.spec.js.map +1 -0
  85. package/dist/matter/behaviors/IdentifyBehavior.d.ts +21 -0
  86. package/dist/matter/behaviors/IdentifyBehavior.d.ts.map +1 -0
  87. package/dist/matter/behaviors/IdentifyBehavior.js +27 -0
  88. package/dist/matter/behaviors/IdentifyBehavior.js.map +1 -0
  89. package/dist/matter/behaviors/IdentifyBehavior.spec.d.ts +2 -0
  90. package/dist/matter/behaviors/IdentifyBehavior.spec.d.ts.map +1 -0
  91. package/dist/matter/behaviors/IdentifyBehavior.spec.js +64 -0
  92. package/dist/matter/behaviors/IdentifyBehavior.spec.js.map +1 -0
  93. package/dist/matter/behaviors/LevelControlBehavior.d.ts +34 -0
  94. package/dist/matter/behaviors/LevelControlBehavior.d.ts.map +1 -0
  95. package/dist/matter/behaviors/LevelControlBehavior.js +75 -0
  96. package/dist/matter/behaviors/LevelControlBehavior.js.map +1 -0
  97. package/dist/matter/behaviors/LevelControlBehavior.spec.d.ts +2 -0
  98. package/dist/matter/behaviors/LevelControlBehavior.spec.d.ts.map +1 -0
  99. package/dist/matter/behaviors/LevelControlBehavior.spec.js +140 -0
  100. package/dist/matter/behaviors/LevelControlBehavior.spec.js.map +1 -0
  101. package/dist/matter/behaviors/OnOffBehavior.d.ts +28 -0
  102. package/dist/matter/behaviors/OnOffBehavior.d.ts.map +1 -0
  103. package/dist/matter/behaviors/OnOffBehavior.js +63 -0
  104. package/dist/matter/behaviors/OnOffBehavior.js.map +1 -0
  105. package/dist/matter/behaviors/OnOffBehavior.spec.d.ts +2 -0
  106. package/dist/matter/behaviors/OnOffBehavior.spec.d.ts.map +1 -0
  107. package/dist/matter/behaviors/OnOffBehavior.spec.js +116 -0
  108. package/dist/matter/behaviors/OnOffBehavior.spec.js.map +1 -0
  109. package/dist/matter/behaviors/RvcCleanModeBehavior.d.ts +19 -0
  110. package/dist/matter/behaviors/RvcCleanModeBehavior.d.ts.map +1 -0
  111. package/dist/matter/behaviors/RvcCleanModeBehavior.js +27 -0
  112. package/dist/matter/behaviors/RvcCleanModeBehavior.js.map +1 -0
  113. package/dist/matter/behaviors/RvcCleanModeBehavior.spec.d.ts +2 -0
  114. package/dist/matter/behaviors/RvcCleanModeBehavior.spec.d.ts.map +1 -0
  115. package/dist/matter/behaviors/RvcCleanModeBehavior.spec.js +56 -0
  116. package/dist/matter/behaviors/RvcCleanModeBehavior.spec.js.map +1 -0
  117. package/dist/matter/behaviors/RvcOperationalStateBehavior.d.ts +23 -0
  118. package/dist/matter/behaviors/RvcOperationalStateBehavior.d.ts.map +1 -0
  119. package/dist/matter/behaviors/RvcOperationalStateBehavior.js +49 -0
  120. package/dist/matter/behaviors/RvcOperationalStateBehavior.js.map +1 -0
  121. package/dist/matter/behaviors/RvcOperationalStateBehavior.spec.d.ts +2 -0
  122. package/dist/matter/behaviors/RvcOperationalStateBehavior.spec.d.ts.map +1 -0
  123. package/dist/matter/behaviors/RvcOperationalStateBehavior.spec.js +55 -0
  124. package/dist/matter/behaviors/RvcOperationalStateBehavior.spec.js.map +1 -0
  125. package/dist/matter/behaviors/RvcRunModeBehavior.d.ts +19 -0
  126. package/dist/matter/behaviors/RvcRunModeBehavior.d.ts.map +1 -0
  127. package/dist/matter/behaviors/RvcRunModeBehavior.js +27 -0
  128. package/dist/matter/behaviors/RvcRunModeBehavior.js.map +1 -0
  129. package/dist/matter/behaviors/RvcRunModeBehavior.spec.d.ts +2 -0
  130. package/dist/matter/behaviors/RvcRunModeBehavior.spec.d.ts.map +1 -0
  131. package/dist/matter/behaviors/RvcRunModeBehavior.spec.js +56 -0
  132. package/dist/matter/behaviors/RvcRunModeBehavior.spec.js.map +1 -0
  133. package/dist/matter/behaviors/ServiceAreaBehavior.d.ts +22 -0
  134. package/dist/matter/behaviors/ServiceAreaBehavior.d.ts.map +1 -0
  135. package/dist/matter/behaviors/ServiceAreaBehavior.js +35 -0
  136. package/dist/matter/behaviors/ServiceAreaBehavior.js.map +1 -0
  137. package/dist/matter/behaviors/ServiceAreaBehavior.spec.d.ts +2 -0
  138. package/dist/matter/behaviors/ServiceAreaBehavior.spec.d.ts.map +1 -0
  139. package/dist/matter/behaviors/ServiceAreaBehavior.spec.js +52 -0
  140. package/dist/matter/behaviors/ServiceAreaBehavior.spec.js.map +1 -0
  141. package/dist/matter/behaviors/ThermostatBehavior.d.ts +23 -0
  142. package/dist/matter/behaviors/ThermostatBehavior.d.ts.map +1 -0
  143. package/dist/matter/behaviors/ThermostatBehavior.js +79 -0
  144. package/dist/matter/behaviors/ThermostatBehavior.js.map +1 -0
  145. package/dist/matter/behaviors/ThermostatBehavior.spec.d.ts +2 -0
  146. package/dist/matter/behaviors/ThermostatBehavior.spec.d.ts.map +1 -0
  147. package/dist/matter/behaviors/ThermostatBehavior.spec.js +23 -0
  148. package/dist/matter/behaviors/ThermostatBehavior.spec.js.map +1 -0
  149. package/dist/matter/behaviors/WindowCoveringBehavior.d.ts +32 -0
  150. package/dist/matter/behaviors/WindowCoveringBehavior.d.ts.map +1 -0
  151. package/dist/matter/behaviors/WindowCoveringBehavior.js +106 -0
  152. package/dist/matter/behaviors/WindowCoveringBehavior.js.map +1 -0
  153. package/dist/matter/behaviors/WindowCoveringBehavior.spec.d.ts +2 -0
  154. package/dist/matter/behaviors/WindowCoveringBehavior.spec.d.ts.map +1 -0
  155. package/dist/matter/behaviors/WindowCoveringBehavior.spec.js +27 -0
  156. package/dist/matter/behaviors/WindowCoveringBehavior.spec.js.map +1 -0
  157. package/dist/matter/behaviors/index.d.ts +20 -0
  158. package/dist/matter/behaviors/index.d.ts.map +1 -0
  159. package/dist/matter/behaviors/index.js +21 -0
  160. package/dist/matter/behaviors/index.js.map +1 -0
  161. package/dist/matter/{matterConfigValidator.d.ts → configValidator.d.ts} +1 -1
  162. package/dist/matter/configValidator.d.ts.map +1 -0
  163. package/dist/matter/{matterConfigValidator.js → configValidator.js} +1 -1
  164. package/dist/matter/configValidator.js.map +1 -0
  165. package/dist/matter/configValidator.spec.d.ts +2 -0
  166. package/dist/matter/configValidator.spec.d.ts.map +1 -0
  167. package/dist/matter/configValidator.spec.js +390 -0
  168. package/dist/matter/configValidator.spec.js.map +1 -0
  169. package/dist/matter/errorHandler.d.ts +33 -0
  170. package/dist/matter/errorHandler.d.ts.map +1 -0
  171. package/dist/matter/errorHandler.js +113 -0
  172. package/dist/matter/errorHandler.js.map +1 -0
  173. package/dist/matter/errorHandler.spec.d.ts +2 -0
  174. package/dist/matter/errorHandler.spec.d.ts.map +1 -0
  175. package/dist/matter/errorHandler.spec.js +159 -0
  176. package/dist/matter/errorHandler.spec.js.map +1 -0
  177. package/dist/matter/index.d.ts +7 -4
  178. package/dist/matter/index.d.ts.map +1 -1
  179. package/dist/matter/index.js +7 -4
  180. package/dist/matter/index.js.map +1 -1
  181. package/dist/matter/{matterLogFormatter.d.ts → logFormatter.d.ts} +1 -1
  182. package/dist/matter/logFormatter.d.ts.map +1 -0
  183. package/dist/matter/{matterLogFormatter.js → logFormatter.js} +33 -19
  184. package/dist/matter/logFormatter.js.map +1 -0
  185. package/dist/matter/logFormatter.spec.d.ts +2 -0
  186. package/dist/matter/logFormatter.spec.d.ts.map +1 -0
  187. package/dist/matter/logFormatter.spec.js +252 -0
  188. package/dist/matter/logFormatter.spec.js.map +1 -0
  189. package/dist/matter/server.d.ts +343 -0
  190. package/dist/matter/server.d.ts.map +1 -0
  191. package/dist/matter/{matterServer.js → server.js} +549 -365
  192. package/dist/matter/server.js.map +1 -0
  193. package/dist/matter/{matterServerHelpers.d.ts → serverHelpers.d.ts} +2 -2
  194. package/dist/matter/serverHelpers.d.ts.map +1 -0
  195. package/dist/matter/{matterServerHelpers.js → serverHelpers.js} +11 -8
  196. package/dist/matter/serverHelpers.js.map +1 -0
  197. package/dist/matter/serverHelpers.spec.d.ts +2 -0
  198. package/dist/matter/serverHelpers.spec.d.ts.map +1 -0
  199. package/dist/matter/serverHelpers.spec.js +521 -0
  200. package/dist/matter/serverHelpers.spec.js.map +1 -0
  201. package/dist/matter/{matterSharedTypes.d.ts → sharedTypes.d.ts} +4 -10
  202. package/dist/matter/sharedTypes.d.ts.map +1 -0
  203. package/dist/matter/{matterSharedTypes.js → sharedTypes.js} +4 -10
  204. package/dist/matter/sharedTypes.js.map +1 -0
  205. package/dist/matter/{matterStorage.d.ts → storage.d.ts} +9 -2
  206. package/dist/matter/storage.d.ts.map +1 -0
  207. package/dist/matter/{matterStorage.js → storage.js} +14 -5
  208. package/dist/matter/{matterStorage.js.map → storage.js.map} +1 -1
  209. package/dist/matter/storage.spec.d.ts +2 -0
  210. package/dist/matter/storage.spec.d.ts.map +1 -0
  211. package/dist/matter/storage.spec.js +570 -0
  212. package/dist/matter/storage.spec.js.map +1 -0
  213. package/dist/matter/typeHelpers.d.ts +45 -0
  214. package/dist/matter/typeHelpers.d.ts.map +1 -0
  215. package/dist/matter/typeHelpers.js +57 -0
  216. package/dist/matter/typeHelpers.js.map +1 -0
  217. package/dist/matter/typeHelpers.spec.d.ts +2 -0
  218. package/dist/matter/typeHelpers.spec.d.ts.map +1 -0
  219. package/dist/matter/typeHelpers.spec.js +127 -0
  220. package/dist/matter/typeHelpers.spec.js.map +1 -0
  221. package/dist/matter/{matterTypes.d.ts → types.d.ts} +2 -2
  222. package/dist/matter/types.d.ts.map +1 -0
  223. package/dist/matter/{matterTypes.js → types.js} +1 -2
  224. package/dist/matter/types.js.map +1 -0
  225. package/dist/platformAccessory.spec.d.ts +2 -0
  226. package/dist/platformAccessory.spec.d.ts.map +1 -0
  227. package/dist/platformAccessory.spec.js +126 -0
  228. package/dist/platformAccessory.spec.js.map +1 -0
  229. package/dist/pluginManager.spec.d.ts +2 -0
  230. package/dist/pluginManager.spec.d.ts.map +1 -0
  231. package/dist/pluginManager.spec.js +43 -0
  232. package/dist/pluginManager.spec.js.map +1 -0
  233. package/dist/server.d.ts +4 -13
  234. package/dist/server.d.ts.map +1 -1
  235. package/dist/server.js +43 -346
  236. package/dist/server.js.map +1 -1
  237. package/dist/server.spec.d.ts +2 -0
  238. package/dist/server.spec.d.ts.map +1 -0
  239. package/dist/server.spec.js +57 -0
  240. package/dist/server.spec.js.map +1 -0
  241. package/dist/user.spec.d.ts +2 -0
  242. package/dist/user.spec.d.ts.map +1 -0
  243. package/dist/user.spec.js +31 -0
  244. package/dist/user.spec.js.map +1 -0
  245. package/dist/util/mac.spec.d.ts +2 -0
  246. package/dist/util/mac.spec.d.ts.map +1 -0
  247. package/dist/util/mac.spec.js +36 -0
  248. package/dist/util/mac.spec.js.map +1 -0
  249. package/dist/version.spec.d.ts +2 -0
  250. package/dist/version.spec.d.ts.map +1 -0
  251. package/dist/version.spec.js +20 -0
  252. package/dist/version.spec.js.map +1 -0
  253. package/package.json +4 -4
  254. package/dist/matter/matterAccessoryCache.d.ts.map +0 -1
  255. package/dist/matter/matterAccessoryCache.js.map +0 -1
  256. package/dist/matter/matterBehaviors.d.ts +0 -194
  257. package/dist/matter/matterBehaviors.d.ts.map +0 -1
  258. package/dist/matter/matterBehaviors.js +0 -665
  259. package/dist/matter/matterBehaviors.js.map +0 -1
  260. package/dist/matter/matterConfigValidator.d.ts.map +0 -1
  261. package/dist/matter/matterConfigValidator.js.map +0 -1
  262. package/dist/matter/matterErrorHandler.d.ts +0 -106
  263. package/dist/matter/matterErrorHandler.d.ts.map +0 -1
  264. package/dist/matter/matterErrorHandler.js +0 -495
  265. package/dist/matter/matterErrorHandler.js.map +0 -1
  266. package/dist/matter/matterLogFormatter.d.ts.map +0 -1
  267. package/dist/matter/matterLogFormatter.js.map +0 -1
  268. package/dist/matter/matterNetworkMonitor.d.ts +0 -68
  269. package/dist/matter/matterNetworkMonitor.d.ts.map +0 -1
  270. package/dist/matter/matterNetworkMonitor.js +0 -249
  271. package/dist/matter/matterNetworkMonitor.js.map +0 -1
  272. package/dist/matter/matterServer.d.ts.map +0 -1
  273. package/dist/matter/matterServer.js.map +0 -1
  274. package/dist/matter/matterServerHelpers.d.ts.map +0 -1
  275. package/dist/matter/matterServerHelpers.js.map +0 -1
  276. package/dist/matter/matterSharedTypes.d.ts.map +0 -1
  277. package/dist/matter/matterSharedTypes.js.map +0 -1
  278. package/dist/matter/matterStorage.d.ts.map +0 -1
  279. package/dist/matter/matterTypes.d.ts.map +0 -1
  280. package/dist/matter/matterTypes.js.map +0 -1
@@ -1,12 +1,7 @@
1
- /**
2
- * Matter.js Server Implementation for Homebridge Plugin API
3
- *
4
- * This provides a Matter bridge that plugins can use to register
5
- * Matter accessories via the Homebridge API.
6
- */
1
+ /* global NodeJS */
7
2
  import { randomBytes } from 'node:crypto';
8
3
  import { EventEmitter } from 'node:events';
9
- import { constants, existsSync } from 'node:fs';
4
+ import { constants, existsSync, watch as fsWatch, writeFileSync } from 'node:fs';
10
5
  import { access, writeFile } from 'node:fs/promises';
11
6
  import { homedir, release } from 'node:os';
12
7
  import { join, normalize, resolve } from 'node:path';
@@ -21,15 +16,15 @@ import fse from 'fs-extra';
21
16
  import QRCode from 'qrcode-terminal';
22
17
  import { Logger } from '../logger.js';
23
18
  import getVersion from '../version.js';
24
- import { MatterAccessoryCache } from './matterAccessoryCache.js';
25
- import { HomebridgeColorControlServer, HomebridgeDoorLockServer, HomebridgeFanControlServer, HomebridgeIdentifyServer, HomebridgeLevelControlServer, HomebridgeOnOffServer, HomebridgeRvcCleanModeServer, HomebridgeRvcOperationalStateServer, HomebridgeRvcRunModeServer, HomebridgeServiceAreaServer, HomebridgeThermostatServer, HomebridgeWindowCoveringServer, registerHandler, registerPartEndpoint, setAccessoriesMap, } from './matterBehaviors.js';
26
- import { sanitizeUniqueId, truncateString, validatePort } from './matterConfigValidator.js';
27
- import { errorHandler } from './matterErrorHandler.js';
28
- import { createHomebridgeLogFormatter } from './matterLogFormatter.js';
29
- import { networkMonitor } from './matterNetworkMonitor.js';
30
- import { applyWindowCoveringFeatures, CLUSTER_IDS, detectBehaviorFeatures, detectWindowCoveringFeatures, determineColorControlFeaturesFromHandlers, extractColorControlFeatures, extractThermostatFeatures, validateAccessoryRequiredFields, } from './matterServerHelpers.js';
31
- import { MatterStorageManager } from './matterStorage.js';
32
- import { deviceTypes, MatterDeviceError, } from './matterTypes.js';
19
+ import { MatterAccessoryCache } from './accessoryCache.js';
20
+ import { BehaviorRegistry, HomebridgeColorControlServer, HomebridgeDoorLockServer, HomebridgeFanControlServer, HomebridgeIdentifyServer, HomebridgeLevelControlServer, HomebridgeOnOffServer, HomebridgeRvcCleanModeServer, HomebridgeRvcOperationalStateServer, HomebridgeRvcRunModeServer, HomebridgeServiceAreaServer, HomebridgeThermostatServer, HomebridgeWindowCoveringServer, } from './behaviors/index.js';
21
+ import { sanitizeUniqueId, truncateString, validatePort } from './configValidator.js';
22
+ import { errorHandler } from './errorHandler.js';
23
+ import { createHomebridgeLogFormatter } from './logFormatter.js';
24
+ import { applyWindowCoveringFeatures, CLUSTER_IDS, detectBehaviorFeatures, detectWindowCoveringFeatures, determineColorControlFeaturesFromHandlers, extractColorControlFeatures, extractThermostatFeatures, validateAccessoryRequiredFields, } from './serverHelpers.js';
25
+ import { MatterStorageManager } from './storage.js';
26
+ import { isDeviceType, withBehaviors, withFeatures } from './typeHelpers.js';
27
+ import { deviceTypes, MatterDeviceError, } from './types.js';
33
28
  const log = Logger.withPrefix('Matter/Server');
34
29
  /**
35
30
  * Constants for Matter server configuration
@@ -42,7 +37,7 @@ const SERVER_READY_TIMEOUT_MS = 5000;
42
37
  const SERVER_READY_POLL_INTERVAL_MS = 100;
43
38
  const SERVER_INIT_DELAY_MS = 200;
44
39
  const MAX_PASSCODE_ATTEMPTS = 100;
45
- const FABRIC_MONITOR_INTERVAL_MS = 2000; // check for fabric changes every 2 seconds
40
+ const FABRIC_MONITOR_INTERVAL_MS = 60000; // check for fabric changes every minute
46
41
  /**
47
42
  * Matter Server for Homebridge Plugin API
48
43
  * Allows plugins to register Matter accessories explicitly
@@ -54,9 +49,31 @@ export class MatterServer extends EventEmitter {
54
49
  serverNode = null;
55
50
  aggregator = null;
56
51
  accessories = new Map();
52
+ behaviorRegistry;
57
53
  isRunning = false;
58
54
  MAX_DEVICES = MAX_DEVICES_PER_BRIDGE;
59
55
  shutdownHandler = null;
56
+ // Map cluster names to custom behavior classes
57
+ // Only clusters with user-triggered commands need custom behaviors
58
+ static CLUSTER_BEHAVIOR_MAP = {
59
+ // Core controls
60
+ onOff: HomebridgeOnOffServer,
61
+ levelControl: HomebridgeLevelControlServer,
62
+ colorControl: HomebridgeColorControlServer,
63
+ // Coverings & locks
64
+ windowCovering: HomebridgeWindowCoveringServer,
65
+ doorLock: HomebridgeDoorLockServer,
66
+ // Climate control
67
+ fanControl: HomebridgeFanControlServer,
68
+ thermostat: HomebridgeThermostatServer,
69
+ // Robotic vacuum cleaners
70
+ rvcOperationalState: HomebridgeRvcOperationalStateServer,
71
+ rvcRunMode: HomebridgeRvcRunModeServer,
72
+ rvcCleanMode: HomebridgeRvcCleanModeServer,
73
+ serviceArea: HomebridgeServiceAreaServer,
74
+ // Identification
75
+ identify: HomebridgeIdentifyServer,
76
+ };
60
77
  // Internal commissioning values (generated, not user-configurable)
61
78
  passcode = 0;
62
79
  discriminator = 0;
@@ -70,6 +87,7 @@ export class MatterServer extends EventEmitter {
70
87
  accessoryCache = null;
71
88
  // Fabric monitoring
72
89
  fabricMonitorInterval = null;
90
+ fabricCheckTimeout = null;
73
91
  previousFabrics = new Map();
74
92
  constructor(config) {
75
93
  super();
@@ -84,19 +102,34 @@ export class MatterServer extends EventEmitter {
84
102
  else {
85
103
  MatterLogger.level = MatterLogLevel.NOTICE;
86
104
  }
87
- // Set custom log format to match homebridge format:
88
- // [DD/MM/YYYY, HH:MM:SS] [Matter/Facility] message
105
+ // Set custom log format to match homebridge format
89
106
  MatterLogger.format = createHomebridgeLogFormatter();
90
107
  // Redirect all Matter.js logs to console.log to prevent console.debug() suppression.
91
108
  // Matter.js uses console.debug() for DEBUG level logs, which is silently ignored in many Node.js environments.
92
109
  MatterLogger.destinations.default.write = (text) => {
93
- console.log(text); // eslint-disable-line no-console
110
+ // Skip empty strings to avoid blank lines (from suppressed log facilities)
111
+ if (text.trim() !== '') {
112
+ console.log(text); // eslint-disable-line no-console
113
+ }
94
114
  };
95
115
  // Initialize commissioning values (will be loaded from storage in start())
96
116
  this.vendorId = DEFAULT_VENDOR_ID;
97
117
  this.productId = DEFAULT_PRODUCT_ID;
98
- // Provide accessories map reference to behaviors for cache syncing
99
- setAccessoriesMap(this.accessories);
118
+ // Create behavior registry and set it on all behavior classes
119
+ this.behaviorRegistry = new BehaviorRegistry(this.accessories);
120
+ // Set the registry on all custom behavior classes
121
+ HomebridgeOnOffServer.setRegistry(this.behaviorRegistry);
122
+ HomebridgeLevelControlServer.setRegistry(this.behaviorRegistry);
123
+ HomebridgeColorControlServer.setRegistry(this.behaviorRegistry);
124
+ HomebridgeWindowCoveringServer.setRegistry(this.behaviorRegistry);
125
+ HomebridgeDoorLockServer.setRegistry(this.behaviorRegistry);
126
+ HomebridgeFanControlServer.setRegistry(this.behaviorRegistry);
127
+ HomebridgeThermostatServer.setRegistry(this.behaviorRegistry);
128
+ HomebridgeIdentifyServer.setRegistry(this.behaviorRegistry);
129
+ HomebridgeRvcOperationalStateServer.setRegistry(this.behaviorRegistry);
130
+ HomebridgeRvcRunModeServer.setRegistry(this.behaviorRegistry);
131
+ HomebridgeRvcCleanModeServer.setRegistry(this.behaviorRegistry);
132
+ HomebridgeServiceAreaServer.setRegistry(this.behaviorRegistry);
100
133
  }
101
134
  /**
102
135
  * Validate and sanitize Matter server configuration
@@ -239,7 +272,8 @@ export class MatterServer extends EventEmitter {
239
272
  const digitCounts = new Map();
240
273
  for (const digit of passcodeStr) {
241
274
  digitCounts.set(digit, (digitCounts.get(digit) || 0) + 1);
242
- if (digitCounts.get(digit) > 3) {
275
+ const count = digitCounts.get(digit);
276
+ if (count !== undefined && count > 3) {
243
277
  return false;
244
278
  }
245
279
  }
@@ -285,10 +319,12 @@ export class MatterServer extends EventEmitter {
285
319
  }
286
320
  catch (error) {
287
321
  // Check if this is a storage corruption error
288
- const isStorageError = error?.message?.includes('Invalid public key encoding')
289
- || error?.message?.includes('FabricManager unavailable')
290
- || error?.message?.includes('key-input')
291
- || error?.cause?.message?.includes('Invalid public key encoding');
322
+ const errorMessage = error instanceof Error ? error.message : '';
323
+ const causeMessage = error instanceof Error && error.cause instanceof Error ? error.cause.message : '';
324
+ const isStorageError = errorMessage.includes('Invalid public key encoding')
325
+ || errorMessage.includes('FabricManager unavailable')
326
+ || errorMessage.includes('key-input')
327
+ || causeMessage.includes('Invalid public key encoding');
292
328
  if (!isStorageError) {
293
329
  // Not a storage error, rethrow
294
330
  throw error;
@@ -306,18 +342,32 @@ export class MatterServer extends EventEmitter {
306
342
  const serverNodeStoreJsonFile = `${serverNodeStorePath}.json`;
307
343
  try {
308
344
  let removedSomething = false;
309
- // Delete the ServerNodeStore subdirectory
310
- if (existsSync(serverNodeStorePath)) {
345
+ // Delete the ServerNodeStore subdirectory (async check and removal)
346
+ try {
347
+ await fse.stat(serverNodeStorePath);
311
348
  log.info(`Removing corrupted ServerNodeStore directory: ${serverNodeStorePath}`);
312
349
  await fse.remove(serverNodeStorePath);
313
350
  removedSomething = true;
314
351
  }
352
+ catch (err) {
353
+ const code = err instanceof Error && 'code' in err ? err.code : undefined;
354
+ if (code !== 'ENOENT') {
355
+ throw err;
356
+ }
357
+ }
315
358
  // Delete the ServerNodeStore JSON file (contains fabric data)
316
- if (existsSync(serverNodeStoreJsonFile)) {
359
+ try {
360
+ await fse.stat(serverNodeStoreJsonFile);
317
361
  log.info(`Removing corrupted ServerNodeStore JSON file: ${serverNodeStoreJsonFile}`);
318
362
  await fse.remove(serverNodeStoreJsonFile);
319
363
  removedSomething = true;
320
364
  }
365
+ catch (err) {
366
+ const code = err instanceof Error && 'code' in err ? err.code : undefined;
367
+ if (code !== 'ENOENT') {
368
+ throw err;
369
+ }
370
+ }
321
371
  if (removedSomething) {
322
372
  log.info('Corrupted storage removed, retrying ServerNode creation...');
323
373
  }
@@ -353,9 +403,6 @@ export class MatterServer extends EventEmitter {
353
403
  // Load or generate commissioning credentials
354
404
  await this.loadOrGenerateCredentials();
355
405
  log.info(`Configuration: Port=${this.config.port}, Passcode=${this.passcode}, Discriminator=${this.discriminator}`);
356
- // Start network monitoring
357
- networkMonitor.startMonitoring();
358
- this.cleanupHandlers.push(() => networkMonitor.stopMonitoring());
359
406
  // Configure network interfaces if specified in the config
360
407
  if (this.config.networkInterfaces && this.config.networkInterfaces.length > 0) {
361
408
  const environment = Environment.default;
@@ -474,7 +521,17 @@ export class MatterServer extends EventEmitter {
474
521
  }
475
522
  /**
476
523
  * Run the server after devices have been added (for external accessory mode)
477
- * This must be called after registerPlatformAccessories() when using externalAccessory mode
524
+ *
525
+ * This must be called after registerPlatformAccessories() when using externalAccessory mode.
526
+ * In bridge mode, the server starts automatically when accessories are registered.
527
+ *
528
+ * @throws {MatterDeviceError} If server node is not initialized or server is already running
529
+ * @example
530
+ * ```typescript
531
+ * await matterServer.start()
532
+ * await matterServer.registerPlatformAccessories('plugin', 'platform', accessories)
533
+ * await matterServer.runServer() // External accessory mode only
534
+ * ```
478
535
  */
479
536
  async runServer() {
480
537
  if (!this.serverNode) {
@@ -655,22 +712,23 @@ export class MatterServer extends EventEmitter {
655
712
  log.debug(`Saved commissioning info to ${commissioningFilePath}`);
656
713
  }
657
714
  catch (error) {
658
- log.warn(`Failed to save commissioning info to disk: ${error.message}`);
715
+ const errorMessage = error instanceof Error ? error.message : String(error);
716
+ log.warn(`Failed to save commissioning info to disk: ${errorMessage}`);
659
717
  }
660
718
  // Display commissioning information
661
- log.info(`\n${'='.repeat(60)}`);
719
+ log.info(`${'='.repeat(60)}`);
662
720
  log.info('📱 MATTER COMMISSIONING INFORMATION');
663
721
  log.info('='.repeat(60));
664
722
  log.info(`Manual Pairing Code: ${manualPairingCode}`);
665
723
  log.info(`Passcode: ${passcode}`);
666
724
  log.info(`Discriminator: ${discriminator}`);
667
- log.info('\nQR Code for commissioning:');
725
+ log.info('QR Code for commissioning:');
668
726
  // Generate and display QR code in terminal
669
727
  QRCode.generate(qrCodePayload, { small: true }, (qrcode) => {
670
728
  // eslint-disable-next-line no-console
671
729
  console.log(qrcode);
672
730
  });
673
- log.info(`${'='.repeat(60)}\n`);
731
+ log.info(`${'='.repeat(60)}`);
674
732
  }
675
733
  /**
676
734
  * Wait for the server to be ready
@@ -690,6 +748,7 @@ export class MatterServer extends EventEmitter {
690
748
  }
691
749
  /**
692
750
  * Start monitoring fabric changes to emit commissioning events
751
+ * Uses file watching instead of polling for better performance
693
752
  */
694
753
  startFabricMonitoring() {
695
754
  // Stop any existing monitor
@@ -700,22 +759,69 @@ export class MatterServer extends EventEmitter {
700
759
  this.previousFabrics.set(fabric.fabricIndex, fabric);
701
760
  }
702
761
  log.debug('Starting fabric monitoring for commissioning events');
703
- // Set up periodic monitoring
704
- this.fabricMonitorInterval = setInterval(() => {
705
- this.checkFabricChanges();
706
- }, FABRIC_MONITOR_INTERVAL_MS);
762
+ // Watch the storage file for fabric changes instead of polling
763
+ // This is much more efficient - only checks when the file actually changes
764
+ if (this.storageManager && this.matterStoragePath) {
765
+ const fabricStorageFile = join(this.matterStoragePath, `${this.config.uniqueId}.json`);
766
+ try {
767
+ // Ensure the storage file exists before watching it
768
+ // fs.watch will fail with ENOENT if the file doesn't exist
769
+ if (!existsSync(fabricStorageFile)) {
770
+ log.debug(`Creating fabric storage file: ${fabricStorageFile}`);
771
+ writeFileSync(fabricStorageFile, '{}', 'utf-8');
772
+ }
773
+ // Use fs.watch for efficient file monitoring
774
+ this.fabricMonitorInterval = fsWatch(fabricStorageFile, (eventType) => {
775
+ if (eventType === 'change') {
776
+ // Debounce rapid changes (file writes may trigger multiple events)
777
+ if (this.fabricCheckTimeout) {
778
+ clearTimeout(this.fabricCheckTimeout);
779
+ }
780
+ this.fabricCheckTimeout = setTimeout(() => {
781
+ this.checkFabricChanges();
782
+ }, 100); // 100ms debounce
783
+ }
784
+ });
785
+ log.debug(`Watching fabric storage file for changes: ${fabricStorageFile}`);
786
+ }
787
+ catch (error) {
788
+ // Fall back to polling if file watching fails
789
+ log.warn('Failed to set up file watcher for fabric monitoring, falling back to polling:', error);
790
+ this.fabricMonitorInterval = setInterval(() => {
791
+ this.checkFabricChanges();
792
+ }, FABRIC_MONITOR_INTERVAL_MS);
793
+ }
794
+ }
795
+ else {
796
+ // Fallback to polling if storage not available
797
+ this.fabricMonitorInterval = setInterval(() => {
798
+ this.checkFabricChanges();
799
+ }, FABRIC_MONITOR_INTERVAL_MS);
800
+ }
707
801
  // Add to clean up handlers
708
802
  this.cleanupHandlers.push(() => this.stopFabricMonitoring());
709
803
  }
710
804
  /**
711
- * Stop fabric monitoring
805
+ * Stop fabric monitoring and clean up watchers/timers
712
806
  */
713
807
  stopFabricMonitoring() {
714
808
  if (this.fabricMonitorInterval) {
715
- clearInterval(this.fabricMonitorInterval);
809
+ // Could be either a setInterval or a file watcher
810
+ if (typeof this.fabricMonitorInterval === 'object' && 'close' in this.fabricMonitorInterval) {
811
+ // It's a file watcher
812
+ this.fabricMonitorInterval.close();
813
+ }
814
+ else {
815
+ // It's a setInterval
816
+ clearInterval(this.fabricMonitorInterval);
817
+ }
716
818
  this.fabricMonitorInterval = null;
717
819
  log.debug('Stopped fabric monitoring');
718
820
  }
821
+ if (this.fabricCheckTimeout) {
822
+ clearTimeout(this.fabricCheckTimeout);
823
+ this.fabricCheckTimeout = null;
824
+ }
719
825
  }
720
826
  /**
721
827
  * Check for fabric changes and emit appropriate events
@@ -794,11 +900,21 @@ export class MatterServer extends EventEmitter {
794
900
  log.debug('Updated commissioning info file');
795
901
  }
796
902
  catch (error) {
797
- log.debug(`Failed to update commissioning info file: ${error.message}`);
903
+ const errorMessage = error instanceof Error ? error.message : String(error);
904
+ log.debug(`Failed to update commissioning info file: ${errorMessage}`);
798
905
  }
799
906
  }
800
907
  /**
801
908
  * Register Matter platform accessories (Plugin API - matches HAP pattern)
909
+ *
910
+ * Registers Matter accessories from a dynamic platform plugin. Accessories are stored
911
+ * and automatically restored on server restart.
912
+ *
913
+ * @param pluginIdentifier - The plugin identifier (e.g., 'homebridge-example')
914
+ * @param platformName - The platform name from config.json
915
+ * @param accessories - Array of Matter accessories to register
916
+ * @throws {MatterDeviceError} If maximum device limit is reached or accessory is invalid
917
+ * @see {@link MatterAccessory} for accessory structure
802
918
  */
803
919
  async registerPlatformAccessories(pluginIdentifier, platformName, accessories) {
804
920
  for (const accessory of accessories) {
@@ -813,6 +929,36 @@ export class MatterServer extends EventEmitter {
813
929
  await this.unregisterAccessory(accessory.uuid);
814
930
  }
815
931
  }
932
+ /**
933
+ * Update Matter platform accessories in the cache
934
+ * Similar to api.updatePlatformAccessories() for HAP accessories
935
+ *
936
+ * This updates the cached accessory information without unregistering and re-registering.
937
+ * Useful when device metadata changes (name, manufacturer, firmware version, etc.)
938
+ */
939
+ async updatePlatformAccessories(accessories) {
940
+ if (!this.accessoryCache) {
941
+ log.warn('Cannot update Matter platform accessories - cache not initialized');
942
+ return;
943
+ }
944
+ for (const accessory of accessories) {
945
+ const internal = accessory;
946
+ // Verify accessory exists in current session and cache
947
+ if (!this.accessories.has(accessory.uuid)) {
948
+ log.warn(`Cannot update Matter accessory ${accessory.uuid} - not registered in current session`);
949
+ continue;
950
+ }
951
+ if (!this.accessoryCache.hasCached(accessory.uuid)) {
952
+ log.warn(`Cannot update Matter accessory ${accessory.uuid} - not found in cache`);
953
+ continue;
954
+ }
955
+ // Update the in-memory accessory
956
+ this.accessories.set(accessory.uuid, internal);
957
+ log.debug(`Updated Matter accessory ${accessory.uuid} (${accessory.displayName})`);
958
+ }
959
+ // Save updated accessories to cache
960
+ this.accessoryCache.requestSave(this.accessories);
961
+ }
816
962
  /**
817
963
  * Register a single Matter accessory (internal method)
818
964
  */
@@ -832,6 +978,69 @@ export class MatterServer extends EventEmitter {
832
978
  + `New accessory: "${accessory.displayName}"\n`
833
979
  + 'Each accessory must have a unique UUID. Use api.hap.uuid.generate() with a unique string.');
834
980
  }
981
+ // Restore cached state if available
982
+ this.restoreCachedState(accessory);
983
+ // Check device limit
984
+ if (this.accessories.size >= this.MAX_DEVICES) {
985
+ throw new MatterDeviceError(`Cannot register Matter accessory "${accessory.displayName}": `
986
+ + `Maximum device limit reached (${this.MAX_DEVICES} devices).\n`
987
+ + `Current registered devices: ${this.accessories.size}`);
988
+ }
989
+ try {
990
+ // Prepare device type with WindowCovering features
991
+ let deviceType = accessory.deviceType;
992
+ const windowCoveringFeatures = detectWindowCoveringFeatures(accessory);
993
+ if (windowCoveringFeatures.length > 0) {
994
+ deviceType = applyWindowCoveringFeatures(deviceType, accessory, windowCoveringFeatures);
995
+ }
996
+ // Detect cluster features for behavior configuration
997
+ const features = this.detectClusterFeatures(accessory, deviceType);
998
+ // Build and apply custom behaviors based on handlers
999
+ const customBehaviors = this.buildCustomBehaviors(accessory, deviceType, features);
1000
+ if (customBehaviors.length > 0) {
1001
+ deviceType = withBehaviors(deviceType, customBehaviors);
1002
+ log.info(`Applied ${customBehaviors.length} custom behavior(s) to device type`);
1003
+ }
1004
+ // Add BridgedDeviceBasicInformationServer for bridged devices only
1005
+ // This is required by the Matter spec for devices behind an aggregator
1006
+ // External accessories should NOT have this cluster
1007
+ if (!this.config.externalAccessory) {
1008
+ deviceType = withBehaviors(deviceType, [BridgedDeviceBasicInformationServer]);
1009
+ log.debug(`Added BridgedDeviceBasicInformationServer to ${accessory.displayName}`);
1010
+ }
1011
+ // Create endpoint with cluster states
1012
+ const endpointOptions = this.createEndpointOptions(accessory);
1013
+ const endpoint = new Endpoint(deviceType, endpointOptions);
1014
+ if (this.config.debugModeEnabled) {
1015
+ log.debug(`Created endpoint for ${accessory.displayName} with initial cluster states`);
1016
+ }
1017
+ // Add endpoint to aggregator or serverNode depending on mode
1018
+ if (this.config.externalAccessory) {
1019
+ await this.serverNode.add(endpoint);
1020
+ log.debug(`Added ${accessory.displayName} as external accessory to ServerNode`);
1021
+ }
1022
+ else {
1023
+ await this.aggregator.add(endpoint);
1024
+ if (this.config.debugModeEnabled) {
1025
+ log.debug(`Added endpoint for ${accessory.displayName} to aggregator`);
1026
+ }
1027
+ }
1028
+ // Register command handlers
1029
+ this.registerAccessoryHandlers(accessory);
1030
+ // Create and register child endpoints (parts)
1031
+ const internalParts = await this.createAccessoryParts(accessory);
1032
+ // Finalize registration (store, emit events, save cache)
1033
+ await this.finalizeAccessoryRegistration(accessory, endpoint, internalParts);
1034
+ }
1035
+ catch (error) {
1036
+ log.error(`Failed to register Matter accessory ${accessory.displayName}:`, error);
1037
+ throw new MatterDeviceError(`Failed to register accessory: ${error}`);
1038
+ }
1039
+ }
1040
+ /**
1041
+ * Restore cached state for an accessory
1042
+ */
1043
+ restoreCachedState(accessory) {
835
1044
  // Check if there's a cached version - merge cached cluster states with new registration.
836
1045
  // This ensures state persistence across Homebridge restarts.
837
1046
  if (this.accessoryCache && this.accessoryCache.hasCached(accessory.uuid)) {
@@ -858,345 +1067,304 @@ export class MatterServer extends EventEmitter {
858
1067
  log.info(`Restored cached state for Matter accessory: ${accessory.displayName}`);
859
1068
  }
860
1069
  }
861
- // Check device limit
862
- if (this.accessories.size >= this.MAX_DEVICES) {
863
- throw new MatterDeviceError(`Cannot register Matter accessory "${accessory.displayName}": `
864
- + `Maximum device limit reached (${this.MAX_DEVICES} devices).\n`
865
- + `Current registered devices: ${this.accessories.size}`);
1070
+ }
1071
+ /**
1072
+ * Detect cluster features for an accessory
1073
+ * Returns an object containing detected features for various clusters
1074
+ */
1075
+ detectClusterFeatures(accessory, deviceType) {
1076
+ // Detect WindowCovering features
1077
+ const windowCoveringFeatures = detectWindowCoveringFeatures(accessory);
1078
+ // Detect ServiceArea features
1079
+ let serviceAreaFeatures = null;
1080
+ if (accessory.clusters?.serviceArea) {
1081
+ const features = [];
1082
+ // Check if Maps feature should be enabled (when supportedMaps is defined)
1083
+ if (accessory.clusters.serviceArea.supportedMaps) {
1084
+ features.push('Maps');
1085
+ }
1086
+ // Check if ProgressReporting feature should be enabled (when progress is defined)
1087
+ if (accessory.clusters.serviceArea.progress !== undefined) {
1088
+ features.push('ProgressReporting');
1089
+ }
1090
+ if (features.length > 0) {
1091
+ serviceAreaFeatures = features;
1092
+ log.info(`ServiceArea features will be enabled for ${accessory.displayName}: ${features.join(', ')}`);
1093
+ }
866
1094
  }
867
- try {
868
- // Modify device type with custom behaviors if handlers are defined
869
- let deviceType = accessory.deviceType;
870
- // Detect WindowCovering features
871
- const windowCoveringFeatures = detectWindowCoveringFeatures(accessory);
872
- if (windowCoveringFeatures.length > 0) {
873
- deviceType = applyWindowCoveringFeatures(deviceType, accessory, windowCoveringFeatures);
1095
+ // Detect ColorControl features
1096
+ let colorControlFeatures = null;
1097
+ if (accessory.handlers?.colorControl) {
1098
+ colorControlFeatures = detectBehaviorFeatures(deviceType, CLUSTER_IDS.COLOR_CONTROL, extractColorControlFeatures);
1099
+ if (colorControlFeatures) {
1100
+ colorControlFeatures = determineColorControlFeaturesFromHandlers(accessory.handlers.colorControl);
874
1101
  }
875
- if (accessory.handlers) {
876
- log.warn(`📝 [${accessory.displayName}] Has handlers: ${Object.keys(accessory.handlers).join(', ')}`);
877
- // IMPORTANT: Detect ServiceArea features BEFORE modifying device type
878
- let serviceAreaFeatures = null;
879
- if (accessory.clusters?.serviceArea) {
880
- const features = [];
881
- // Check if Maps feature should be enabled (when supportedMaps is defined)
882
- if (accessory.clusters.serviceArea.supportedMaps) {
883
- features.push('Maps');
884
- }
885
- // Check if ProgressReporting feature should be enabled (when progress is defined)
886
- if (accessory.clusters.serviceArea.progress !== undefined) {
887
- features.push('ProgressReporting');
888
- }
889
- if (features.length > 0) {
890
- serviceAreaFeatures = features;
891
- log.info(`ServiceArea features will be enabled for ${accessory.displayName}: ${features.join(', ')}`);
1102
+ }
1103
+ // Detect Thermostat features
1104
+ let thermostatFeatures = null;
1105
+ if (accessory.handlers?.thermostat) {
1106
+ thermostatFeatures = detectBehaviorFeatures(deviceType, CLUSTER_IDS.THERMOSTAT, extractThermostatFeatures);
1107
+ }
1108
+ return {
1109
+ windowCoveringFeatures,
1110
+ serviceAreaFeatures,
1111
+ colorControlFeatures,
1112
+ thermostatFeatures,
1113
+ };
1114
+ }
1115
+ /**
1116
+ * Build custom behaviors for an accessory based on handlers
1117
+ */
1118
+ buildCustomBehaviors(accessory, deviceType, features) {
1119
+ const customBehaviors = [];
1120
+ if (!accessory.handlers) {
1121
+ return customBehaviors;
1122
+ }
1123
+ log.debug(`[${accessory.displayName}] Has handlers: ${Object.keys(accessory.handlers).join(', ')}`);
1124
+ // Use the static cluster behavior map
1125
+ const behaviorMap = MatterServer.CLUSTER_BEHAVIOR_MAP;
1126
+ // For RoboticVacuumCleaner, add optional clusters if they're defined in accessory.clusters
1127
+ // These clusters need to be added to the device type even if there are no handlers
1128
+ if (isDeviceType(deviceType, devices.RoboticVacuumCleanerDevice)) {
1129
+ // Import RVC requirements
1130
+ const { RvcCleanModeServer, ServiceAreaServer } = devices.RoboticVacuumCleanerRequirements;
1131
+ // Add RvcCleanMode if defined in clusters
1132
+ if (accessory.clusters?.rvcCleanMode) {
1133
+ // Check if there's a custom behavior with handlers
1134
+ if (accessory.handlers?.rvcCleanMode) {
1135
+ const behaviorClass = HomebridgeRvcCleanModeServer;
1136
+ customBehaviors.push(behaviorClass);
1137
+ log.info('Adding custom RvcCleanMode behavior with handlers');
1138
+ }
1139
+ else {
1140
+ // No handlers, use base server
1141
+ customBehaviors.push(RvcCleanModeServer);
1142
+ log.info('Adding base RvcCleanMode server');
1143
+ }
1144
+ }
1145
+ // Add ServiceArea if defined in clusters
1146
+ if (accessory.clusters?.serviceArea) {
1147
+ // Check if there's a custom behavior with handlers
1148
+ if (accessory.handlers?.serviceArea) {
1149
+ let behaviorClass = HomebridgeServiceAreaServer;
1150
+ // Apply features if detected
1151
+ if (features.serviceAreaFeatures && features.serviceAreaFeatures.length > 0) {
1152
+ behaviorClass = withFeatures(behaviorClass, features.serviceAreaFeatures);
1153
+ log.info(`ServiceArea custom behavior will have features: ${features.serviceAreaFeatures.join(', ')}`);
892
1154
  }
1155
+ customBehaviors.push(behaviorClass);
1156
+ log.info('Adding custom ServiceArea behavior with handlers');
893
1157
  }
894
- // Detect ColorControl features
895
- let colorControlFeatures = null;
896
- if (accessory.handlers.colorControl) {
897
- colorControlFeatures = detectBehaviorFeatures(deviceType, CLUSTER_IDS.COLOR_CONTROL, extractColorControlFeatures);
898
- if (colorControlFeatures) {
899
- colorControlFeatures = determineColorControlFeaturesFromHandlers(accessory.handlers.colorControl);
1158
+ else {
1159
+ // No handlers, use base server with features
1160
+ let behaviorClass = ServiceAreaServer;
1161
+ if (features.serviceAreaFeatures && features.serviceAreaFeatures.length > 0) {
1162
+ behaviorClass = withFeatures(behaviorClass, features.serviceAreaFeatures);
1163
+ log.info(`ServiceArea base server will have features: ${features.serviceAreaFeatures.join(', ')}`);
900
1164
  }
1165
+ customBehaviors.push(behaviorClass);
1166
+ log.info('Adding base ServiceArea server');
901
1167
  }
902
- // Detect Thermostat features
903
- let thermostatFeatures = null;
904
- if (accessory.handlers.thermostat) {
905
- thermostatFeatures = detectBehaviorFeatures(deviceType, CLUSTER_IDS.THERMOSTAT, extractThermostatFeatures);
1168
+ }
1169
+ }
1170
+ for (const clusterName of Object.keys(accessory.handlers || {})) {
1171
+ // Skip windowCovering if we already applied features via base WindowCoveringServer
1172
+ const skipWindowCoveringBehavior = accessory.context?._skipWindowCoveringBehavior;
1173
+ if (clusterName === 'windowCovering' && skipWindowCoveringBehavior) {
1174
+ log.debug('Skipping custom WindowCovering behavior (using base server with features instead)');
1175
+ continue;
1176
+ }
1177
+ // Skip RVC clusters - they're handled specially above for RoboticVacuumCleaner
1178
+ if (clusterName === 'rvcCleanMode' || clusterName === 'serviceArea') {
1179
+ continue;
1180
+ }
1181
+ let behaviorClass = behaviorMap[clusterName];
1182
+ // Apply ColorControl features if we detected them earlier
1183
+ if (clusterName === 'colorControl' && behaviorClass && features.colorControlFeatures && features.colorControlFeatures.length > 0) {
1184
+ behaviorClass = withFeatures(behaviorClass, features.colorControlFeatures);
1185
+ log.info(`ColorControl custom behavior will preserve features: ${features.colorControlFeatures.join(', ')}`);
1186
+ }
1187
+ // Apply Thermostat features if we detected them earlier
1188
+ if (clusterName === 'thermostat' && behaviorClass && features.thermostatFeatures && features.thermostatFeatures.length > 0) {
1189
+ behaviorClass = withFeatures(behaviorClass, features.thermostatFeatures);
1190
+ log.info(`Thermostat custom behavior will preserve features: ${features.thermostatFeatures.join(', ')}`);
1191
+ }
1192
+ // Apply ServiceArea features if we detected them earlier
1193
+ if (clusterName === 'serviceArea' && behaviorClass && features.serviceAreaFeatures && features.serviceAreaFeatures.length > 0) {
1194
+ behaviorClass = withFeatures(behaviorClass, features.serviceAreaFeatures);
1195
+ log.info(`ServiceArea custom behavior will preserve features: ${features.serviceAreaFeatures.join(', ')}`);
1196
+ }
1197
+ // Apply WindowCovering features to custom behavior as well
1198
+ // (features were already applied to base device type, but custom behavior needs them too)
1199
+ if (clusterName === 'windowCovering') {
1200
+ log.debug(`WindowCovering handler found: behaviorClass=${!!behaviorClass}, windowCoveringFeatures=${features.windowCoveringFeatures}, length=${features.windowCoveringFeatures?.length}`);
1201
+ if (behaviorClass && features.windowCoveringFeatures && features.windowCoveringFeatures.length > 0) {
1202
+ behaviorClass = withFeatures(behaviorClass, features.windowCoveringFeatures);
1203
+ log.debug(`WindowCovering custom behavior will have features: ${features.windowCoveringFeatures.join(', ')}`);
906
1204
  }
907
- // Map cluster names to custom behavior classes
908
- // Only clusters with user-triggered commands need custom behaviors
909
- const behaviorMap = {
910
- // Core controls
911
- onOff: HomebridgeOnOffServer,
912
- levelControl: HomebridgeLevelControlServer,
913
- colorControl: HomebridgeColorControlServer,
914
- // Coverings & locks
915
- windowCovering: HomebridgeWindowCoveringServer,
916
- doorLock: HomebridgeDoorLockServer,
917
- // Climate control
918
- fanControl: HomebridgeFanControlServer,
919
- thermostat: HomebridgeThermostatServer,
920
- // Robotic vacuum cleaners
921
- rvcOperationalState: HomebridgeRvcOperationalStateServer,
922
- rvcRunMode: HomebridgeRvcRunModeServer,
923
- rvcCleanMode: HomebridgeRvcCleanModeServer,
924
- serviceArea: HomebridgeServiceAreaServer,
925
- // Identification
926
- identify: HomebridgeIdentifyServer,
927
- };
928
- // Build array of custom behaviors to apply based on what handlers are defined
929
- const customBehaviors = [];
930
- // For RoboticVacuumCleaner, add optional clusters if they're defined in accessory.clusters
931
- // These clusters need to be added to the device type even if there are no handlers
932
- if (deviceType === devices.RoboticVacuumCleanerDevice || deviceType.name === 'RoboticVacuumCleaner') {
933
- // Import RVC requirements
934
- const { RvcCleanModeServer, ServiceAreaServer } = devices.RoboticVacuumCleanerRequirements;
935
- // Add RvcCleanMode if defined in clusters
936
- if (accessory.clusters?.rvcCleanMode) {
937
- // Check if there's a custom behavior with handlers
938
- if (accessory.handlers?.rvcCleanMode) {
939
- const behaviorClass = HomebridgeRvcCleanModeServer;
940
- customBehaviors.push(behaviorClass);
941
- log.info('Adding custom RvcCleanMode behavior with handlers');
942
- }
943
- else {
944
- // No handlers, use base server
945
- customBehaviors.push(RvcCleanModeServer);
946
- log.info('Adding base RvcCleanMode server');
947
- }
948
- }
949
- // Add ServiceArea if defined in clusters
950
- if (accessory.clusters?.serviceArea) {
951
- // Check if there's a custom behavior with handlers
952
- if (accessory.handlers?.serviceArea) {
953
- let behaviorClass = HomebridgeServiceAreaServer;
954
- // Apply features if detected
955
- if (serviceAreaFeatures && serviceAreaFeatures.length > 0) {
956
- behaviorClass = behaviorClass.with(...serviceAreaFeatures);
957
- log.info(`ServiceArea custom behavior will have features: ${serviceAreaFeatures.join(', ')}`);
958
- }
959
- customBehaviors.push(behaviorClass);
960
- log.info('Adding custom ServiceArea behavior with handlers');
961
- }
962
- else {
963
- // No handlers, use base server with features
964
- let behaviorClass = ServiceAreaServer;
965
- if (serviceAreaFeatures && serviceAreaFeatures.length > 0) {
966
- behaviorClass = behaviorClass.with(...serviceAreaFeatures);
967
- log.info(`ServiceArea base server will have features: ${serviceAreaFeatures.join(', ')}`);
968
- }
969
- customBehaviors.push(behaviorClass);
970
- log.info('Adding base ServiceArea server');
971
- }
972
- }
1205
+ else {
1206
+ log.debug(`Skipping WindowCovering feature application: behaviorClass=${!!behaviorClass}, features=${features.windowCoveringFeatures}`);
973
1207
  }
974
- for (const clusterName of Object.keys(accessory.handlers || {})) {
975
- // Skip windowCovering if we already applied features via base WindowCoveringServer
976
- if (clusterName === 'windowCovering' && accessory.context?._skipWindowCoveringBehavior) {
977
- log.debug('Skipping custom WindowCovering behavior (using base server with features instead)');
978
- continue;
979
- }
980
- // Skip RVC clusters - they're handled specially above for RoboticVacuumCleaner
981
- if (clusterName === 'rvcCleanMode' || clusterName === 'serviceArea') {
982
- continue;
983
- }
984
- let behaviorClass = behaviorMap[clusterName];
985
- // Apply ColorControl features if we detected them earlier
986
- if (clusterName === 'colorControl' && behaviorClass && colorControlFeatures && colorControlFeatures.length > 0) {
987
- behaviorClass = behaviorClass.with(...colorControlFeatures);
988
- log.info(`ColorControl custom behavior will preserve features: ${colorControlFeatures.join(', ')}`);
989
- }
990
- // Apply Thermostat features if we detected them earlier
991
- if (clusterName === 'thermostat' && behaviorClass && thermostatFeatures && thermostatFeatures.length > 0) {
992
- behaviorClass = behaviorClass.with(...thermostatFeatures);
993
- log.info(`Thermostat custom behavior will preserve features: ${thermostatFeatures.join(', ')}`);
994
- }
995
- // Apply ServiceArea features if we detected them earlier
996
- if (clusterName === 'serviceArea' && behaviorClass && serviceAreaFeatures && serviceAreaFeatures.length > 0) {
997
- behaviorClass = behaviorClass.with(...serviceAreaFeatures);
998
- log.info(`ServiceArea custom behavior will preserve features: ${serviceAreaFeatures.join(', ')}`);
999
- }
1000
- // Apply WindowCovering features to custom behavior as well
1001
- // (features were already applied to base device type, but custom behavior needs them too)
1002
- if (clusterName === 'windowCovering') {
1003
- log.warn(`🔍 WindowCovering handler found: behaviorClass=${!!behaviorClass}, windowCoveringFeatures=${windowCoveringFeatures}, length=${windowCoveringFeatures?.length}`);
1004
- if (behaviorClass && windowCoveringFeatures && windowCoveringFeatures.length > 0) {
1005
- behaviorClass = behaviorClass.with(...windowCoveringFeatures);
1006
- log.warn(`🔧 WindowCovering custom behavior will have features: ${windowCoveringFeatures.join(', ')}`);
1007
- }
1008
- else {
1009
- log.warn(`⚠️ Skipping WindowCovering feature application: behaviorClass=${!!behaviorClass}, features=${windowCoveringFeatures}`);
1010
- }
1011
- }
1208
+ }
1209
+ if (behaviorClass) {
1210
+ customBehaviors.push(behaviorClass);
1211
+ log.info(`Will use ${behaviorClass.name} for ${accessory.displayName}`);
1212
+ }
1213
+ else {
1214
+ log.warn(`No custom behavior class available for cluster '${clusterName}' - handlers will be registered but may not be called`);
1215
+ }
1216
+ }
1217
+ return customBehaviors;
1218
+ }
1219
+ /**
1220
+ * Create endpoint options for an accessory
1221
+ */
1222
+ createEndpointOptions(accessory) {
1223
+ const endpointOptions = {
1224
+ id: accessory.uuid,
1225
+ ...accessory.clusters, // Spread cluster states as initial values
1226
+ };
1227
+ // Add bridgedDeviceBasicInformation cluster only for bridged devices
1228
+ // For external accessories, use the root basicInformation instead
1229
+ if (!this.config.externalAccessory) {
1230
+ endpointOptions.bridgedDeviceBasicInformation = {
1231
+ vendorName: accessory.manufacturer,
1232
+ nodeLabel: accessory.displayName, // Main end user name for the device
1233
+ productName: accessory.model,
1234
+ productLabel: accessory.displayName,
1235
+ serialNumber: accessory.serialNumber,
1236
+ reachable: true,
1237
+ };
1238
+ }
1239
+ return endpointOptions;
1240
+ }
1241
+ /**
1242
+ * Register command handlers for an accessory
1243
+ */
1244
+ registerAccessoryHandlers(accessory) {
1245
+ if (!accessory.handlers) {
1246
+ return;
1247
+ }
1248
+ log.info(`Setting up handlers for accessory ${accessory.uuid}`);
1249
+ // Register handlers with the custom behavior classes
1250
+ for (const [clusterName, handlers] of Object.entries(accessory.handlers)) {
1251
+ log.info(` Processing cluster: ${clusterName}`);
1252
+ for (const [commandName, handler] of Object.entries(handlers)) {
1253
+ this.behaviorRegistry.registerHandler(accessory.uuid, clusterName, commandName, handler);
1254
+ }
1255
+ }
1256
+ }
1257
+ /**
1258
+ * Create and register child endpoints (parts) for an accessory
1259
+ */
1260
+ async createAccessoryParts(accessory) {
1261
+ const internalParts = [];
1262
+ if (!accessory.parts || accessory.parts.length === 0) {
1263
+ return internalParts;
1264
+ }
1265
+ log.info(`Creating ${accessory.parts.length} child endpoint(s) for ${accessory.displayName}`);
1266
+ for (const part of accessory.parts) {
1267
+ // Create unique endpoint ID for this part
1268
+ const partEndpointId = `${accessory.uuid}-part-${part.id}`;
1269
+ // Register the part endpoint mapping for handler context
1270
+ this.behaviorRegistry.registerPartEndpoint(partEndpointId, accessory.uuid, part.id);
1271
+ // Apply custom behaviors to part based on its handlers (same logic as main accessory)
1272
+ let partDeviceType = part.deviceType;
1273
+ const partCustomBehaviors = [];
1274
+ if (part.handlers) {
1275
+ // Use the static cluster behavior map for parts as well
1276
+ const partBehaviorMap = MatterServer.CLUSTER_BEHAVIOR_MAP;
1277
+ for (const clusterName of Object.keys(part.handlers)) {
1278
+ const behaviorClass = partBehaviorMap[clusterName];
1012
1279
  if (behaviorClass) {
1013
- customBehaviors.push(behaviorClass);
1014
- log.info(`Will use ${behaviorClass.name} for ${accessory.displayName}`);
1280
+ partCustomBehaviors.push(behaviorClass);
1281
+ log.info(` Will use ${behaviorClass.name} for part ${part.id}`);
1015
1282
  }
1016
1283
  else {
1017
- log.warn(`No custom behavior class available for cluster '${clusterName}' - handlers will be registered but may not be called`);
1284
+ log.warn(`No custom behavior class available for cluster '${clusterName}' on part ${part.id}`);
1018
1285
  }
1019
1286
  }
1020
- if (customBehaviors.length > 0) {
1021
- // Cast to any to bypass TypeScript limitations
1022
- deviceType = deviceType.with(...customBehaviors);
1023
- log.info(`Applied ${customBehaviors.length} custom behavior(s) to device type`);
1287
+ if (partCustomBehaviors.length > 0) {
1288
+ // Add custom behaviors to part device type
1289
+ partDeviceType = withBehaviors(partDeviceType, partCustomBehaviors);
1290
+ log.info(` Applied ${partCustomBehaviors.length} custom behavior(s) to part ${part.id}`);
1024
1291
  }
1025
1292
  }
1026
- // Add BridgedDeviceBasicInformationServer for bridged devices only
1027
- // This is required by the Matter spec for devices behind an aggregator
1028
- // External accessories should NOT have this cluster
1293
+ // Add BridgedDeviceBasicInformationServer for bridged parts
1029
1294
  if (!this.config.externalAccessory) {
1030
- deviceType = deviceType.with(BridgedDeviceBasicInformationServer);
1031
- log.debug(`Added BridgedDeviceBasicInformationServer to ${accessory.displayName}`);
1295
+ partDeviceType = withBehaviors(partDeviceType, [BridgedDeviceBasicInformationServer]);
1032
1296
  }
1033
- // Create endpoint with the modified device type AND initial cluster states
1034
- // IMPORTANT: Cluster states must be provided at creation time, before adding to aggregator
1035
- // This ensures mandatory attributes are set before Matter.js validates the endpoint
1036
- const endpointOptions = {
1037
- id: accessory.uuid,
1038
- ...accessory.clusters, // Spread cluster states as initial values
1297
+ // Create endpoint options with cluster states
1298
+ const partEndpointOptions = {
1299
+ id: partEndpointId,
1300
+ ...part.clusters,
1039
1301
  };
1040
- // Add bridgedDeviceBasicInformation cluster only for bridged devices
1041
- // For external accessories, use the root basicInformation instead
1302
+ // Add bridgedDeviceBasicInformation for the part
1042
1303
  if (!this.config.externalAccessory) {
1043
- endpointOptions.bridgedDeviceBasicInformation = {
1304
+ partEndpointOptions.bridgedDeviceBasicInformation = {
1044
1305
  vendorName: accessory.manufacturer,
1045
- nodeLabel: accessory.displayName, // Main end user name for the device
1306
+ nodeLabel: part.displayName || `${accessory.displayName} - ${part.id}`,
1046
1307
  productName: accessory.model,
1047
- productLabel: accessory.displayName,
1048
- serialNumber: accessory.serialNumber,
1308
+ productLabel: part.displayName || part.id,
1309
+ serialNumber: `${accessory.serialNumber}-${part.id}`,
1049
1310
  reachable: true,
1050
1311
  };
1051
1312
  }
1052
- const endpoint = new Endpoint(deviceType, endpointOptions);
1053
- if (this.config.debugModeEnabled) {
1054
- log.debug(`Created endpoint for ${accessory.displayName} with initial cluster states`);
1055
- }
1056
- // Add to aggregator or serverNode depending on mode
1057
- // (validation happens here - cluster states must already be set)
1313
+ // Create the part endpoint
1314
+ const partEndpoint = new Endpoint(partDeviceType, partEndpointOptions);
1315
+ // Add part endpoint to aggregator or serverNode
1058
1316
  if (this.config.externalAccessory) {
1059
- // In external accessory mode, add device directly to serverNode as root endpoint
1060
- await this.serverNode.add(endpoint);
1061
- log.debug(`Added ${accessory.displayName} as external accessory to ServerNode`);
1317
+ await this.serverNode.add(partEndpoint);
1062
1318
  }
1063
1319
  else {
1064
- // In bridge mode, add to aggregator
1065
- await this.aggregator.add(endpoint);
1066
- if (this.config.debugModeEnabled) {
1067
- log.debug(`Added endpoint for ${accessory.displayName} to aggregator`);
1068
- }
1320
+ await this.aggregator.add(partEndpoint);
1069
1321
  }
1070
- // Set up command handlers if provided
1071
- if (accessory.handlers) {
1072
- log.info(`Setting up handlers for accessory ${accessory.uuid}`);
1073
- // Register handlers with the custom behavior classes
1074
- for (const [clusterName, handlers] of Object.entries(accessory.handlers)) {
1075
- log.info(` Processing cluster: ${clusterName}`);
1322
+ log.info(` Created part endpoint: ${part.displayName || part.id} (${partEndpointId})`);
1323
+ // Set up handlers for this part
1324
+ if (part.handlers) {
1325
+ for (const [clusterName, handlers] of Object.entries(part.handlers)) {
1076
1326
  for (const [commandName, handler] of Object.entries(handlers)) {
1077
- registerHandler(accessory.uuid, clusterName, commandName, handler);
1078
- }
1079
- }
1080
- }
1081
- // Create and register parts (child endpoints) if provided
1082
- const internalParts = [];
1083
- if (accessory.parts && accessory.parts.length > 0) {
1084
- log.info(`Creating ${accessory.parts.length} child endpoint(s) for ${accessory.displayName}`);
1085
- for (const part of accessory.parts) {
1086
- // Create unique endpoint ID for this part
1087
- const partEndpointId = `${accessory.uuid}-part-${part.id}`;
1088
- // Register the part endpoint mapping for handler context
1089
- registerPartEndpoint(partEndpointId, accessory.uuid, part.id);
1090
- // Apply custom behaviors to part based on its handlers (same logic as main accessory)
1091
- let partDeviceType = part.deviceType;
1092
- const partCustomBehaviors = [];
1093
- if (part.handlers) {
1094
- // Map cluster names to custom behavior classes for parts
1095
- const partBehaviorMap = {
1096
- onOff: HomebridgeOnOffServer,
1097
- levelControl: HomebridgeLevelControlServer,
1098
- colorControl: HomebridgeColorControlServer,
1099
- windowCovering: HomebridgeWindowCoveringServer,
1100
- doorLock: HomebridgeDoorLockServer,
1101
- fanControl: HomebridgeFanControlServer,
1102
- thermostat: HomebridgeThermostatServer,
1103
- identify: HomebridgeIdentifyServer,
1104
- };
1105
- for (const clusterName of Object.keys(part.handlers)) {
1106
- const behaviorClass = partBehaviorMap[clusterName];
1107
- if (behaviorClass) {
1108
- partCustomBehaviors.push(behaviorClass);
1109
- log.info(` Will use ${behaviorClass.name} for part ${part.id}`);
1110
- }
1111
- else {
1112
- log.warn(`No custom behavior class available for cluster '${clusterName}' on part ${part.id}`);
1113
- }
1114
- }
1115
- if (partCustomBehaviors.length > 0) {
1116
- // Matter.js device types support .with() to add behaviors
1117
- partDeviceType = partDeviceType.with(...partCustomBehaviors);
1118
- log.info(` Applied ${partCustomBehaviors.length} custom behavior(s) to part ${part.id}`);
1119
- }
1120
- }
1121
- // Add BridgedDeviceBasicInformationServer for bridged parts
1122
- if (!this.config.externalAccessory) {
1123
- // Matter.js device types support .with() to add behaviors
1124
- partDeviceType = partDeviceType.with(BridgedDeviceBasicInformationServer);
1125
- }
1126
- // Create endpoint options with cluster states
1127
- const partEndpointOptions = {
1128
- id: partEndpointId,
1129
- ...part.clusters,
1130
- };
1131
- // Add bridgedDeviceBasicInformation for the part
1132
- if (!this.config.externalAccessory) {
1133
- partEndpointOptions.bridgedDeviceBasicInformation = {
1134
- vendorName: accessory.manufacturer,
1135
- nodeLabel: part.displayName || `${accessory.displayName} - ${part.id}`,
1136
- productName: accessory.model,
1137
- productLabel: part.displayName || part.id,
1138
- serialNumber: `${accessory.serialNumber}-${part.id}`,
1139
- reachable: true,
1140
- };
1141
- }
1142
- // Create the part endpoint
1143
- const partEndpoint = new Endpoint(partDeviceType, partEndpointOptions);
1144
- // Add part endpoint to aggregator or serverNode
1145
- if (this.config.externalAccessory) {
1146
- await this.serverNode.add(partEndpoint);
1147
- }
1148
- else {
1149
- await this.aggregator.add(partEndpoint);
1150
- }
1151
- log.info(` Created part endpoint: ${part.displayName || part.id} (${partEndpointId})`);
1152
- // Set up handlers for this part
1153
- if (part.handlers) {
1154
- for (const [clusterName, handlers] of Object.entries(part.handlers)) {
1155
- for (const [commandName, handler] of Object.entries(handlers)) {
1156
- // Register handler with the part's endpoint ID
1157
- registerHandler(partEndpointId, clusterName, commandName, handler);
1158
- }
1159
- }
1160
- log.debug(` Registered ${Object.keys(part.handlers).length} handler(s) for part ${part.id}`);
1327
+ // Register handler with the part's endpoint ID
1328
+ this.behaviorRegistry.registerHandler(partEndpointId, clusterName, commandName, handler);
1161
1329
  }
1162
- // Store the internal part
1163
- internalParts.push({
1164
- ...part,
1165
- endpoint: partEndpoint,
1166
- });
1167
1330
  }
1331
+ log.debug(` Registered ${Object.keys(part.handlers).length} handler(s) for part ${part.id}`);
1168
1332
  }
1169
- // Store accessory with internal metadata and event emitter
1170
- // The event emitter allows plugins to listen for lifecycle events (ready, commissioned, decommissioned)
1171
- const internalAccessory = {
1172
- ...accessory,
1173
- _associatedPlugin: pluginIdentifier,
1174
- _associatedPlatform: platformName,
1175
- endpoint,
1176
- registered: true,
1177
- _parts: internalParts.length > 0 ? internalParts : undefined,
1178
- _eventEmitter: new EventEmitter(),
1179
- };
1180
- this.accessories.set(accessory.uuid, internalAccessory);
1181
- log.info(`Registered Matter accessory: ${accessory.displayName} (${accessory.uuid})`);
1182
- if (this.config.debugModeEnabled) {
1183
- log.debug(`Total registered accessories: ${this.accessories.size}/${this.MAX_DEVICES}`);
1184
- }
1185
- // Emit accessory-registered event
1186
- this.emit('accessory-registered', accessory);
1187
- // Notify controllers about the new device (parts list changed)
1188
- // This allows the Home app to discover new devices without re-pairing
1189
- await this.notifyPartsListChanged();
1190
- // Save to cache asynchronously (don't block registration)
1191
- if (this.accessoryCache) {
1192
- this.accessoryCache.save(this.accessories).catch((error) => {
1193
- log.warn('Failed to save accessory cache:', error);
1194
- });
1195
- }
1333
+ // Store the internal part
1334
+ internalParts.push({
1335
+ ...part,
1336
+ endpoint: partEndpoint,
1337
+ });
1196
1338
  }
1197
- catch (error) {
1198
- log.error(`Failed to register Matter accessory ${accessory.displayName}:`, error);
1199
- throw new MatterDeviceError(`Failed to register accessory: ${error}`);
1339
+ return internalParts;
1340
+ }
1341
+ /**
1342
+ * Finalize accessory registration (store, emit events, save cache)
1343
+ */
1344
+ async finalizeAccessoryRegistration(accessory, endpoint, internalParts) {
1345
+ // Store accessory with internal metadata and event emitter
1346
+ // The event emitter allows plugins to listen for lifecycle events (ready, commissioned, decommissioned)
1347
+ // Note: _associatedPlugin and _associatedPlatform are already set by MatterAPIImpl
1348
+ const internalAccessory = {
1349
+ ...accessory,
1350
+ endpoint,
1351
+ registered: true,
1352
+ _parts: internalParts.length > 0 ? internalParts : undefined,
1353
+ _eventEmitter: new EventEmitter(),
1354
+ };
1355
+ this.accessories.set(accessory.uuid, internalAccessory);
1356
+ log.info(`Registered Matter accessory: ${accessory.displayName} (${accessory.uuid})`);
1357
+ if (this.config.debugModeEnabled) {
1358
+ log.debug(`Total registered accessories: ${this.accessories.size}/${this.MAX_DEVICES}`);
1359
+ }
1360
+ // Emit accessory-registered event
1361
+ this.emit('accessory-registered', accessory);
1362
+ // Notify controllers about the new device (parts list changed)
1363
+ // This allows the Home app to discover new devices without re-pairing
1364
+ await this.notifyPartsListChanged();
1365
+ // Request debounced save to cache (reduces disk I/O during rapid registration)
1366
+ if (this.accessoryCache) {
1367
+ this.accessoryCache.requestSave(this.accessories);
1200
1368
  }
1201
1369
  }
1202
1370
  /**
@@ -1211,9 +1379,7 @@ export class MatterServer extends EventEmitter {
1211
1379
  if (this.accessoryCache && this.accessoryCache.getCached(uuid)) {
1212
1380
  log.debug(`Removing ${uuid} from cache`);
1213
1381
  this.accessoryCache.removeCached(uuid);
1214
- this.accessoryCache.save(this.accessories).catch((error) => {
1215
- log.warn('Failed to save accessory cache:', error);
1216
- });
1382
+ this.accessoryCache.requestSave(this.accessories);
1217
1383
  }
1218
1384
  return;
1219
1385
  }
@@ -1231,9 +1397,7 @@ export class MatterServer extends EventEmitter {
1231
1397
  // Update cache (remove the accessory)
1232
1398
  if (this.accessoryCache) {
1233
1399
  this.accessoryCache.removeCached(uuid);
1234
- this.accessoryCache.save(this.accessories).catch((error) => {
1235
- log.warn('Failed to save accessory cache:', error);
1236
- });
1400
+ this.accessoryCache.requestSave(this.accessories);
1237
1401
  }
1238
1402
  }
1239
1403
  catch (error) {
@@ -1281,10 +1445,10 @@ export class MatterServer extends EventEmitter {
1281
1445
  displayName = accessory.displayName;
1282
1446
  }
1283
1447
  // Defer the update to avoid "read-only transaction" errors when called from handlers
1284
- // Matter.js uses transactions, and we need to wait until the transaction fully completes
1285
- // Use setTimeout with a delay to ensure we're completely outside the transaction
1448
+ // Matter.js uses transactions, and we need to escape the current call stack
1449
+ // setImmediate ensures we're in a new event loop tick without arbitrary delays
1286
1450
  return new Promise((resolve, reject) => {
1287
- setTimeout(async () => {
1451
+ setImmediate(async () => {
1288
1452
  try {
1289
1453
  // Construct the update object
1290
1454
  const updateObject = { [cluster]: attributes };
@@ -1313,7 +1477,7 @@ export class MatterServer extends EventEmitter {
1313
1477
  log.error(`Failed to update state for accessory ${uuid}${partInfo}:`, error);
1314
1478
  reject(new MatterDeviceError(`Failed to update accessory state: ${error}`));
1315
1479
  }
1316
- }, 50); // 50ms delay to ensure we're completely outside the transaction
1480
+ });
1317
1481
  });
1318
1482
  }
1319
1483
  /**
@@ -1452,7 +1616,6 @@ export class MatterServer extends EventEmitter {
1452
1616
  this.isRunning = false;
1453
1617
  // Stop monitoring
1454
1618
  this.stopFabricMonitoring();
1455
- networkMonitor.stopMonitoring();
1456
1619
  try {
1457
1620
  // Save accessory cache before shutting down (BEFORE clearing accessories!)
1458
1621
  if (this.accessoryCache && this.accessories.size > 0) {
@@ -1508,6 +1671,23 @@ export class MatterServer extends EventEmitter {
1508
1671
  }
1509
1672
  /**
1510
1673
  * Get fabric information for commissioned controllers
1674
+ *
1675
+ * Returns information about each paired controller (fabric) including:
1676
+ * - fabricIndex: Unique identifier for the fabric
1677
+ * - fabricId: 64-bit fabric identifier
1678
+ * - nodeId: Node identifier within the fabric
1679
+ * - rootVendorId: Vendor ID of the root node
1680
+ * - label: Optional human-readable label
1681
+ *
1682
+ * @returns Array of fabric information objects, or empty array if no fabrics are commissioned
1683
+ * @example
1684
+ * ```typescript
1685
+ * const fabrics = matterServer.getFabricInfo()
1686
+ * console.log(`Commissioned to ${fabrics.length} controller(s)`)
1687
+ * fabrics.forEach(fabric => {
1688
+ * console.log(` Fabric ${fabric.fabricIndex}: ${fabric.label || 'Unnamed'}`)
1689
+ * })
1690
+ * ```
1511
1691
  */
1512
1692
  getFabricInfo() {
1513
1693
  try {
@@ -1641,8 +1821,11 @@ export class MatterServer extends EventEmitter {
1641
1821
  // The fabric monitoring will detect this change and emit the appropriate events
1642
1822
  }
1643
1823
  catch (error) {
1824
+ const errorMessage = error instanceof Error ? error.message : String(error);
1644
1825
  log.error(`Failed to remove fabric ${fabricIndex}:`, error);
1645
- throw new MatterDeviceError(`Failed to remove fabric: ${error.message}`, error);
1826
+ throw new MatterDeviceError(`Failed to remove fabric: ${errorMessage}`, {
1827
+ originalError: error instanceof Error ? error : undefined,
1828
+ });
1646
1829
  }
1647
1830
  }
1648
1831
  /**
@@ -1685,8 +1868,9 @@ export class MatterServer extends EventEmitter {
1685
1868
  }
1686
1869
  catch (error) {
1687
1870
  // Non-fatal error - log but don't throw
1688
- log.warn(`Failed to notify controllers of parts list change: ${error.message}`);
1871
+ const errorMessage = error instanceof Error ? error.message : String(error);
1872
+ log.warn(`Failed to notify controllers of parts list change: ${errorMessage}`);
1689
1873
  }
1690
1874
  }
1691
1875
  }
1692
- //# sourceMappingURL=matterServer.js.map
1876
+ //# sourceMappingURL=server.js.map