homebridge 2.0.0-beta.4 → 2.0.0-beta.40

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 (359) hide show
  1. package/README.md +16 -17
  2. package/bin/homebridge.js +22 -0
  3. package/config-sample.json +3 -3
  4. package/dist/api.d.ts +499 -0
  5. package/dist/api.d.ts.map +1 -0
  6. package/dist/api.js +223 -0
  7. package/dist/api.js.map +1 -0
  8. package/dist/api.spec.d.ts +2 -0
  9. package/dist/api.spec.d.ts.map +1 -0
  10. package/dist/api.spec.js +413 -0
  11. package/dist/api.spec.js.map +1 -0
  12. package/{lib → dist}/bridgeService.d.ts +19 -10
  13. package/dist/bridgeService.d.ts.map +1 -0
  14. package/{lib → dist}/bridgeService.js +85 -117
  15. package/dist/bridgeService.js.map +1 -0
  16. package/{lib → dist}/childBridgeFork.d.ts +11 -5
  17. package/dist/childBridgeFork.d.ts.map +1 -0
  18. package/{lib → dist}/childBridgeFork.js +93 -44
  19. package/dist/childBridgeFork.js.map +1 -0
  20. package/{lib → dist}/childBridgeService.d.ts +37 -7
  21. package/dist/childBridgeService.d.ts.map +1 -0
  22. package/{lib → dist}/childBridgeService.js +127 -69
  23. package/dist/childBridgeService.js.map +1 -0
  24. package/dist/cli.d.ts +3 -0
  25. package/dist/cli.d.ts.map +1 -0
  26. package/dist/cli.js +90 -0
  27. package/dist/cli.js.map +1 -0
  28. package/dist/externalPortService.d.ts +47 -0
  29. package/dist/externalPortService.d.ts.map +1 -0
  30. package/dist/externalPortService.js +105 -0
  31. package/dist/externalPortService.js.map +1 -0
  32. package/dist/index.d.ts +141 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +53 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/ipcService.d.ts +48 -0
  37. package/dist/ipcService.d.ts.map +1 -0
  38. package/{lib → dist}/ipcService.js +12 -14
  39. package/dist/ipcService.js.map +1 -0
  40. package/{lib → dist}/logger.d.ts +12 -6
  41. package/dist/logger.d.ts.map +1 -0
  42. package/{lib → dist}/logger.js +27 -28
  43. package/dist/logger.js.map +1 -0
  44. package/dist/logger.spec.d.ts +2 -0
  45. package/dist/logger.spec.d.ts.map +1 -0
  46. package/dist/logger.spec.js +95 -0
  47. package/dist/logger.spec.js.map +1 -0
  48. package/dist/matter/ChildBridgeMatterManager.d.ts +96 -0
  49. package/dist/matter/ChildBridgeMatterManager.d.ts.map +1 -0
  50. package/dist/matter/ChildBridgeMatterManager.js +399 -0
  51. package/dist/matter/ChildBridgeMatterManager.js.map +1 -0
  52. package/dist/matter/ExternalMatterAccessoryPublisher.d.ts +48 -0
  53. package/dist/matter/ExternalMatterAccessoryPublisher.d.ts.map +1 -0
  54. package/dist/matter/ExternalMatterAccessoryPublisher.js +73 -0
  55. package/dist/matter/ExternalMatterAccessoryPublisher.js.map +1 -0
  56. package/dist/matter/ExternalMatterAccessoryPublisher.spec.d.ts +2 -0
  57. package/dist/matter/ExternalMatterAccessoryPublisher.spec.d.ts.map +1 -0
  58. package/dist/matter/ExternalMatterAccessoryPublisher.spec.js +293 -0
  59. package/dist/matter/ExternalMatterAccessoryPublisher.spec.js.map +1 -0
  60. package/dist/matter/MatterAPIImpl.d.ts +504 -0
  61. package/dist/matter/MatterAPIImpl.d.ts.map +1 -0
  62. package/dist/matter/MatterAPIImpl.js +305 -0
  63. package/dist/matter/MatterAPIImpl.js.map +1 -0
  64. package/dist/matter/MatterBridgeManager.d.ts +87 -0
  65. package/dist/matter/MatterBridgeManager.d.ts.map +1 -0
  66. package/dist/matter/MatterBridgeManager.js +413 -0
  67. package/dist/matter/MatterBridgeManager.js.map +1 -0
  68. package/dist/matter/MatterConfigCollector.d.ts +26 -0
  69. package/dist/matter/MatterConfigCollector.d.ts.map +1 -0
  70. package/dist/matter/MatterConfigCollector.js +78 -0
  71. package/dist/matter/MatterConfigCollector.js.map +1 -0
  72. package/dist/matter/accessoryCache.d.ts +105 -0
  73. package/dist/matter/accessoryCache.d.ts.map +1 -0
  74. package/dist/matter/accessoryCache.js +214 -0
  75. package/dist/matter/accessoryCache.js.map +1 -0
  76. package/dist/matter/accessoryCache.spec.d.ts +2 -0
  77. package/dist/matter/accessoryCache.spec.d.ts.map +1 -0
  78. package/dist/matter/accessoryCache.spec.js +452 -0
  79. package/dist/matter/accessoryCache.spec.js.map +1 -0
  80. package/dist/matter/behaviors/BehaviorRegistry.d.ts +65 -0
  81. package/dist/matter/behaviors/BehaviorRegistry.d.ts.map +1 -0
  82. package/dist/matter/behaviors/BehaviorRegistry.js +139 -0
  83. package/dist/matter/behaviors/BehaviorRegistry.js.map +1 -0
  84. package/dist/matter/behaviors/BehaviorRegistry.spec.d.ts +2 -0
  85. package/dist/matter/behaviors/BehaviorRegistry.spec.d.ts.map +1 -0
  86. package/dist/matter/behaviors/BehaviorRegistry.spec.js +307 -0
  87. package/dist/matter/behaviors/BehaviorRegistry.spec.js.map +1 -0
  88. package/dist/matter/behaviors/ColorControlBehavior.d.ts +63 -0
  89. package/dist/matter/behaviors/ColorControlBehavior.d.ts.map +1 -0
  90. package/dist/matter/behaviors/ColorControlBehavior.js +223 -0
  91. package/dist/matter/behaviors/ColorControlBehavior.js.map +1 -0
  92. package/dist/matter/behaviors/ColorControlBehavior.spec.d.ts +2 -0
  93. package/dist/matter/behaviors/ColorControlBehavior.spec.d.ts.map +1 -0
  94. package/dist/matter/behaviors/ColorControlBehavior.spec.js +29 -0
  95. package/dist/matter/behaviors/ColorControlBehavior.spec.js.map +1 -0
  96. package/dist/matter/behaviors/DoorLockBehavior.d.ts +20 -0
  97. package/dist/matter/behaviors/DoorLockBehavior.d.ts.map +1 -0
  98. package/dist/matter/behaviors/DoorLockBehavior.js +71 -0
  99. package/dist/matter/behaviors/DoorLockBehavior.js.map +1 -0
  100. package/dist/matter/behaviors/DoorLockBehavior.spec.d.ts +2 -0
  101. package/dist/matter/behaviors/DoorLockBehavior.spec.d.ts.map +1 -0
  102. package/dist/matter/behaviors/DoorLockBehavior.spec.js +120 -0
  103. package/dist/matter/behaviors/DoorLockBehavior.spec.js.map +1 -0
  104. package/dist/matter/behaviors/FanControlBehavior.d.ts +20 -0
  105. package/dist/matter/behaviors/FanControlBehavior.d.ts.map +1 -0
  106. package/dist/matter/behaviors/FanControlBehavior.js +71 -0
  107. package/dist/matter/behaviors/FanControlBehavior.js.map +1 -0
  108. package/dist/matter/behaviors/FanControlBehavior.spec.d.ts +2 -0
  109. package/dist/matter/behaviors/FanControlBehavior.spec.d.ts.map +1 -0
  110. package/dist/matter/behaviors/FanControlBehavior.spec.js +23 -0
  111. package/dist/matter/behaviors/FanControlBehavior.spec.js.map +1 -0
  112. package/dist/matter/behaviors/IdentifyBehavior.d.ts +20 -0
  113. package/dist/matter/behaviors/IdentifyBehavior.d.ts.map +1 -0
  114. package/dist/matter/behaviors/IdentifyBehavior.js +41 -0
  115. package/dist/matter/behaviors/IdentifyBehavior.js.map +1 -0
  116. package/dist/matter/behaviors/IdentifyBehavior.spec.d.ts +2 -0
  117. package/dist/matter/behaviors/IdentifyBehavior.spec.d.ts.map +1 -0
  118. package/dist/matter/behaviors/IdentifyBehavior.spec.js +64 -0
  119. package/dist/matter/behaviors/IdentifyBehavior.spec.js.map +1 -0
  120. package/dist/matter/behaviors/LevelControlBehavior.d.ts +33 -0
  121. package/dist/matter/behaviors/LevelControlBehavior.d.ts.map +1 -0
  122. package/dist/matter/behaviors/LevelControlBehavior.js +138 -0
  123. package/dist/matter/behaviors/LevelControlBehavior.js.map +1 -0
  124. package/dist/matter/behaviors/LevelControlBehavior.spec.d.ts +2 -0
  125. package/dist/matter/behaviors/LevelControlBehavior.spec.d.ts.map +1 -0
  126. package/dist/matter/behaviors/LevelControlBehavior.spec.js +145 -0
  127. package/dist/matter/behaviors/LevelControlBehavior.spec.js.map +1 -0
  128. package/dist/matter/behaviors/OnOffBehavior.d.ts +27 -0
  129. package/dist/matter/behaviors/OnOffBehavior.d.ts.map +1 -0
  130. package/dist/matter/behaviors/OnOffBehavior.js +95 -0
  131. package/dist/matter/behaviors/OnOffBehavior.js.map +1 -0
  132. package/dist/matter/behaviors/OnOffBehavior.spec.d.ts +2 -0
  133. package/dist/matter/behaviors/OnOffBehavior.spec.d.ts.map +1 -0
  134. package/dist/matter/behaviors/OnOffBehavior.spec.js +128 -0
  135. package/dist/matter/behaviors/OnOffBehavior.spec.js.map +1 -0
  136. package/dist/matter/behaviors/RvcCleanModeBehavior.d.ts +19 -0
  137. package/dist/matter/behaviors/RvcCleanModeBehavior.d.ts.map +1 -0
  138. package/dist/matter/behaviors/RvcCleanModeBehavior.js +41 -0
  139. package/dist/matter/behaviors/RvcCleanModeBehavior.js.map +1 -0
  140. package/dist/matter/behaviors/RvcCleanModeBehavior.spec.d.ts +2 -0
  141. package/dist/matter/behaviors/RvcCleanModeBehavior.spec.d.ts.map +1 -0
  142. package/dist/matter/behaviors/RvcCleanModeBehavior.spec.js +57 -0
  143. package/dist/matter/behaviors/RvcCleanModeBehavior.spec.js.map +1 -0
  144. package/dist/matter/behaviors/RvcOperationalStateBehavior.d.ts +22 -0
  145. package/dist/matter/behaviors/RvcOperationalStateBehavior.d.ts.map +1 -0
  146. package/dist/matter/behaviors/RvcOperationalStateBehavior.js +86 -0
  147. package/dist/matter/behaviors/RvcOperationalStateBehavior.js.map +1 -0
  148. package/dist/matter/behaviors/RvcOperationalStateBehavior.spec.d.ts +2 -0
  149. package/dist/matter/behaviors/RvcOperationalStateBehavior.spec.d.ts.map +1 -0
  150. package/dist/matter/behaviors/RvcOperationalStateBehavior.spec.js +55 -0
  151. package/dist/matter/behaviors/RvcOperationalStateBehavior.spec.js.map +1 -0
  152. package/dist/matter/behaviors/RvcRunModeBehavior.d.ts +19 -0
  153. package/dist/matter/behaviors/RvcRunModeBehavior.d.ts.map +1 -0
  154. package/dist/matter/behaviors/RvcRunModeBehavior.js +41 -0
  155. package/dist/matter/behaviors/RvcRunModeBehavior.js.map +1 -0
  156. package/dist/matter/behaviors/RvcRunModeBehavior.spec.d.ts +2 -0
  157. package/dist/matter/behaviors/RvcRunModeBehavior.spec.d.ts.map +1 -0
  158. package/dist/matter/behaviors/RvcRunModeBehavior.spec.js +57 -0
  159. package/dist/matter/behaviors/RvcRunModeBehavior.spec.js.map +1 -0
  160. package/dist/matter/behaviors/ServiceAreaBehavior.d.ts +21 -0
  161. package/dist/matter/behaviors/ServiceAreaBehavior.d.ts.map +1 -0
  162. package/dist/matter/behaviors/ServiceAreaBehavior.js +61 -0
  163. package/dist/matter/behaviors/ServiceAreaBehavior.js.map +1 -0
  164. package/dist/matter/behaviors/ServiceAreaBehavior.spec.d.ts +2 -0
  165. package/dist/matter/behaviors/ServiceAreaBehavior.spec.d.ts.map +1 -0
  166. package/dist/matter/behaviors/ServiceAreaBehavior.spec.js +53 -0
  167. package/dist/matter/behaviors/ServiceAreaBehavior.spec.js.map +1 -0
  168. package/dist/matter/behaviors/ThermostatBehavior.d.ts +22 -0
  169. package/dist/matter/behaviors/ThermostatBehavior.d.ts.map +1 -0
  170. package/dist/matter/behaviors/ThermostatBehavior.js +127 -0
  171. package/dist/matter/behaviors/ThermostatBehavior.js.map +1 -0
  172. package/dist/matter/behaviors/ThermostatBehavior.spec.d.ts +2 -0
  173. package/dist/matter/behaviors/ThermostatBehavior.spec.d.ts.map +1 -0
  174. package/dist/matter/behaviors/ThermostatBehavior.spec.js +23 -0
  175. package/dist/matter/behaviors/ThermostatBehavior.spec.js.map +1 -0
  176. package/dist/matter/behaviors/WindowCoveringBehavior.d.ts +31 -0
  177. package/dist/matter/behaviors/WindowCoveringBehavior.d.ts.map +1 -0
  178. package/dist/matter/behaviors/WindowCoveringBehavior.js +158 -0
  179. package/dist/matter/behaviors/WindowCoveringBehavior.js.map +1 -0
  180. package/dist/matter/behaviors/WindowCoveringBehavior.spec.d.ts +2 -0
  181. package/dist/matter/behaviors/WindowCoveringBehavior.spec.d.ts.map +1 -0
  182. package/dist/matter/behaviors/WindowCoveringBehavior.spec.js +27 -0
  183. package/dist/matter/behaviors/WindowCoveringBehavior.spec.js.map +1 -0
  184. package/dist/matter/behaviors/index.d.ts +20 -0
  185. package/dist/matter/behaviors/index.d.ts.map +1 -0
  186. package/dist/matter/behaviors/index.js +21 -0
  187. package/dist/matter/behaviors/index.js.map +1 -0
  188. package/dist/matter/configValidator.d.ts +81 -0
  189. package/dist/matter/configValidator.d.ts.map +1 -0
  190. package/dist/matter/configValidator.js +240 -0
  191. package/dist/matter/configValidator.js.map +1 -0
  192. package/dist/matter/configValidator.spec.d.ts +2 -0
  193. package/dist/matter/configValidator.spec.d.ts.map +1 -0
  194. package/dist/matter/configValidator.spec.js +390 -0
  195. package/dist/matter/configValidator.spec.js.map +1 -0
  196. package/dist/matter/errorHandler.d.ts +33 -0
  197. package/dist/matter/errorHandler.d.ts.map +1 -0
  198. package/dist/matter/errorHandler.js +113 -0
  199. package/dist/matter/errorHandler.js.map +1 -0
  200. package/dist/matter/errorHandler.spec.d.ts +2 -0
  201. package/dist/matter/errorHandler.spec.d.ts.map +1 -0
  202. package/dist/matter/errorHandler.spec.js +159 -0
  203. package/dist/matter/errorHandler.spec.js.map +1 -0
  204. package/dist/matter/errors.d.ts +178 -0
  205. package/dist/matter/errors.d.ts.map +1 -0
  206. package/dist/matter/errors.js +200 -0
  207. package/dist/matter/errors.js.map +1 -0
  208. package/dist/matter/index.d.ts +127 -0
  209. package/dist/matter/index.d.ts.map +1 -0
  210. package/dist/matter/index.js +23 -0
  211. package/dist/matter/index.js.map +1 -0
  212. package/dist/matter/logFormatter.d.ts +19 -0
  213. package/dist/matter/logFormatter.d.ts.map +1 -0
  214. package/dist/matter/logFormatter.js +158 -0
  215. package/dist/matter/logFormatter.js.map +1 -0
  216. package/dist/matter/logFormatter.spec.d.ts +2 -0
  217. package/dist/matter/logFormatter.spec.d.ts.map +1 -0
  218. package/dist/matter/logFormatter.spec.js +252 -0
  219. package/dist/matter/logFormatter.spec.js.map +1 -0
  220. package/dist/matter/server.d.ts +336 -0
  221. package/dist/matter/server.d.ts.map +1 -0
  222. package/dist/matter/server.js +1810 -0
  223. package/dist/matter/server.js.map +1 -0
  224. package/dist/matter/serverHelpers.d.ts +81 -0
  225. package/dist/matter/serverHelpers.d.ts.map +1 -0
  226. package/dist/matter/serverHelpers.js +326 -0
  227. package/dist/matter/serverHelpers.js.map +1 -0
  228. package/dist/matter/serverHelpers.spec.d.ts +2 -0
  229. package/dist/matter/serverHelpers.spec.d.ts.map +1 -0
  230. package/dist/matter/serverHelpers.spec.js +521 -0
  231. package/dist/matter/serverHelpers.spec.js.map +1 -0
  232. package/dist/matter/sharedTypes.d.ts +164 -0
  233. package/dist/matter/sharedTypes.d.ts.map +1 -0
  234. package/dist/matter/sharedTypes.js +46 -0
  235. package/dist/matter/sharedTypes.js.map +1 -0
  236. package/dist/matter/storage.d.ts +135 -0
  237. package/dist/matter/storage.d.ts.map +1 -0
  238. package/dist/matter/storage.js +424 -0
  239. package/dist/matter/storage.js.map +1 -0
  240. package/dist/matter/storage.spec.d.ts +2 -0
  241. package/dist/matter/storage.spec.d.ts.map +1 -0
  242. package/dist/matter/storage.spec.js +570 -0
  243. package/dist/matter/storage.spec.js.map +1 -0
  244. package/dist/matter/typeHelpers.d.ts +45 -0
  245. package/dist/matter/typeHelpers.d.ts.map +1 -0
  246. package/dist/matter/typeHelpers.js +57 -0
  247. package/dist/matter/typeHelpers.js.map +1 -0
  248. package/dist/matter/typeHelpers.spec.d.ts +2 -0
  249. package/dist/matter/typeHelpers.spec.d.ts.map +1 -0
  250. package/dist/matter/typeHelpers.spec.js +127 -0
  251. package/dist/matter/typeHelpers.spec.js.map +1 -0
  252. package/dist/matter/types.d.ts +826 -0
  253. package/dist/matter/types.d.ts.map +1 -0
  254. package/dist/matter/types.js +204 -0
  255. package/dist/matter/types.js.map +1 -0
  256. package/{lib → dist}/platformAccessory.d.ts +8 -6
  257. package/dist/platformAccessory.d.ts.map +1 -0
  258. package/{lib → dist}/platformAccessory.js +19 -16
  259. package/dist/platformAccessory.js.map +1 -0
  260. package/dist/platformAccessory.spec.d.ts +2 -0
  261. package/dist/platformAccessory.spec.d.ts.map +1 -0
  262. package/dist/platformAccessory.spec.js +126 -0
  263. package/dist/platformAccessory.spec.js.map +1 -0
  264. package/{lib → dist}/plugin.d.ts +2 -3
  265. package/dist/plugin.d.ts.map +1 -0
  266. package/{lib → dist}/plugin.js +39 -51
  267. package/dist/plugin.js.map +1 -0
  268. package/{lib → dist}/pluginManager.d.ts +3 -3
  269. package/dist/pluginManager.d.ts.map +1 -0
  270. package/{lib → dist}/pluginManager.js +76 -81
  271. package/dist/pluginManager.js.map +1 -0
  272. package/dist/pluginManager.spec.d.ts +2 -0
  273. package/dist/pluginManager.spec.d.ts.map +1 -0
  274. package/dist/pluginManager.spec.js +43 -0
  275. package/dist/pluginManager.spec.js.map +1 -0
  276. package/{lib → dist}/server.d.ts +14 -1
  277. package/dist/server.d.ts.map +1 -0
  278. package/{lib → dist}/server.js +193 -123
  279. package/dist/server.js.map +1 -0
  280. package/dist/server.spec.d.ts +2 -0
  281. package/dist/server.spec.d.ts.map +1 -0
  282. package/dist/server.spec.js +57 -0
  283. package/dist/server.spec.js.map +1 -0
  284. package/{lib → dist}/storageService.d.ts.map +1 -1
  285. package/dist/storageService.js +41 -0
  286. package/dist/storageService.js.map +1 -0
  287. package/{lib → dist}/user.d.ts +1 -0
  288. package/dist/user.d.ts.map +1 -0
  289. package/dist/user.js +32 -0
  290. package/dist/user.js.map +1 -0
  291. package/dist/user.spec.d.ts +2 -0
  292. package/dist/user.spec.d.ts.map +1 -0
  293. package/dist/user.spec.js +31 -0
  294. package/dist/user.spec.js.map +1 -0
  295. package/{lib → dist}/util/mac.d.ts +1 -0
  296. package/dist/util/mac.d.ts.map +1 -0
  297. package/dist/util/mac.js +14 -0
  298. package/dist/util/mac.js.map +1 -0
  299. package/dist/util/mac.spec.d.ts +2 -0
  300. package/dist/util/mac.spec.d.ts.map +1 -0
  301. package/dist/util/mac.spec.js +36 -0
  302. package/dist/util/mac.spec.js.map +1 -0
  303. package/dist/version.d.ts.map +1 -0
  304. package/dist/version.js +16 -0
  305. package/dist/version.js.map +1 -0
  306. package/dist/version.spec.d.ts +2 -0
  307. package/dist/version.spec.d.ts.map +1 -0
  308. package/dist/version.spec.js +20 -0
  309. package/dist/version.spec.js.map +1 -0
  310. package/package.json +49 -50
  311. package/bin/homebridge +0 -17
  312. package/lib/api.d.ts +0 -210
  313. package/lib/api.d.ts.map +0 -1
  314. package/lib/api.js +0 -155
  315. package/lib/api.js.map +0 -1
  316. package/lib/bridgeService.d.ts.map +0 -1
  317. package/lib/bridgeService.js.map +0 -1
  318. package/lib/childBridgeFork.d.ts.map +0 -1
  319. package/lib/childBridgeFork.js.map +0 -1
  320. package/lib/childBridgeService.d.ts.map +0 -1
  321. package/lib/childBridgeService.js.map +0 -1
  322. package/lib/cli.d.ts +0 -4
  323. package/lib/cli.d.ts.map +0 -1
  324. package/lib/cli.js +0 -111
  325. package/lib/cli.js.map +0 -1
  326. package/lib/externalPortService.d.ts +0 -33
  327. package/lib/externalPortService.d.ts.map +0 -1
  328. package/lib/externalPortService.js +0 -64
  329. package/lib/externalPortService.js.map +0 -1
  330. package/lib/index.d.ts +0 -76
  331. package/lib/index.d.ts.map +0 -1
  332. package/lib/index.js +0 -72
  333. package/lib/index.js.map +0 -1
  334. package/lib/ipcService.d.ts +0 -33
  335. package/lib/ipcService.d.ts.map +0 -1
  336. package/lib/ipcService.js.map +0 -1
  337. package/lib/logger.d.ts.map +0 -1
  338. package/lib/logger.js.map +0 -1
  339. package/lib/platformAccessory.d.ts.map +0 -1
  340. package/lib/platformAccessory.js.map +0 -1
  341. package/lib/plugin.d.ts.map +0 -1
  342. package/lib/plugin.js.map +0 -1
  343. package/lib/pluginManager.d.ts.map +0 -1
  344. package/lib/pluginManager.js.map +0 -1
  345. package/lib/server.d.ts.map +0 -1
  346. package/lib/server.js.map +0 -1
  347. package/lib/storageService.js +0 -70
  348. package/lib/storageService.js.map +0 -1
  349. package/lib/user.d.ts.map +0 -1
  350. package/lib/user.js +0 -36
  351. package/lib/user.js.map +0 -1
  352. package/lib/util/mac.d.ts.map +0 -1
  353. package/lib/util/mac.js +0 -20
  354. package/lib/util/mac.js.map +0 -1
  355. package/lib/version.d.ts.map +0 -1
  356. package/lib/version.js +0 -21
  357. package/lib/version.js.map +0 -1
  358. /package/{lib → dist}/storageService.d.ts +0 -0
  359. /package/{lib → dist}/version.d.ts +0 -0
@@ -0,0 +1,1810 @@
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
+ */
7
+ import { randomBytes } from 'node:crypto';
8
+ import { EventEmitter } from 'node:events';
9
+ import { constants } from 'node:fs';
10
+ import { access, writeFile } from 'node:fs/promises';
11
+ import { homedir, release } from 'node:os';
12
+ import { join, normalize, resolve } from 'node:path';
13
+ import process from 'node:process';
14
+ import { Endpoint, Environment, Logger as MatterLogger, LogLevel as MatterLogLevel, ServerNode, StorageService, VendorId, } from '@matter/main';
15
+ import { BridgedDeviceBasicInformationServer } from '@matter/main/behaviors';
16
+ import * as clusters from '@matter/main/clusters';
17
+ import * as devices from '@matter/main/devices';
18
+ import { AggregatorEndpoint } from '@matter/main/endpoints';
19
+ import { PowerSourceServer } from '@matter/node/behaviors';
20
+ import { ManualPairingCodeCodec, QrPairingCodeCodec } from '@matter/types/schema';
21
+ import fse from 'fs-extra';
22
+ import QRCode from 'qrcode-terminal';
23
+ import { Logger } from '../logger.js';
24
+ import getVersion from '../version.js';
25
+ import { MatterAccessoryCache } from './accessoryCache.js';
26
+ import { BehaviorRegistry, HomebridgeColorControlServer, HomebridgeDoorLockServer, HomebridgeFanControlServer, HomebridgeIdentifyServer, HomebridgeLevelControlServer, HomebridgeOnOffServer, HomebridgeRvcCleanModeServer, HomebridgeRvcOperationalStateServer, HomebridgeRvcRunModeServer, HomebridgeServiceAreaServer, HomebridgeThermostatServer, HomebridgeWindowCoveringServer, } from './behaviors/index.js';
27
+ import { sanitizeUniqueId, truncateString, validatePort } from './configValidator.js';
28
+ import { errorHandler } from './errorHandler.js';
29
+ import { createHomebridgeLogFormatter } from './logFormatter.js';
30
+ import { applyWindowCoveringFeatures, CLUSTER_IDS, detectBehaviorFeatures, detectWindowCoveringFeatures, determineColorControlFeaturesFromHandlers, extractColorControlFeatures, extractThermostatFeatures, validateAccessoryRequiredFields, } from './serverHelpers.js';
31
+ import { MatterStorageManager } from './storage.js';
32
+ import { isDeviceType, withBehaviors, withFeatures } from './typeHelpers.js';
33
+ import { deviceTypes, MatterDeviceError, } from './types.js';
34
+ const log = Logger.withPrefix('Matter/Server');
35
+ /**
36
+ * Constants for Matter server configuration
37
+ */
38
+ const DEFAULT_MATTER_PORT = 5540;
39
+ const DEFAULT_VENDOR_ID = 0xFFF1; // test vendor ID from Matter spec
40
+ const DEFAULT_PRODUCT_ID = 0x8001; // test product ID
41
+ const MAX_DEVICES_PER_BRIDGE = 1000; // matter spec maximum devices per aggregator
42
+ const SERVER_READY_TIMEOUT_MS = 5000;
43
+ const SERVER_READY_POLL_INTERVAL_MS = 100;
44
+ const SERVER_INIT_DELAY_MS = 200;
45
+ const MAX_PASSCODE_ATTEMPTS = 100;
46
+ /**
47
+ * Matter Server for Homebridge Plugin API
48
+ * Allows plugins to register Matter accessories explicitly
49
+ */
50
+ export class MatterServer extends EventEmitter {
51
+ config;
52
+ serverNode = null;
53
+ aggregator = null;
54
+ accessories = new Map();
55
+ behaviorRegistry;
56
+ isRunning = false;
57
+ MAX_DEVICES = MAX_DEVICES_PER_BRIDGE;
58
+ shutdownHandler = null;
59
+ // Map cluster names to custom behavior classes
60
+ // Only clusters with user-triggered commands need custom behaviors
61
+ static CLUSTER_BEHAVIOR_MAP = {
62
+ // Core controls
63
+ onOff: HomebridgeOnOffServer,
64
+ levelControl: HomebridgeLevelControlServer,
65
+ colorControl: HomebridgeColorControlServer,
66
+ // Coverings & locks
67
+ windowCovering: HomebridgeWindowCoveringServer,
68
+ doorLock: HomebridgeDoorLockServer,
69
+ // Climate control
70
+ fanControl: HomebridgeFanControlServer,
71
+ thermostat: HomebridgeThermostatServer,
72
+ // Robotic vacuum cleaners
73
+ rvcOperationalState: HomebridgeRvcOperationalStateServer,
74
+ rvcRunMode: HomebridgeRvcRunModeServer,
75
+ rvcCleanMode: HomebridgeRvcCleanModeServer,
76
+ serviceArea: HomebridgeServiceAreaServer,
77
+ // Identification
78
+ identify: HomebridgeIdentifyServer,
79
+ };
80
+ // Internal commissioning values (generated, not user-configurable)
81
+ passcode = 0;
82
+ discriminator = 0;
83
+ vendorId;
84
+ productId;
85
+ commissioningInfo = {};
86
+ serialNumber;
87
+ cleanupHandlers = [];
88
+ storageManager = null;
89
+ matterStoragePath;
90
+ accessoryCache = null;
91
+ constructor(config) {
92
+ super();
93
+ // Store the validated config
94
+ this.config = this.validateAndSanitizeConfig(config);
95
+ // Configure Matter.js library logging
96
+ // Suppress DEBUG/INFO logs from Matter.js library unless debug mode is explicitly enabled
97
+ if (this.config.debugModeEnabled) {
98
+ log.info('Matter debug mode enabled - verbose logging active');
99
+ MatterLogger.level = MatterLogLevel.DEBUG;
100
+ }
101
+ else {
102
+ MatterLogger.level = MatterLogLevel.NOTICE;
103
+ }
104
+ // Set custom log format to match homebridge format
105
+ MatterLogger.format = createHomebridgeLogFormatter();
106
+ // Redirect all Matter.js logs to console.log to prevent console.debug() suppression.
107
+ // Matter.js uses console.debug() for DEBUG level logs, which is silently ignored in many Node.js environments.
108
+ MatterLogger.destinations.default.write = (text) => {
109
+ // Skip empty strings to avoid blank lines (from suppressed log facilities)
110
+ if (text.trim() !== '') {
111
+ console.log(text); // eslint-disable-line no-console
112
+ }
113
+ };
114
+ // Initialize commissioning values (will be loaded from storage in start())
115
+ this.vendorId = DEFAULT_VENDOR_ID;
116
+ this.productId = DEFAULT_PRODUCT_ID;
117
+ // Create behavior registry and set it on all behavior classes
118
+ this.behaviorRegistry = new BehaviorRegistry(this.accessories);
119
+ // Set the registry on all custom behavior classes
120
+ HomebridgeOnOffServer.setRegistry(this.behaviorRegistry);
121
+ HomebridgeLevelControlServer.setRegistry(this.behaviorRegistry);
122
+ HomebridgeColorControlServer.setRegistry(this.behaviorRegistry);
123
+ HomebridgeWindowCoveringServer.setRegistry(this.behaviorRegistry);
124
+ HomebridgeDoorLockServer.setRegistry(this.behaviorRegistry);
125
+ HomebridgeFanControlServer.setRegistry(this.behaviorRegistry);
126
+ HomebridgeThermostatServer.setRegistry(this.behaviorRegistry);
127
+ HomebridgeIdentifyServer.setRegistry(this.behaviorRegistry);
128
+ HomebridgeRvcOperationalStateServer.setRegistry(this.behaviorRegistry);
129
+ HomebridgeRvcRunModeServer.setRegistry(this.behaviorRegistry);
130
+ HomebridgeRvcCleanModeServer.setRegistry(this.behaviorRegistry);
131
+ HomebridgeServiceAreaServer.setRegistry(this.behaviorRegistry);
132
+ }
133
+ /**
134
+ * Validate and sanitize Matter server configuration
135
+ * Throws descriptive errors if configuration is invalid
136
+ */
137
+ validateAndSanitizeConfig(config) {
138
+ const errors = [];
139
+ // Validate port
140
+ const port = config.port || DEFAULT_MATTER_PORT;
141
+ const portValidation = validatePort(port, false);
142
+ if (!portValidation.valid) {
143
+ errors.push(`Invalid port: ${portValidation.error}`);
144
+ }
145
+ // Validate and sanitize uniqueId (REQUIRED)
146
+ if (!config.uniqueId) {
147
+ errors.push('uniqueId is required for Matter server configuration');
148
+ }
149
+ const rawUniqueId = config.uniqueId || '';
150
+ const uniqueIdResult = sanitizeUniqueId(rawUniqueId);
151
+ const uniqueId = uniqueIdResult.value;
152
+ if (uniqueId.length === 0) {
153
+ errors.push('Invalid uniqueId: must be a non-empty string');
154
+ }
155
+ // Validate storagePath (if provided)
156
+ let storagePath = config.storagePath;
157
+ if (storagePath !== undefined) {
158
+ storagePath = resolve(storagePath); // resolve to absolute path
159
+ }
160
+ // Validate and sanitize manufacturer
161
+ let manufacturer = config.manufacturer;
162
+ if (manufacturer !== undefined) {
163
+ manufacturer = truncateString(manufacturer, 32, 'Manufacturer name').value;
164
+ }
165
+ // Validate and sanitize model
166
+ let model = config.model;
167
+ if (model !== undefined) {
168
+ model = truncateString(model, 32, 'Model name').value;
169
+ }
170
+ // Validate firmwareRevision
171
+ let firmwareRevision = config.firmwareRevision;
172
+ if (firmwareRevision !== undefined) {
173
+ firmwareRevision = truncateString(firmwareRevision, 64, 'Firmware revision').value;
174
+ }
175
+ // Validate serialNumber
176
+ let serialNumber = config.serialNumber;
177
+ if (serialNumber !== undefined) {
178
+ serialNumber = truncateString(serialNumber, 32, 'Serial number').value;
179
+ }
180
+ // Validate debugModeEnabled
181
+ const debugModeEnabled = config.debugModeEnabled || false;
182
+ // Validate externalAccessory
183
+ const externalAccessory = config.externalAccessory || false;
184
+ // Throw if there are validation errors
185
+ if (errors.length > 0) {
186
+ throw new MatterDeviceError(`Matter configuration validation failed:\n${errors.map(e => ` - ${e}`).join('\n')}`);
187
+ }
188
+ return {
189
+ port,
190
+ uniqueId,
191
+ storagePath,
192
+ manufacturer,
193
+ model,
194
+ firmwareRevision,
195
+ serialNumber,
196
+ debugModeEnabled,
197
+ externalAccessory,
198
+ };
199
+ }
200
+ /**
201
+ * Generate a secure random passcode
202
+ * According to Matter spec, passcode must be:
203
+ * - 8 digits (00000001 to 99999998)
204
+ * - Not in the invalid list
205
+ * - Not sequential or repeating patterns
206
+ */
207
+ generateSecurePasscode() {
208
+ let passcode;
209
+ const maxAttempts = MAX_PASSCODE_ATTEMPTS;
210
+ let attempts = 0;
211
+ const invalidPasscodes = [
212
+ 0,
213
+ 11111111,
214
+ 22222222,
215
+ 33333333,
216
+ 44444444,
217
+ 55555555,
218
+ 66666666,
219
+ 77777777,
220
+ 88888888,
221
+ 99999999,
222
+ 12345678,
223
+ 87654321,
224
+ ];
225
+ do {
226
+ // Use cryptographically secure random number generation
227
+ const randomValue = randomBytes(4).readUInt32BE(0);
228
+ // Generate a value between 1 and 99999998
229
+ passcode = (randomValue % 99999998) + 1;
230
+ attempts++;
231
+ if (attempts > maxAttempts) {
232
+ throw new Error('Failed to generate secure passcode after maximum attempts');
233
+ }
234
+ } while (invalidPasscodes.includes(passcode)
235
+ || !this.isValidPasscode(passcode));
236
+ return passcode;
237
+ }
238
+ /**
239
+ * Validate a passcode according to Matter specifications
240
+ */
241
+ isValidPasscode(passcode) {
242
+ // Must be between 1 and 99999998
243
+ if (passcode < 1 || passcode > 99999998) {
244
+ return false;
245
+ }
246
+ // Convert to 8-digit string
247
+ const passcodeStr = passcode.toString().padStart(8, '0');
248
+ // Check for sequential patterns (12345678, 23456789, etc.)
249
+ let isSequential = true;
250
+ for (let i = 1; i < passcodeStr.length; i++) {
251
+ if (Number.parseInt(passcodeStr[i]) !== Number.parseInt(passcodeStr[i - 1]) + 1) {
252
+ isSequential = false;
253
+ break;
254
+ }
255
+ }
256
+ if (isSequential) {
257
+ return false;
258
+ }
259
+ // Check for reverse sequential (87654321, 76543210, etc.)
260
+ let isReverseSequential = true;
261
+ for (let i = 1; i < passcodeStr.length; i++) {
262
+ if (Number.parseInt(passcodeStr[i]) !== Number.parseInt(passcodeStr[i - 1]) - 1) {
263
+ isReverseSequential = false;
264
+ break;
265
+ }
266
+ }
267
+ if (isReverseSequential) {
268
+ return false;
269
+ }
270
+ // Check for too many repeating digits (more than 3 of same digit)
271
+ const digitCounts = new Map();
272
+ for (const digit of passcodeStr) {
273
+ digitCounts.set(digit, (digitCounts.get(digit) || 0) + 1);
274
+ const count = digitCounts.get(digit);
275
+ if (count !== undefined && count > 3) {
276
+ return false;
277
+ }
278
+ }
279
+ return true;
280
+ }
281
+ /**
282
+ * Generate a random discriminator
283
+ * According to Matter spec, discriminator must be:
284
+ * - 12 bits (0-4095)
285
+ * - Should be random for security
286
+ */
287
+ generateRandomDiscriminator() {
288
+ // Generate cryptographically secure random 12-bit discriminator (0-4095)
289
+ const discriminator = randomBytes(2).readUInt16BE(0) & 0x0FFF; // Mask to 12 bits
290
+ // Validate discriminator range
291
+ if (discriminator < 0 || discriminator > 4095) {
292
+ throw new Error(`Invalid discriminator generated: ${discriminator}`);
293
+ }
294
+ return discriminator;
295
+ }
296
+ /**
297
+ * Create ServerNode with automatic recovery from corrupted storage
298
+ *
299
+ * Matter.js can fail to start if fabric data is corrupted (common after
300
+ * hard shutdowns or disk errors). This method implements automatic recovery by:
301
+ *
302
+ * 1. Attempting normal ServerNode creation
303
+ * 2. If it fails with storage errors, identifying and removing corrupted files
304
+ * 3. Retrying ServerNode creation with fresh storage
305
+ *
306
+ * This prevents the need for manual intervention while preserving data
307
+ * safety by only removing storage on confirmed corruption errors.
308
+ *
309
+ * @param nodeOptions - Matter.js ServerNode configuration
310
+ * @param sanitizedId - Filesystem-safe bridge identifier
311
+ * @returns Initialized ServerNode instance
312
+ * @throws Error if recovery fails or error is not storage-related
313
+ */
314
+ async createServerNodeWithRecovery(nodeOptions, sanitizedId) {
315
+ try {
316
+ // First attempt to create ServerNode
317
+ return await ServerNode.create(nodeOptions);
318
+ }
319
+ catch (error) {
320
+ // Check if this is a storage corruption error
321
+ const errorMessage = error instanceof Error ? error.message : '';
322
+ const causeMessage = error instanceof Error && error.cause instanceof Error ? error.cause.message : '';
323
+ const isStorageError = errorMessage.includes('Invalid public key encoding')
324
+ || errorMessage.includes('FabricManager unavailable')
325
+ || errorMessage.includes('key-input')
326
+ || causeMessage.includes('Invalid public key encoding');
327
+ if (!isStorageError) {
328
+ // Not a storage error, rethrow
329
+ throw error;
330
+ }
331
+ // Storage is corrupted - clean up and retry
332
+ log.warn('Detected corrupted Matter storage, attempting automatic recovery...');
333
+ // The ServerNodeStore directory is inside our storage path with the same name as the bridge ID
334
+ const environment = Environment.default;
335
+ const storageService = environment.get(StorageService);
336
+ const storageLocation = storageService.location;
337
+ if (!storageLocation) {
338
+ throw new Error('Storage location not set, cannot recover from corrupted storage');
339
+ }
340
+ const serverNodeStorePath = join(storageLocation, sanitizedId);
341
+ const serverNodeStoreJsonFile = `${serverNodeStorePath}.json`;
342
+ try {
343
+ let removedSomething = false;
344
+ // Delete the ServerNodeStore subdirectory (async check and removal)
345
+ try {
346
+ await fse.stat(serverNodeStorePath);
347
+ log.info(`Removing corrupted ServerNodeStore directory: ${serverNodeStorePath}`);
348
+ await fse.remove(serverNodeStorePath);
349
+ removedSomething = true;
350
+ }
351
+ catch (err) {
352
+ const code = err instanceof Error && 'code' in err ? err.code : undefined;
353
+ if (code !== 'ENOENT') {
354
+ throw err;
355
+ }
356
+ }
357
+ // Delete the ServerNodeStore JSON file (contains fabric data)
358
+ try {
359
+ await fse.stat(serverNodeStoreJsonFile);
360
+ log.info(`Removing corrupted ServerNodeStore JSON file: ${serverNodeStoreJsonFile}`);
361
+ await fse.remove(serverNodeStoreJsonFile);
362
+ removedSomething = true;
363
+ }
364
+ catch (err) {
365
+ const code = err instanceof Error && 'code' in err ? err.code : undefined;
366
+ if (code !== 'ENOENT') {
367
+ throw err;
368
+ }
369
+ }
370
+ if (removedSomething) {
371
+ log.info('Corrupted storage removed, retrying ServerNode creation...');
372
+ }
373
+ else {
374
+ log.warn('No corrupted storage files found, corruption may be elsewhere');
375
+ }
376
+ // Retry ServerNode creation
377
+ const serverNode = await ServerNode.create(nodeOptions);
378
+ log.info('Successfully recovered from corrupted Matter storage');
379
+ return serverNode;
380
+ }
381
+ catch (retryError) {
382
+ log.error('Failed to recover from corrupted storage:', retryError);
383
+ log.error('Original error:', error);
384
+ throw new Error('Matter storage is corrupted and automatic recovery failed. '
385
+ + `Please manually delete: ${serverNodeStorePath}`);
386
+ }
387
+ }
388
+ }
389
+ /**
390
+ * Start the Matter server
391
+ */
392
+ async start() {
393
+ if (this.isRunning) {
394
+ log.warn('Matter server is already running');
395
+ return;
396
+ }
397
+ try {
398
+ log.info('Starting Matter.js server...');
399
+ // IMPORTANT: Storage must be configured BEFORE any Matter.js operations
400
+ // This ensures persistent fabric data across restarts
401
+ await this.setupStorage();
402
+ // Load or generate commissioning credentials
403
+ await this.loadOrGenerateCredentials();
404
+ log.info(`Configuration: Port=${this.config.port}, Passcode=${this.passcode}, Discriminator=${this.discriminator}`);
405
+ // Configure network interfaces if specified in the config
406
+ if (this.config.networkInterfaces && this.config.networkInterfaces.length > 0) {
407
+ const environment = Environment.default;
408
+ const interfaceConfig = {};
409
+ // Map each interface name to type 2 (Ethernet) as default
410
+ // Matter.js will use only these interfaces for the server
411
+ for (const interfaceName of this.config.networkInterfaces) {
412
+ interfaceConfig[interfaceName] = { type: 2 }; // 2 = Ethernet
413
+ }
414
+ environment.vars.set('network.interface', interfaceConfig);
415
+ log.info(`Configured Matter server to use network interfaces: ${this.config.networkInterfaces.join(', ')}`);
416
+ }
417
+ else {
418
+ log.debug('No network interfaces specified, using all available interfaces');
419
+ }
420
+ // Create commissioning options
421
+ const commissioningOptions = {
422
+ passcode: this.passcode,
423
+ discriminator: this.discriminator,
424
+ };
425
+ log.info(`Using commissioning credentials: passcode=${this.passcode}, discriminator=${this.discriminator}`);
426
+ // Use different names based on mode
427
+ const displayName = this.config.externalAccessory
428
+ ? (this.config.model || 'Matter Device')
429
+ : 'Homebridge Matter Bridge';
430
+ // uniqueId is already sanitized in validateAndSanitizeConfig()
431
+ const sanitizedId = this.config.uniqueId;
432
+ // Create node options with proper typing
433
+ const nodeOptions = {
434
+ id: sanitizedId,
435
+ network: {
436
+ port: this.config.port,
437
+ ipv4: true, // Always enable IPv4 for Matter
438
+ },
439
+ commissioning: commissioningOptions,
440
+ basicInformation: {
441
+ nodeLabel: displayName.slice(0, 32), // Maximum 32 characters
442
+ vendorId: VendorId(this.vendorId),
443
+ vendorName: (this.config.manufacturer || 'Homebridge').slice(0, 32),
444
+ productId: this.productId,
445
+ productName: displayName.slice(0, 32),
446
+ productLabel: displayName.slice(0, 64), // Maximum 64 characters
447
+ serialNumber: this.serialNumber = this.config.serialNumber || this.config.uniqueId,
448
+ hardwareVersion: 1,
449
+ hardwareVersionString: release(),
450
+ softwareVersion: 1,
451
+ softwareVersionString: this.config.firmwareRevision || getVersion(),
452
+ reachable: true,
453
+ },
454
+ };
455
+ // Only add productDescription with bridge deviceType in bridge mode
456
+ if (!this.config.externalAccessory) {
457
+ nodeOptions.productDescription = {
458
+ name: displayName,
459
+ deviceType: AggregatorEndpoint.deviceType,
460
+ };
461
+ }
462
+ // Create server node with automatic recovery from corrupted storage
463
+ this.serverNode = await this.createServerNodeWithRecovery(nodeOptions, sanitizedId);
464
+ // Set up commissioning event listeners
465
+ this.setupCommissioningEventListeners();
466
+ // Create aggregator endpoint for bridge pattern (skip for external accessories)
467
+ if (!this.config.externalAccessory) {
468
+ this.aggregator = new Endpoint(AggregatorEndpoint, {
469
+ id: 'homebridge-aggregator',
470
+ });
471
+ // Add aggregator to server
472
+ await this.serverNode.add(this.aggregator);
473
+ log.debug('Created aggregator endpoint for bridged mode');
474
+ }
475
+ else {
476
+ log.debug('External accessory mode - skipping aggregator creation');
477
+ }
478
+ // Generate and display commissioning information
479
+ await this.generateCommissioningInfo();
480
+ // Set up graceful shutdown handler
481
+ this.shutdownHandler = async () => {
482
+ log.info('Shutting down Matter server...');
483
+ await this.stop();
484
+ };
485
+ // Register shutdown handlers
486
+ process.on('SIGINT', this.shutdownHandler);
487
+ process.on('SIGTERM', this.shutdownHandler);
488
+ // If external accessory mode, skip running the server now (will be run later via runServer())
489
+ if (!this.config.externalAccessory) {
490
+ // Start the server in a non-blocking way
491
+ this.serverNode.run().then(() => {
492
+ log.info('Matter server stopped normally');
493
+ }, (error) => {
494
+ log.error('Matter server stopped with error:', error);
495
+ errorHandler.handleError(error, 'server-runtime');
496
+ });
497
+ // Wait for server to be ready
498
+ await this.waitForServerReady();
499
+ // Load cached accessories (don't restore them yet - wait for plugins to re-register)
500
+ if (this.accessoryCache) {
501
+ const loaded = await this.accessoryCache.load();
502
+ log.debug(`Matter cache loaded: ${loaded.size} accessories`);
503
+ }
504
+ else {
505
+ log.debug('No accessory cache available');
506
+ }
507
+ // Update commissioning file to reflect current state
508
+ this.updateCommissioningFile().catch((error) => {
509
+ log.warn('Failed to update commissioning file on startup:', error);
510
+ });
511
+ this.isRunning = true;
512
+ }
513
+ else {
514
+ log.debug('Deferred start mode - server prepared but not running yet (will start after device registration)');
515
+ }
516
+ log.info(`Matter server started successfully on port ${this.config.port}`);
517
+ log.info('Plugins can now register Matter accessories via the API');
518
+ }
519
+ catch (error) {
520
+ log.error('Failed to start Matter server:', error);
521
+ await this.cleanup();
522
+ throw error;
523
+ }
524
+ }
525
+ /**
526
+ * Run the server after devices have been added (for external accessory mode)
527
+ *
528
+ * This must be called after registerPlatformAccessories() when using externalAccessory mode.
529
+ * In bridge mode, the server starts automatically when accessories are registered.
530
+ *
531
+ * @throws {MatterDeviceError} If server node is not initialized or server is already running
532
+ * @example
533
+ * ```typescript
534
+ * await matterServer.start()
535
+ * await matterServer.registerPlatformAccessories('plugin', 'platform', accessories)
536
+ * await matterServer.runServer() // External accessory mode only
537
+ * ```
538
+ */
539
+ async runServer() {
540
+ if (!this.serverNode) {
541
+ throw new MatterDeviceError('Server node not initialized - call start() first');
542
+ }
543
+ if (this.isRunning) {
544
+ log.warn('Matter server is already running');
545
+ return;
546
+ }
547
+ if (!this.config.externalAccessory) {
548
+ throw new MatterDeviceError('runServer() should only be called when externalAccessory mode is enabled');
549
+ }
550
+ log.debug('Running deferred server with device(s) already attached');
551
+ // Start the server in a non-blocking way
552
+ this.serverNode.run().then(() => {
553
+ log.info('Matter server stopped normally');
554
+ }, (error) => {
555
+ log.error('Matter server stopped with error:', error);
556
+ errorHandler.handleError(error, 'server-runtime');
557
+ });
558
+ // Wait for server to be ready
559
+ await this.waitForServerReady();
560
+ // Load cached accessories (don't restore them yet - wait for plugins to re-register)
561
+ if (this.accessoryCache) {
562
+ const loaded = await this.accessoryCache.load();
563
+ log.debug(`Matter cache loaded: ${loaded.size} accessories`);
564
+ }
565
+ else {
566
+ log.debug('No accessory cache available');
567
+ }
568
+ // Update commissioning file to reflect current state
569
+ this.updateCommissioningFile().catch((error) => {
570
+ log.warn('Failed to update commissioning file on startup:', error);
571
+ });
572
+ this.isRunning = true;
573
+ log.info('Matter server is now running');
574
+ }
575
+ /**
576
+ * Set up and validate storage
577
+ */
578
+ async setupStorage() {
579
+ if (!this.config.storagePath) {
580
+ throw new Error('Storage path is required for Matter server');
581
+ }
582
+ // Resolve to absolute path and validate
583
+ const storagePath = resolve(this.config.storagePath);
584
+ const normalizedPath = normalize(storagePath);
585
+ // Ensure path is within allowed directories
586
+ const allowedBasePaths = [
587
+ resolve(homedir(), '.homebridge'),
588
+ resolve(process.cwd()),
589
+ '/var/lib/homebridge', // Common system location
590
+ ];
591
+ const isAllowed = allowedBasePaths.some(basePath => normalizedPath.startsWith(basePath));
592
+ if (!isAllowed || normalizedPath.includes('..')) {
593
+ throw new Error(`Storage path not allowed: ${normalizedPath}. Must be within homebridge directories.`);
594
+ }
595
+ // Ensure the storage directory exists with proper permissions
596
+ try {
597
+ await fse.ensureDir(normalizedPath);
598
+ await access(normalizedPath, constants.R_OK | constants.W_OK);
599
+ }
600
+ catch (error) {
601
+ throw new Error(`Storage path not accessible: ${error}`);
602
+ }
603
+ // Create bridge-specific storage directory
604
+ // uniqueId is already sanitized in validateAndSanitizeConfig()
605
+ const bridgeId = this.config.uniqueId || 'default';
606
+ this.matterStoragePath = join(normalizedPath, bridgeId);
607
+ await fse.ensureDir(this.matterStoragePath);
608
+ // Create storage manager
609
+ this.storageManager = new MatterStorageManager(this.matterStoragePath);
610
+ // Create accessory cache
611
+ this.accessoryCache = new MatterAccessoryCache(normalizedPath, bridgeId);
612
+ // Configure environment to use our custom storage
613
+ const environment = Environment.default;
614
+ const storageService = environment.get(StorageService);
615
+ storageService.location = this.matterStoragePath;
616
+ // CRITICAL: Override storage factory with custom implementation
617
+ // This ensures fabric data is properly persisted
618
+ storageService.factory = (namespace) => {
619
+ if (!this.storageManager) {
620
+ throw new Error('Storage manager not initialized');
621
+ }
622
+ const storage = this.storageManager.getStorage(namespace);
623
+ // Initialize asynchronously - Matter.js handles async storage properly
624
+ storage.initialize().catch((error) => {
625
+ log.error(`Failed to initialize storage namespace ${namespace}:`, error);
626
+ });
627
+ // Note: Cast to unknown first to satisfy TypeScript - our storage implements the required interface
628
+ return storage;
629
+ };
630
+ // Add cleanup handler for storage
631
+ this.cleanupHandlers.push(async () => {
632
+ if (this.storageManager) {
633
+ await this.storageManager.closeAll();
634
+ }
635
+ });
636
+ log.info(`Matter storage initialized at: ${this.matterStoragePath}`);
637
+ }
638
+ /**
639
+ * Load or generate commissioning credentials (passcode and discriminator)
640
+ * These must be persistent across restarts to maintain the same QR code
641
+ */
642
+ async loadOrGenerateCredentials() {
643
+ if (!this.storageManager) {
644
+ throw new Error('Storage manager not initialized');
645
+ }
646
+ // Use 'credentials' namespace
647
+ const storage = this.storageManager.getStorage('credentials');
648
+ // CRITICAL: Initialize storage before reading to avoid race condition
649
+ await storage.initialize();
650
+ // Try to load existing credentials
651
+ const storedPasscode = storage.get([], 'passcode');
652
+ const storedDiscriminator = storage.get([], 'discriminator');
653
+ if (storedPasscode && storedDiscriminator) {
654
+ // Use stored credentials
655
+ log.info('Loading existing commissioning credentials from storage');
656
+ this.passcode = storedPasscode;
657
+ this.discriminator = storedDiscriminator;
658
+ }
659
+ else {
660
+ // Generate new credentials and store them
661
+ log.info('Generating new commissioning credentials');
662
+ this.passcode = this.generateSecurePasscode();
663
+ this.discriminator = this.generateRandomDiscriminator();
664
+ // Store for future use
665
+ storage.set([], 'passcode', this.passcode);
666
+ storage.set([], 'discriminator', this.discriminator);
667
+ log.info('Commissioning credentials saved to storage');
668
+ }
669
+ }
670
+ /**
671
+ * Generate and display commissioning information
672
+ */
673
+ async generateCommissioningInfo() {
674
+ const passcode = this.passcode.toString().padStart(8, '0');
675
+ const discriminator = this.discriminator;
676
+ const vendorId = this.vendorId;
677
+ const productId = this.productId;
678
+ // Use Matter.js library to generate pairing codes properly
679
+ const manualCode = ManualPairingCodeCodec.encode({
680
+ discriminator,
681
+ passcode: this.passcode,
682
+ });
683
+ // Format as XXXX-XXX-XXXX for display
684
+ const manualPairingCode = `${manualCode.slice(0, 4)}-${manualCode.slice(4, 7)}-${manualCode.slice(7, 11)}`;
685
+ log.info(`Encoding QR code with: passcode=${this.passcode}, discriminator=${discriminator}, vendorId=${vendorId}, productId=${productId}`);
686
+ const qrCodePayload = QrPairingCodeCodec.encode([{
687
+ version: 0,
688
+ vendorId,
689
+ productId,
690
+ flowType: 0, // Standard commissioning flow
691
+ discoveryCapabilities: 4, // OnNetwork=4
692
+ discriminator,
693
+ passcode: this.passcode,
694
+ }]);
695
+ log.info(`Generated QR code: ${qrCodePayload}`);
696
+ log.info(`Generated manual code: ${manualPairingCode}`);
697
+ // Store commissioning info
698
+ this.commissioningInfo = {
699
+ qrCode: qrCodePayload,
700
+ manualPairingCode,
701
+ };
702
+ // Save commissioning info to disk for UI access
703
+ try {
704
+ if (!this.matterStoragePath) {
705
+ throw new Error('Matter storage path not initialized');
706
+ }
707
+ const commissioningFilePath = join(this.matterStoragePath, 'commissioning.json');
708
+ const commissioningData = {
709
+ qrCode: qrCodePayload,
710
+ manualPairingCode,
711
+ serialNumber: this.serialNumber,
712
+ passcode: this.passcode,
713
+ discriminator: this.discriminator,
714
+ commissioned: this.isCommissioned(),
715
+ };
716
+ await writeFile(commissioningFilePath, JSON.stringify(commissioningData, null, 2), 'utf-8');
717
+ log.debug(`Saved commissioning info to ${commissioningFilePath}`);
718
+ }
719
+ catch (error) {
720
+ const errorMessage = error instanceof Error ? error.message : String(error);
721
+ log.warn(`Failed to save commissioning info to disk: ${errorMessage}`);
722
+ }
723
+ // Display commissioning information
724
+ log.info(`${'='.repeat(60)}`);
725
+ log.info('📱 MATTER COMMISSIONING INFORMATION');
726
+ log.info('='.repeat(60));
727
+ log.info(`Manual Pairing Code: ${manualPairingCode}`);
728
+ log.info(`Passcode: ${passcode}`);
729
+ log.info(`Discriminator: ${discriminator}`);
730
+ log.info('QR Code for commissioning:');
731
+ // Generate and display QR code in terminal
732
+ QRCode.generate(qrCodePayload, { small: true }, (qrcode) => {
733
+ // eslint-disable-next-line no-console
734
+ console.log(qrcode);
735
+ });
736
+ log.info(`${'='.repeat(60)}`);
737
+ }
738
+ /**
739
+ * Wait for the server to be ready
740
+ */
741
+ async waitForServerReady(maxWaitTime = SERVER_READY_TIMEOUT_MS) {
742
+ const startTime = Date.now();
743
+ // In external accessory mode, only wait for serverNode (no aggregator)
744
+ // In bridge mode, wait for both serverNode and aggregator
745
+ while (!this.serverNode || (!this.config.externalAccessory && !this.aggregator)) {
746
+ if (Date.now() - startTime > maxWaitTime) {
747
+ throw new Error('Server failed to become ready within timeout');
748
+ }
749
+ await new Promise(resolve => setTimeout(resolve, SERVER_READY_POLL_INTERVAL_MS));
750
+ }
751
+ // Additional small delay to ensure everything is initialized
752
+ await new Promise(resolve => setTimeout(resolve, SERVER_INIT_DELAY_MS));
753
+ }
754
+ /**
755
+ * Set up Matter.js commissioning event listeners
756
+ * Uses native Matter.js events instead of file watching for reliability
757
+ */
758
+ setupCommissioningEventListeners() {
759
+ if (!this.serverNode) {
760
+ log.warn('Cannot set up commissioning event listeners - serverNode not initialized');
761
+ return;
762
+ }
763
+ log.debug('Setting up commissioning event listeners');
764
+ try {
765
+ // Listen for fabric changes (add/remove/update)
766
+ this.serverNode.events.commissioning.fabricsChanged.on((fabricIndex, action) => {
767
+ log.info(`Fabric ${action}: index ${fabricIndex}`);
768
+ // Update commissioning file when fabrics change
769
+ this.updateCommissioningFile().catch((error) => {
770
+ log.warn('Failed to update commissioning file after fabric change:', error);
771
+ });
772
+ // Emit event for child bridge to update UI
773
+ const commissioned = this.isCommissioned();
774
+ const fabricCount = this.getCommissionedFabricCount();
775
+ this.emit('commissioning-status-changed', commissioned, fabricCount);
776
+ });
777
+ // Listen for commissioning (first fabric added)
778
+ this.serverNode.events.commissioning.commissioned.on(() => {
779
+ log.info('Bridge commissioned');
780
+ // Update commissioning file
781
+ this.updateCommissioningFile().catch((error) => {
782
+ log.warn('Failed to update commissioning file after commissioning:', error);
783
+ });
784
+ // Emit event for child bridge to update UI
785
+ const fabricCount = this.getCommissionedFabricCount();
786
+ this.emit('commissioning-status-changed', true, fabricCount);
787
+ });
788
+ // Listen for decommissioning (last fabric removed)
789
+ this.serverNode.events.commissioning.decommissioned.on(() => {
790
+ log.info('Bridge decommissioned');
791
+ // Update commissioning file
792
+ this.updateCommissioningFile().catch((error) => {
793
+ log.warn('Failed to update commissioning file after decommissioning:', error);
794
+ });
795
+ // Emit event for child bridge to update UI
796
+ this.emit('commissioning-status-changed', false, 0);
797
+ });
798
+ log.debug('Commissioning event listeners registered successfully');
799
+ }
800
+ catch (error) {
801
+ log.error('Failed to set up commissioning event listeners:', error);
802
+ }
803
+ }
804
+ /**
805
+ * Update commissioning info file when commissioning state changes
806
+ */
807
+ async updateCommissioningFile() {
808
+ try {
809
+ if (!this.matterStoragePath) {
810
+ return;
811
+ }
812
+ const commissioningFilePath = join(this.matterStoragePath, 'commissioning.json');
813
+ const commissioningData = {
814
+ qrCode: this.commissioningInfo.qrCode,
815
+ manualPairingCode: this.commissioningInfo.manualPairingCode,
816
+ serialNumber: this.serialNumber,
817
+ passcode: this.passcode,
818
+ discriminator: this.discriminator,
819
+ commissioned: this.isCommissioned(),
820
+ fabricCount: this.getCommissionedFabricCount(),
821
+ fabrics: this.getFabricInfo(),
822
+ };
823
+ await writeFile(commissioningFilePath, JSON.stringify(commissioningData, null, 2), 'utf-8');
824
+ log.debug('Updated commissioning info file');
825
+ }
826
+ catch (error) {
827
+ const errorMessage = error instanceof Error ? error.message : String(error);
828
+ log.debug(`Failed to update commissioning info file: ${errorMessage}`);
829
+ }
830
+ }
831
+ /**
832
+ * Register Matter platform accessories (Plugin API - matches HAP pattern)
833
+ *
834
+ * Registers Matter accessories from a dynamic platform plugin. Accessories are stored
835
+ * and automatically restored on server restart.
836
+ *
837
+ * @param pluginIdentifier - The plugin identifier (e.g., 'homebridge-example')
838
+ * @param platformName - The platform name from config.json
839
+ * @param accessories - Array of Matter accessories to register
840
+ * @throws {MatterDeviceError} If maximum device limit is reached or accessory is invalid
841
+ * @see {@link MatterAccessory} for accessory structure
842
+ */
843
+ async registerPlatformAccessories(pluginIdentifier, platformName, accessories) {
844
+ for (const accessory of accessories) {
845
+ await this.registerAccessory(pluginIdentifier, platformName, accessory);
846
+ }
847
+ }
848
+ /**
849
+ * Unregister Matter platform accessories (Plugin API - matches HAP pattern)
850
+ */
851
+ async unregisterPlatformAccessories(pluginIdentifier, platformName, accessories) {
852
+ for (const accessory of accessories) {
853
+ await this.unregisterAccessory(accessory.uuid);
854
+ }
855
+ }
856
+ /**
857
+ * Update Matter platform accessories in the cache
858
+ * Similar to api.updatePlatformAccessories() for HAP accessories
859
+ *
860
+ * This updates the cached accessory information without unregistering and re-registering.
861
+ * Useful when device metadata changes (name, manufacturer, firmware version, etc.)
862
+ */
863
+ async updatePlatformAccessories(accessories) {
864
+ if (!this.accessoryCache) {
865
+ log.warn('Cannot update Matter platform accessories - cache not initialized');
866
+ return;
867
+ }
868
+ for (const accessory of accessories) {
869
+ const internal = accessory;
870
+ // Verify accessory exists in current session and cache
871
+ if (!this.accessories.has(accessory.uuid)) {
872
+ log.warn(`Cannot update Matter accessory ${accessory.uuid} - not registered in current session`);
873
+ continue;
874
+ }
875
+ if (!this.accessoryCache.hasCached(accessory.uuid)) {
876
+ log.warn(`Cannot update Matter accessory ${accessory.uuid} - not found in cache`);
877
+ continue;
878
+ }
879
+ // Update the in-memory accessory
880
+ this.accessories.set(accessory.uuid, internal);
881
+ log.debug(`Updated Matter accessory ${accessory.uuid} (${accessory.displayName})`);
882
+ }
883
+ // Save updated accessories to cache
884
+ this.accessoryCache.requestSave(this.accessories);
885
+ }
886
+ /**
887
+ * Register a single Matter accessory (internal method)
888
+ */
889
+ async registerAccessory(pluginIdentifier, platformName, accessory) {
890
+ // In external accessory mode, only check for serverNode (no aggregator).
891
+ // In bridge mode, check for both serverNode and aggregator.
892
+ if (!this.serverNode || (!this.config.externalAccessory && !this.aggregator)) {
893
+ throw new MatterDeviceError('Matter server not started');
894
+ }
895
+ // Validate required fields
896
+ validateAccessoryRequiredFields(accessory);
897
+ // Check if already registered (during this session)
898
+ if (this.accessories.has(accessory.uuid)) {
899
+ const existing = this.accessories.get(accessory.uuid);
900
+ throw new MatterDeviceError(`Matter accessory with UUID "${accessory.uuid}" is already registered.\n`
901
+ + `Existing accessory: "${existing?.displayName}"\n`
902
+ + `New accessory: "${accessory.displayName}"\n`
903
+ + 'Each accessory must have a unique UUID. Use api.hap.uuid.generate() with a unique string.');
904
+ }
905
+ // Restore cached state if available
906
+ this.restoreCachedState(accessory);
907
+ // Check device limit
908
+ if (this.accessories.size >= this.MAX_DEVICES) {
909
+ throw new MatterDeviceError(`Cannot register Matter accessory "${accessory.displayName}": `
910
+ + `Maximum device limit reached (${this.MAX_DEVICES} devices).\n`
911
+ + `Current registered devices: ${this.accessories.size}`);
912
+ }
913
+ try {
914
+ // Prepare device type with WindowCovering features
915
+ let deviceType = accessory.deviceType;
916
+ const windowCoveringFeatures = detectWindowCoveringFeatures(accessory);
917
+ if (windowCoveringFeatures.length > 0) {
918
+ deviceType = applyWindowCoveringFeatures(deviceType, accessory, windowCoveringFeatures);
919
+ }
920
+ // Detect cluster features for behavior configuration
921
+ const features = this.detectClusterFeatures(accessory, deviceType);
922
+ // Build and apply custom behaviors based on handlers
923
+ const customBehaviors = await this.buildCustomBehaviors(accessory, deviceType, features);
924
+ if (customBehaviors.length > 0) {
925
+ deviceType = withBehaviors(deviceType, customBehaviors);
926
+ log.info(`Applied ${customBehaviors.length} custom behavior(s) to device type`);
927
+ }
928
+ // Add BridgedDeviceBasicInformationServer for bridged devices only
929
+ // This is required by the Matter spec for devices behind an aggregator
930
+ // External accessories should NOT have this cluster
931
+ if (!this.config.externalAccessory) {
932
+ deviceType = withBehaviors(deviceType, [BridgedDeviceBasicInformationServer]);
933
+ log.debug(`Added BridgedDeviceBasicInformationServer to ${accessory.displayName}`);
934
+ }
935
+ // Create endpoint with cluster states
936
+ const endpointOptions = this.createEndpointOptions(accessory);
937
+ const endpoint = new Endpoint(deviceType, endpointOptions);
938
+ if (this.config.debugModeEnabled) {
939
+ log.debug(`Created endpoint for ${accessory.displayName} with initial cluster states`);
940
+ }
941
+ // Add endpoint to aggregator or serverNode depending on mode
942
+ if (this.config.externalAccessory) {
943
+ await this.serverNode.add(endpoint);
944
+ log.debug(`Added ${accessory.displayName} as external accessory to ServerNode`);
945
+ }
946
+ else {
947
+ await this.aggregator.add(endpoint);
948
+ if (this.config.debugModeEnabled) {
949
+ log.debug(`Added endpoint for ${accessory.displayName} to aggregator`);
950
+ }
951
+ }
952
+ // Register command handlers
953
+ this.registerAccessoryHandlers(accessory);
954
+ // Create and register child endpoints (parts)
955
+ const internalParts = await this.createAccessoryParts(accessory);
956
+ // Finalize registration (store, emit events, save cache)
957
+ await this.finalizeAccessoryRegistration(accessory, endpoint, internalParts);
958
+ }
959
+ catch (error) {
960
+ log.error(`Failed to register Matter accessory ${accessory.displayName}:`, error);
961
+ throw new MatterDeviceError(`Failed to register accessory: ${error}`);
962
+ }
963
+ }
964
+ /**
965
+ * Restore cached state for an accessory
966
+ */
967
+ restoreCachedState(accessory) {
968
+ // Check if there's a cached version - merge cached cluster states with new registration.
969
+ // This ensures state persistence across Homebridge restarts.
970
+ if (this.accessoryCache && this.accessoryCache.hasCached(accessory.uuid)) {
971
+ const cached = this.accessoryCache.getCached(accessory.uuid);
972
+ if (cached?.clusters && accessory.clusters) {
973
+ // Merge cached cluster states with new ones (prefer cached state to persist values across restarts)
974
+ for (const [clusterName, cachedAttrs] of Object.entries(cached.clusters)) {
975
+ if (!accessory.clusters[clusterName]) {
976
+ // Cluster exists in cache but not in new registration - preserve it
977
+ accessory.clusters[clusterName] = cachedAttrs;
978
+ }
979
+ else {
980
+ // Cluster exists in both - merge (prefer cached state over initial values)
981
+ accessory.clusters[clusterName] = {
982
+ ...accessory.clusters[clusterName],
983
+ ...cachedAttrs,
984
+ };
985
+ }
986
+ }
987
+ // Restore context if available
988
+ if (cached.context) {
989
+ accessory.context = cached.context;
990
+ }
991
+ log.info(`Restored cached state for Matter accessory: ${accessory.displayName}`);
992
+ }
993
+ }
994
+ }
995
+ /**
996
+ * Detect cluster features for an accessory
997
+ * Returns an object containing detected features for various clusters
998
+ */
999
+ detectClusterFeatures(accessory, deviceType) {
1000
+ // Detect WindowCovering features
1001
+ const windowCoveringFeatures = detectWindowCoveringFeatures(accessory);
1002
+ // Detect ServiceArea features
1003
+ let serviceAreaFeatures = null;
1004
+ if (accessory.clusters?.serviceArea) {
1005
+ const features = [];
1006
+ // Check if Maps feature should be enabled (when supportedMaps is defined)
1007
+ if (accessory.clusters.serviceArea.supportedMaps) {
1008
+ features.push('Maps');
1009
+ }
1010
+ // Check if ProgressReporting feature should be enabled (when progress is defined)
1011
+ if (accessory.clusters.serviceArea.progress !== undefined) {
1012
+ features.push('ProgressReporting');
1013
+ }
1014
+ if (features.length > 0) {
1015
+ serviceAreaFeatures = features;
1016
+ log.info(`ServiceArea features will be enabled for ${accessory.displayName}: ${features.join(', ')}`);
1017
+ }
1018
+ }
1019
+ // Detect ColorControl features
1020
+ let colorControlFeatures = null;
1021
+ if (accessory.handlers?.colorControl) {
1022
+ colorControlFeatures = detectBehaviorFeatures(deviceType, CLUSTER_IDS.COLOR_CONTROL, extractColorControlFeatures);
1023
+ if (colorControlFeatures) {
1024
+ colorControlFeatures = determineColorControlFeaturesFromHandlers(accessory.handlers.colorControl);
1025
+ }
1026
+ }
1027
+ // Detect Thermostat features
1028
+ let thermostatFeatures = null;
1029
+ if (accessory.handlers?.thermostat) {
1030
+ thermostatFeatures = detectBehaviorFeatures(deviceType, CLUSTER_IDS.THERMOSTAT, extractThermostatFeatures);
1031
+ }
1032
+ return {
1033
+ windowCoveringFeatures,
1034
+ serviceAreaFeatures,
1035
+ colorControlFeatures,
1036
+ thermostatFeatures,
1037
+ };
1038
+ }
1039
+ /**
1040
+ * Build custom behaviors for an accessory based on handlers
1041
+ */
1042
+ async buildCustomBehaviors(accessory, deviceType, features) {
1043
+ const customBehaviors = [];
1044
+ if (!accessory.handlers) {
1045
+ return customBehaviors;
1046
+ }
1047
+ log.debug(`[${accessory.displayName}] Has handlers: ${Object.keys(accessory.handlers).join(', ')}`);
1048
+ // Use the static cluster behavior map
1049
+ const behaviorMap = MatterServer.CLUSTER_BEHAVIOR_MAP;
1050
+ // For RoboticVacuumCleaner, add optional clusters if they're defined in accessory.clusters
1051
+ // These clusters need to be added to the device type even if there are no handlers
1052
+ if (isDeviceType(deviceType, devices.RoboticVacuumCleanerDevice)) {
1053
+ // Import RVC requirements
1054
+ const { RvcCleanModeServer, ServiceAreaServer } = devices.RoboticVacuumCleanerRequirements;
1055
+ // Add RvcCleanMode if defined in clusters
1056
+ if (accessory.clusters?.rvcCleanMode) {
1057
+ // Check if there's a custom behavior with handlers
1058
+ if (accessory.handlers?.rvcCleanMode) {
1059
+ const behaviorClass = HomebridgeRvcCleanModeServer;
1060
+ customBehaviors.push(behaviorClass);
1061
+ log.info('Adding custom RvcCleanMode behavior with handlers');
1062
+ }
1063
+ else {
1064
+ // No handlers, use base server
1065
+ customBehaviors.push(RvcCleanModeServer);
1066
+ log.info('Adding base RvcCleanMode server');
1067
+ }
1068
+ }
1069
+ // Add ServiceArea if defined in clusters
1070
+ if (accessory.clusters?.serviceArea) {
1071
+ // Check if there's a custom behavior with handlers
1072
+ if (accessory.handlers?.serviceArea) {
1073
+ let behaviorClass = HomebridgeServiceAreaServer;
1074
+ // Apply features if detected
1075
+ if (features.serviceAreaFeatures && features.serviceAreaFeatures.length > 0) {
1076
+ behaviorClass = withFeatures(behaviorClass, features.serviceAreaFeatures);
1077
+ log.info(`ServiceArea custom behavior will have features: ${features.serviceAreaFeatures.join(', ')}`);
1078
+ }
1079
+ customBehaviors.push(behaviorClass);
1080
+ log.info('Adding custom ServiceArea behavior with handlers');
1081
+ }
1082
+ else {
1083
+ // No handlers, use base server with features
1084
+ let behaviorClass = ServiceAreaServer;
1085
+ if (features.serviceAreaFeatures && features.serviceAreaFeatures.length > 0) {
1086
+ behaviorClass = withFeatures(behaviorClass, features.serviceAreaFeatures);
1087
+ log.info(`ServiceArea base server will have features: ${features.serviceAreaFeatures.join(', ')}`);
1088
+ }
1089
+ customBehaviors.push(behaviorClass);
1090
+ log.info('Adding base ServiceArea server');
1091
+ }
1092
+ }
1093
+ // Add PowerSource if defined in clusters (for battery percentage)
1094
+ if (accessory.clusters?.powerSource) {
1095
+ // Detect Battery feature from cluster config
1096
+ const hasBattery = accessory.clusters.powerSource.batPercentRemaining !== undefined
1097
+ || accessory.clusters.powerSource.batChargeLevel !== undefined;
1098
+ let powerSourceBehavior = PowerSourceServer;
1099
+ if (hasBattery) {
1100
+ powerSourceBehavior = withFeatures(PowerSourceServer, ['Battery']);
1101
+ log.debug('Adding PowerSource server with battery feature');
1102
+ }
1103
+ else {
1104
+ log.debug('Adding base PowerSource server');
1105
+ }
1106
+ customBehaviors.push(powerSourceBehavior);
1107
+ }
1108
+ }
1109
+ for (const clusterName of Object.keys(accessory.handlers || {})) {
1110
+ // Skip windowCovering if we already applied features via base WindowCoveringServer
1111
+ const skipWindowCoveringBehavior = accessory.context?._skipWindowCoveringBehavior;
1112
+ if (clusterName === 'windowCovering' && skipWindowCoveringBehavior) {
1113
+ log.debug('Skipping custom WindowCovering behavior (using base server with features instead)');
1114
+ continue;
1115
+ }
1116
+ // Skip RVC clusters - they're handled specially above for RoboticVacuumCleaner
1117
+ if (clusterName === 'rvcCleanMode' || clusterName === 'serviceArea' || clusterName === 'powerSource') {
1118
+ continue;
1119
+ }
1120
+ let behaviorClass = behaviorMap[clusterName];
1121
+ // Apply ColorControl features if we detected them earlier
1122
+ if (clusterName === 'colorControl' && behaviorClass && features.colorControlFeatures && features.colorControlFeatures.length > 0) {
1123
+ behaviorClass = withFeatures(behaviorClass, features.colorControlFeatures);
1124
+ log.info(`ColorControl custom behavior will preserve features: ${features.colorControlFeatures.join(', ')}`);
1125
+ }
1126
+ // Apply Thermostat features if we detected them earlier
1127
+ if (clusterName === 'thermostat' && behaviorClass && features.thermostatFeatures && features.thermostatFeatures.length > 0) {
1128
+ behaviorClass = withFeatures(behaviorClass, features.thermostatFeatures);
1129
+ log.info(`Thermostat custom behavior will preserve features: ${features.thermostatFeatures.join(', ')}`);
1130
+ }
1131
+ // Apply ServiceArea features if we detected them earlier
1132
+ if (clusterName === 'serviceArea' && behaviorClass && features.serviceAreaFeatures && features.serviceAreaFeatures.length > 0) {
1133
+ behaviorClass = withFeatures(behaviorClass, features.serviceAreaFeatures);
1134
+ log.info(`ServiceArea custom behavior will preserve features: ${features.serviceAreaFeatures.join(', ')}`);
1135
+ }
1136
+ // Apply WindowCovering features to custom behavior as well
1137
+ // (features were already applied to base device type, but custom behavior needs them too)
1138
+ if (clusterName === 'windowCovering') {
1139
+ log.debug(`WindowCovering handler found: behaviorClass=${!!behaviorClass}, windowCoveringFeatures=${features.windowCoveringFeatures}, length=${features.windowCoveringFeatures?.length}`);
1140
+ if (behaviorClass && features.windowCoveringFeatures && features.windowCoveringFeatures.length > 0) {
1141
+ behaviorClass = withFeatures(behaviorClass, features.windowCoveringFeatures);
1142
+ log.debug(`WindowCovering custom behavior will have features: ${features.windowCoveringFeatures.join(', ')}`);
1143
+ }
1144
+ else {
1145
+ log.debug(`Skipping WindowCovering feature application: behaviorClass=${!!behaviorClass}, features=${features.windowCoveringFeatures}`);
1146
+ }
1147
+ }
1148
+ if (behaviorClass) {
1149
+ customBehaviors.push(behaviorClass);
1150
+ log.info(`Will use ${behaviorClass.name} for ${accessory.displayName}`);
1151
+ }
1152
+ else {
1153
+ log.warn(`No custom behavior class available for cluster '${clusterName}' - handlers will be registered but may not be called`);
1154
+ }
1155
+ }
1156
+ return customBehaviors;
1157
+ }
1158
+ /**
1159
+ * Create endpoint options for an accessory
1160
+ */
1161
+ createEndpointOptions(accessory) {
1162
+ const endpointOptions = {
1163
+ id: accessory.uuid,
1164
+ ...accessory.clusters, // Spread cluster states as initial values
1165
+ };
1166
+ // Add bridgedDeviceBasicInformation cluster only for bridged devices
1167
+ // For external accessories, use the root basicInformation instead
1168
+ if (!this.config.externalAccessory) {
1169
+ endpointOptions.bridgedDeviceBasicInformation = {
1170
+ vendorName: accessory.manufacturer,
1171
+ nodeLabel: accessory.displayName, // Main end user name for the device
1172
+ productName: accessory.model,
1173
+ productLabel: accessory.displayName,
1174
+ serialNumber: accessory.serialNumber,
1175
+ reachable: true,
1176
+ };
1177
+ }
1178
+ return endpointOptions;
1179
+ }
1180
+ /**
1181
+ * Register command handlers for an accessory
1182
+ */
1183
+ registerAccessoryHandlers(accessory) {
1184
+ if (!accessory.handlers) {
1185
+ return;
1186
+ }
1187
+ log.info(`Setting up handlers for accessory ${accessory.uuid}`);
1188
+ // Register handlers with the custom behavior classes
1189
+ for (const [clusterName, handlers] of Object.entries(accessory.handlers)) {
1190
+ log.info(` Processing cluster: ${clusterName}`);
1191
+ for (const [commandName, handler] of Object.entries(handlers)) {
1192
+ this.behaviorRegistry.registerHandler(accessory.uuid, clusterName, commandName, handler);
1193
+ }
1194
+ }
1195
+ }
1196
+ /**
1197
+ * Create and register child endpoints (parts) for an accessory
1198
+ */
1199
+ async createAccessoryParts(accessory) {
1200
+ const internalParts = [];
1201
+ if (!accessory.parts || accessory.parts.length === 0) {
1202
+ return internalParts;
1203
+ }
1204
+ log.info(`Creating ${accessory.parts.length} child endpoint(s) for ${accessory.displayName}`);
1205
+ for (const part of accessory.parts) {
1206
+ // Create unique endpoint ID for this part
1207
+ const partEndpointId = `${accessory.uuid}-part-${part.id}`;
1208
+ // Register the part endpoint mapping for handler context
1209
+ this.behaviorRegistry.registerPartEndpoint(partEndpointId, accessory.uuid, part.id);
1210
+ // Apply custom behaviors to part based on its handlers (same logic as main accessory)
1211
+ let partDeviceType = part.deviceType;
1212
+ const partCustomBehaviors = [];
1213
+ if (part.handlers) {
1214
+ // Use the static cluster behavior map for parts as well
1215
+ const partBehaviorMap = MatterServer.CLUSTER_BEHAVIOR_MAP;
1216
+ for (const clusterName of Object.keys(part.handlers)) {
1217
+ const behaviorClass = partBehaviorMap[clusterName];
1218
+ if (behaviorClass) {
1219
+ partCustomBehaviors.push(behaviorClass);
1220
+ log.info(` Will use ${behaviorClass.name} for part ${part.id}`);
1221
+ }
1222
+ else {
1223
+ log.warn(`No custom behavior class available for cluster '${clusterName}' on part ${part.id}`);
1224
+ }
1225
+ }
1226
+ if (partCustomBehaviors.length > 0) {
1227
+ // Add custom behaviors to part device type
1228
+ partDeviceType = withBehaviors(partDeviceType, partCustomBehaviors);
1229
+ log.info(` Applied ${partCustomBehaviors.length} custom behavior(s) to part ${part.id}`);
1230
+ }
1231
+ }
1232
+ // Add BridgedDeviceBasicInformationServer for bridged parts
1233
+ if (!this.config.externalAccessory) {
1234
+ partDeviceType = withBehaviors(partDeviceType, [BridgedDeviceBasicInformationServer]);
1235
+ }
1236
+ // Create endpoint options with cluster states
1237
+ const partEndpointOptions = {
1238
+ id: partEndpointId,
1239
+ ...part.clusters,
1240
+ };
1241
+ // Add bridgedDeviceBasicInformation for the part
1242
+ if (!this.config.externalAccessory) {
1243
+ partEndpointOptions.bridgedDeviceBasicInformation = {
1244
+ vendorName: accessory.manufacturer,
1245
+ nodeLabel: part.displayName || `${accessory.displayName} - ${part.id}`,
1246
+ productName: accessory.model,
1247
+ productLabel: part.displayName || part.id,
1248
+ serialNumber: `${accessory.serialNumber}-${part.id}`,
1249
+ reachable: true,
1250
+ };
1251
+ }
1252
+ // Create the part endpoint
1253
+ const partEndpoint = new Endpoint(partDeviceType, partEndpointOptions);
1254
+ // Add part endpoint to aggregator or serverNode
1255
+ if (this.config.externalAccessory) {
1256
+ await this.serverNode.add(partEndpoint);
1257
+ }
1258
+ else {
1259
+ await this.aggregator.add(partEndpoint);
1260
+ }
1261
+ log.info(` Created part endpoint: ${part.displayName || part.id} (${partEndpointId})`);
1262
+ // Set up handlers for this part
1263
+ if (part.handlers) {
1264
+ for (const [clusterName, handlers] of Object.entries(part.handlers)) {
1265
+ for (const [commandName, handler] of Object.entries(handlers)) {
1266
+ // Register handler with the part's endpoint ID
1267
+ this.behaviorRegistry.registerHandler(partEndpointId, clusterName, commandName, handler);
1268
+ }
1269
+ }
1270
+ log.debug(` Registered ${Object.keys(part.handlers).length} handler(s) for part ${part.id}`);
1271
+ }
1272
+ // Store the internal part
1273
+ internalParts.push({
1274
+ ...part,
1275
+ endpoint: partEndpoint,
1276
+ });
1277
+ }
1278
+ return internalParts;
1279
+ }
1280
+ /**
1281
+ * Finalize accessory registration (store, emit events, save cache)
1282
+ */
1283
+ async finalizeAccessoryRegistration(accessory, endpoint, internalParts) {
1284
+ // Store accessory with internal metadata and event emitter
1285
+ // The event emitter allows plugins to listen for lifecycle events (currently only 'ready')
1286
+ // Note: _associatedPlugin and _associatedPlatform are already set by MatterAPIImpl
1287
+ const internalAccessory = {
1288
+ ...accessory,
1289
+ endpoint,
1290
+ registered: true,
1291
+ _parts: internalParts.length > 0 ? internalParts : undefined,
1292
+ _eventEmitter: new EventEmitter(),
1293
+ };
1294
+ this.accessories.set(accessory.uuid, internalAccessory);
1295
+ log.info(`Registered Matter accessory: ${accessory.displayName} (${accessory.uuid})`);
1296
+ if (this.config.debugModeEnabled) {
1297
+ log.debug(`Total registered accessories: ${this.accessories.size}/${this.MAX_DEVICES}`);
1298
+ }
1299
+ // Notify controllers about the new device (parts list changed)
1300
+ // This allows the Home app to discover new devices without re-pairing
1301
+ await this.notifyPartsListChanged();
1302
+ // Request debounced save to cache (reduces disk I/O during rapid registration)
1303
+ if (this.accessoryCache) {
1304
+ this.accessoryCache.requestSave(this.accessories);
1305
+ }
1306
+ }
1307
+ /**
1308
+ * Unregister a Matter accessory (Plugin API)
1309
+ */
1310
+ async unregisterAccessory(uuid) {
1311
+ const accessory = this.accessories.get(uuid);
1312
+ if (!accessory) {
1313
+ // Accessory not in memory, but might be in cache - still remove from cache
1314
+ log.debug(`Accessory ${uuid} not found or not registered`);
1315
+ // Check if it exists in cache and remove it
1316
+ if (this.accessoryCache && this.accessoryCache.getCached(uuid)) {
1317
+ log.debug(`Removing ${uuid} from cache`);
1318
+ this.accessoryCache.removeCached(uuid);
1319
+ this.accessoryCache.requestSave(this.accessories);
1320
+ }
1321
+ return;
1322
+ }
1323
+ try {
1324
+ if (accessory.endpoint && this.aggregator) {
1325
+ await accessory.endpoint.close();
1326
+ log.debug(`Removed endpoint for ${accessory.displayName}`);
1327
+ }
1328
+ this.accessories.delete(uuid);
1329
+ log.info(`Unregistered Matter accessory: ${accessory.displayName} (${uuid})`);
1330
+ // Notify controllers about the removed device (parts list changed)
1331
+ await this.notifyPartsListChanged();
1332
+ // Update cache (remove the accessory)
1333
+ if (this.accessoryCache) {
1334
+ this.accessoryCache.removeCached(uuid);
1335
+ this.accessoryCache.requestSave(this.accessories);
1336
+ }
1337
+ }
1338
+ catch (error) {
1339
+ log.error(`Failed to unregister Matter accessory ${uuid}:`, error);
1340
+ throw new MatterDeviceError(`Failed to unregister accessory: ${error}`);
1341
+ }
1342
+ }
1343
+ /**
1344
+ * Update a Matter accessory's state (Plugin API)
1345
+ *
1346
+ * This method can be called from anywhere, including from within handlers.
1347
+ * State updates are automatically deferred to avoid transaction conflicts.
1348
+ *
1349
+ * @param uuid - The UUID of the accessory
1350
+ * @param cluster - The cluster name
1351
+ * @param attributes - The attributes to update
1352
+ * @param partId - Optional: ID of the part to update (for composed devices)
1353
+ */
1354
+ async updateAccessoryState(uuid, cluster, attributes, partId) {
1355
+ const accessory = this.accessories.get(uuid);
1356
+ if (!accessory) {
1357
+ throw new MatterDeviceError(`Accessory ${uuid} not found or not registered`);
1358
+ }
1359
+ // Determine which endpoint to update
1360
+ let targetEndpoint;
1361
+ let targetClusters;
1362
+ let displayName;
1363
+ if (partId) {
1364
+ // Update a specific part
1365
+ const part = accessory._parts?.find(p => p.id === partId);
1366
+ if (!part || !part.endpoint) {
1367
+ throw new MatterDeviceError(`Part ${partId} not found in accessory ${uuid}`);
1368
+ }
1369
+ targetEndpoint = part.endpoint;
1370
+ targetClusters = part.clusters;
1371
+ displayName = part.displayName || `${accessory.displayName} - ${partId}`;
1372
+ }
1373
+ else {
1374
+ // Update the main accessory
1375
+ if (!accessory.endpoint) {
1376
+ throw new MatterDeviceError(`Accessory ${uuid} not registered or missing endpoint`);
1377
+ }
1378
+ targetEndpoint = accessory.endpoint;
1379
+ targetClusters = accessory.clusters;
1380
+ displayName = accessory.displayName;
1381
+ }
1382
+ // Defer the update to avoid "read-only transaction" errors when called from handlers
1383
+ // Matter.js uses transactions, and we need to escape the current call stack
1384
+ // setImmediate ensures we're in a new event loop tick without arbitrary delays
1385
+ return new Promise((resolve, reject) => {
1386
+ setImmediate(async () => {
1387
+ try {
1388
+ // Construct the update object
1389
+ const updateObject = { [cluster]: attributes };
1390
+ // Use endpoint.set() which properly handles state updates
1391
+ await targetEndpoint.set(updateObject);
1392
+ // CRITICAL: Also update the cached clusters object so state persists across restarts
1393
+ // Merge the new attributes into the existing cluster state
1394
+ if (!targetClusters) {
1395
+ log.warn(`Target clusters undefined for ${displayName}, cannot cache state`);
1396
+ }
1397
+ else {
1398
+ if (!targetClusters[cluster]) {
1399
+ targetClusters[cluster] = {};
1400
+ }
1401
+ targetClusters[cluster] = {
1402
+ ...targetClusters[cluster],
1403
+ ...attributes,
1404
+ };
1405
+ }
1406
+ const partInfo = partId ? ` (part: ${partId})` : '';
1407
+ log.debug(`Updated ${cluster} state for ${displayName}${partInfo}:`, attributes);
1408
+ resolve();
1409
+ }
1410
+ catch (error) {
1411
+ const partInfo = partId ? ` part ${partId}` : '';
1412
+ log.error(`Failed to update state for accessory ${uuid}${partInfo}:`, error);
1413
+ reject(new MatterDeviceError(`Failed to update accessory state: ${error}`));
1414
+ }
1415
+ });
1416
+ });
1417
+ }
1418
+ /**
1419
+ * Get a Matter accessory's current state (Plugin API)
1420
+ *
1421
+ * Returns the current cluster attribute values that are exposed to Matter controllers.
1422
+ * This is useful for:
1423
+ * - Reading state after plugin restart (when local variables are lost)
1424
+ * - Verifying current state before making changes
1425
+ * - Multiple parts of code that need to read state
1426
+ * - Debugging and logging
1427
+ *
1428
+ * @param uuid - The UUID of the accessory
1429
+ * @param cluster - The cluster name (e.g., 'onOff', 'levelControl')
1430
+ * @param partId - Optional: ID of the part to get state from (for composed devices)
1431
+ * @returns Current cluster attribute values, or undefined if cluster not found
1432
+ */
1433
+ getAccessoryState(uuid, cluster, partId) {
1434
+ const accessory = this.accessories.get(uuid);
1435
+ if (!accessory) {
1436
+ log.debug(`Accessory ${uuid} not found`);
1437
+ return undefined;
1438
+ }
1439
+ // Determine which endpoint to read from
1440
+ let targetEndpoint;
1441
+ let displayName;
1442
+ if (partId) {
1443
+ const part = accessory._parts?.find(p => p.id === partId);
1444
+ if (!part || !part.endpoint) {
1445
+ log.debug(`Part ${partId} not found in accessory ${uuid}`);
1446
+ return undefined;
1447
+ }
1448
+ targetEndpoint = part.endpoint;
1449
+ displayName = part.displayName || `${accessory.displayName} - ${partId}`;
1450
+ }
1451
+ else {
1452
+ if (!accessory.endpoint) {
1453
+ log.debug(`Accessory ${uuid} not registered or missing endpoint`);
1454
+ return undefined;
1455
+ }
1456
+ targetEndpoint = accessory.endpoint;
1457
+ displayName = accessory.displayName;
1458
+ }
1459
+ try {
1460
+ if (!targetEndpoint.state) {
1461
+ log.debug(`endpoint.state is undefined for ${displayName}`);
1462
+ return undefined;
1463
+ }
1464
+ if (!targetEndpoint.state[cluster]) {
1465
+ const availableClusters = Object.keys(targetEndpoint.state || {});
1466
+ log.debug(`Cluster '${cluster}' not found on ${displayName}. Available: ${availableClusters.join(', ')}`);
1467
+ return undefined;
1468
+ }
1469
+ const clusterState = targetEndpoint.state[cluster];
1470
+ // Build result object by reading each property directly
1471
+ const result = {};
1472
+ // Get list of properties to read - use both approaches for maximum compatibility
1473
+ const allKeys = new Set([
1474
+ ...Object.keys(clusterState),
1475
+ ...Object.getOwnPropertyNames(clusterState),
1476
+ ]);
1477
+ for (const key of allKeys) {
1478
+ try {
1479
+ // Skip internal properties, methods, and symbols
1480
+ if (key.startsWith('_') || key.startsWith('$')) {
1481
+ continue;
1482
+ }
1483
+ // Try to read the value directly
1484
+ const value = clusterState[key];
1485
+ // Skip functions and undefined values
1486
+ if (typeof value === 'function' || value === undefined) {
1487
+ continue;
1488
+ }
1489
+ result[key] = value;
1490
+ }
1491
+ catch (propError) {
1492
+ log.debug(`Could not read property ${key} from ${cluster}:`, propError);
1493
+ }
1494
+ }
1495
+ if (Object.keys(result).length === 0) {
1496
+ log.debug(`Cluster ${cluster} found but no readable properties on accessory ${accessory.displayName}`);
1497
+ return undefined;
1498
+ }
1499
+ return result;
1500
+ }
1501
+ catch (error) {
1502
+ log.error(`Failed to get state for accessory ${uuid}:`, error);
1503
+ return undefined;
1504
+ }
1505
+ }
1506
+ /**
1507
+ * Get all cached accessories (Internal - for restore process)
1508
+ * @internal
1509
+ */
1510
+ getAllCachedAccessories() {
1511
+ if (!this.accessoryCache) {
1512
+ log.debug('getAllCachedAccessories: No cache available');
1513
+ return [];
1514
+ }
1515
+ const cached = Array.from(this.accessoryCache.getAllCached().values());
1516
+ log.debug(`getAllCachedAccessories: Returning ${cached.length} accessories`);
1517
+ return cached;
1518
+ }
1519
+ /**
1520
+ * Get all registered accessories (Plugin API)
1521
+ */
1522
+ getAccessories() {
1523
+ return Array.from(this.accessories.values()).map((acc) => {
1524
+ // Return copy without internal fields
1525
+ // eslint-disable-next-line unused-imports/no-unused-vars
1526
+ const { endpoint, registered, ...publicAccessory } = acc;
1527
+ return publicAccessory;
1528
+ });
1529
+ }
1530
+ /**
1531
+ * Get a specific accessory by UUID (Plugin API)
1532
+ */
1533
+ getAccessory(uuid) {
1534
+ const accessory = this.accessories.get(uuid);
1535
+ if (!accessory) {
1536
+ return undefined;
1537
+ }
1538
+ // Return copy without internal fields
1539
+ // eslint-disable-next-line unused-imports/no-unused-vars
1540
+ const { endpoint, registered, ...publicAccessory } = accessory;
1541
+ return publicAccessory;
1542
+ }
1543
+ /**
1544
+ * Stop the Matter server
1545
+ */
1546
+ async stop() {
1547
+ if (!this.isRunning) {
1548
+ log.debug('Matter server is not running');
1549
+ return;
1550
+ }
1551
+ this.isRunning = false;
1552
+ try {
1553
+ // Save accessory cache before shutting down (BEFORE clearing accessories!)
1554
+ if (this.accessoryCache && this.accessories.size > 0) {
1555
+ await this.accessoryCache.save(this.accessories);
1556
+ log.debug('Saved accessory cache before shutdown');
1557
+ }
1558
+ // Stop server (this will close all child endpoints automatically)
1559
+ // Note: We don't manually close endpoints here because they're part of the ServerNode
1560
+ // hierarchy and will be closed by serverNode.close()
1561
+ if (this.serverNode) {
1562
+ await this.serverNode.close();
1563
+ log.debug('ServerNode closed (all endpoints cleaned up)');
1564
+ }
1565
+ // Clear accessories map after server is stopped
1566
+ this.accessories.clear();
1567
+ await this.cleanup();
1568
+ log.info('Matter server stopped');
1569
+ }
1570
+ catch (error) {
1571
+ log.error('Error stopping Matter server:', error);
1572
+ await errorHandler.handleError(error, 'server-stop');
1573
+ throw error;
1574
+ }
1575
+ finally {
1576
+ this.isRunning = false;
1577
+ }
1578
+ }
1579
+ /**
1580
+ * Cleanup resources
1581
+ */
1582
+ async cleanup() {
1583
+ // Remove signal handlers
1584
+ if (this.shutdownHandler) {
1585
+ process.off('SIGINT', this.shutdownHandler);
1586
+ process.off('SIGTERM', this.shutdownHandler);
1587
+ this.shutdownHandler = null;
1588
+ }
1589
+ // Run all cleanup handlers
1590
+ for (const handler of this.cleanupHandlers) {
1591
+ try {
1592
+ await handler();
1593
+ }
1594
+ catch (error) {
1595
+ log.debug('Error during cleanup handler:', error);
1596
+ }
1597
+ }
1598
+ this.cleanupHandlers = [];
1599
+ // Clear references
1600
+ this.serverNode = null;
1601
+ this.aggregator = null;
1602
+ this.isRunning = false;
1603
+ this.commissioningInfo = {};
1604
+ }
1605
+ /**
1606
+ * Get fabric information for commissioned controllers
1607
+ *
1608
+ * Returns information about each paired controller (fabric) including:
1609
+ * - fabricIndex: Unique identifier for the fabric
1610
+ * - fabricId: 64-bit fabric identifier
1611
+ * - nodeId: Node identifier within the fabric
1612
+ * - rootVendorId: Vendor ID of the root node
1613
+ * - label: Optional human-readable label
1614
+ *
1615
+ * @returns Array of fabric information objects, or empty array if no fabrics are commissioned
1616
+ * @example
1617
+ * ```typescript
1618
+ * const fabrics = matterServer.getFabricInfo()
1619
+ * console.log(`Commissioned to ${fabrics.length} controller(s)`)
1620
+ * fabrics.forEach(fabric => {
1621
+ * console.log(` Fabric ${fabric.fabricIndex}: ${fabric.label || 'Unnamed'}`)
1622
+ * })
1623
+ * ```
1624
+ */
1625
+ getFabricInfo() {
1626
+ try {
1627
+ if (!this.storageManager) {
1628
+ return [];
1629
+ }
1630
+ // Fabric data is stored in the main storage file (uniqueId namespace)
1631
+ // with the key "fabrics.fabrics"
1632
+ // Note: We can read this directly from storage even before serverNode is initialized
1633
+ const storage = this.storageManager.getStorage(this.config.uniqueId);
1634
+ const fabricsData = storage.get(['fabrics'], 'fabrics');
1635
+ if (Array.isArray(fabricsData) && fabricsData.length > 0) {
1636
+ // Map the fabric data to our interface
1637
+ return fabricsData.map(fabric => ({
1638
+ fabricIndex: fabric.fabricIndex || 0,
1639
+ fabricId: fabric.fabricId?.value?.toString() || '',
1640
+ nodeId: fabric.nodeId?.value?.toString() || '',
1641
+ rootVendorId: fabric.rootVendorId || 0,
1642
+ label: fabric.label || '',
1643
+ }));
1644
+ }
1645
+ return [];
1646
+ }
1647
+ catch (error) {
1648
+ log.debug('Failed to get fabric info from storage:', error);
1649
+ return [];
1650
+ }
1651
+ }
1652
+ /**
1653
+ * Check if the server is commissioned
1654
+ */
1655
+ isCommissioned() {
1656
+ try {
1657
+ if (!this.storageManager) {
1658
+ return false;
1659
+ }
1660
+ // Commissioned status is stored in the main storage file (uniqueId namespace)
1661
+ // at key "root.commissioning.commissioned"
1662
+ const storage = this.storageManager.getStorage(this.config.uniqueId);
1663
+ const commissioned = storage.get(['root', 'commissioning'], 'commissioned');
1664
+ if (commissioned === true) {
1665
+ return true;
1666
+ }
1667
+ // Fallback to checking fabric count if commissioned flag not found
1668
+ const fabrics = this.getFabricInfo();
1669
+ return fabrics.length > 0;
1670
+ }
1671
+ catch (error) {
1672
+ log.debug('Failed to check commissioned status from storage:', error);
1673
+ return false;
1674
+ }
1675
+ }
1676
+ /**
1677
+ * Get the number of commissioned fabrics
1678
+ */
1679
+ getCommissionedFabricCount() {
1680
+ return this.getFabricInfo().length;
1681
+ }
1682
+ /**
1683
+ * Get server status information
1684
+ */
1685
+ getServerInfo() {
1686
+ return {
1687
+ running: this.isRunning,
1688
+ port: this.config.port || 5540,
1689
+ deviceCount: this.accessories.size,
1690
+ commissioned: this.isCommissioned(),
1691
+ fabricCount: this.getCommissionedFabricCount(),
1692
+ serialNumber: this.serialNumber,
1693
+ };
1694
+ }
1695
+ /**
1696
+ * Get commissioning information
1697
+ */
1698
+ getCommissioningInfo() {
1699
+ return {
1700
+ ...this.commissioningInfo,
1701
+ serialNumber: this.serialNumber,
1702
+ passcode: this.passcode,
1703
+ discriminator: this.discriminator,
1704
+ commissioned: this.isCommissioned(),
1705
+ };
1706
+ }
1707
+ /**
1708
+ * Get storage statistics
1709
+ */
1710
+ getStorageStats() {
1711
+ if (!this.storageManager) {
1712
+ return null;
1713
+ }
1714
+ return this.storageManager.getAllStats();
1715
+ }
1716
+ /**
1717
+ * Check if server is running
1718
+ */
1719
+ isServerRunning() {
1720
+ return this.isRunning;
1721
+ }
1722
+ /**
1723
+ * Get Matter device types available for plugin use
1724
+ */
1725
+ getDeviceTypes() {
1726
+ return deviceTypes;
1727
+ }
1728
+ /**
1729
+ * Get Matter clusters available for plugin use
1730
+ */
1731
+ getClusters() {
1732
+ return clusters;
1733
+ }
1734
+ /**
1735
+ * Remove a specific fabric (controller) from the bridge
1736
+ * This decommissions a single controller while leaving others intact
1737
+ *
1738
+ * @param fabricIndex - The fabric index to remove
1739
+ * @returns Promise that resolves when the fabric is removed
1740
+ */
1741
+ async removeFabric(fabricIndex) {
1742
+ if (!this.serverNode) {
1743
+ throw new MatterDeviceError('Matter server not started');
1744
+ }
1745
+ try {
1746
+ log.info(`Removing fabric ${fabricIndex}...`);
1747
+ const serverState = this.serverNode;
1748
+ const removeFabric = serverState?.state?.commissioning?.removeFabric;
1749
+ if (typeof removeFabric !== 'function') {
1750
+ throw new MatterDeviceError('Fabric removal not supported by Matter.js version');
1751
+ }
1752
+ // Remove the fabric
1753
+ await removeFabric(fabricIndex);
1754
+ log.info(`Fabric ${fabricIndex} removed successfully`);
1755
+ // The fabric monitoring will detect this change and emit the appropriate events
1756
+ }
1757
+ catch (error) {
1758
+ const errorMessage = error instanceof Error ? error.message : String(error);
1759
+ log.error(`Failed to remove fabric ${fabricIndex}:`, error);
1760
+ throw new MatterDeviceError(`Failed to remove fabric: ${errorMessage}`, {
1761
+ originalError: error instanceof Error ? error : undefined,
1762
+ });
1763
+ }
1764
+ }
1765
+ /**
1766
+ * Check if a specific fabric exists
1767
+ */
1768
+ hasFabric(fabricIndex) {
1769
+ const fabrics = this.getFabricInfo();
1770
+ return fabrics.some(f => f.fabricIndex === fabricIndex);
1771
+ }
1772
+ /**
1773
+ * Notify controllers that the parts list has changed
1774
+ * This triggers controllers (like Home app) to re-read the device list
1775
+ * and discover new or removed accessories without needing to re-pair
1776
+ */
1777
+ async notifyPartsListChanged() {
1778
+ if (!this.aggregator || !this.isCommissioned()) {
1779
+ // No controllers connected, skip notification
1780
+ return;
1781
+ }
1782
+ try {
1783
+ // Access the aggregator's descriptor cluster state
1784
+ // The partsList is automatically updated by Matter.js when endpoints are added/removed
1785
+ // We just need to ensure controllers are notified of the change
1786
+ const aggregatorState = this.aggregator;
1787
+ if (aggregatorState.state?.descriptor) {
1788
+ // Get current parts list (endpoint numbers of all children)
1789
+ const partsList = aggregatorState.state.descriptor.partsList || [];
1790
+ if (this.config.debugModeEnabled) {
1791
+ log.debug(`Parts list changed: ${partsList.length} devices (endpoints: ${partsList.join(', ')})`);
1792
+ }
1793
+ // Trigger a state update event to notify subscribed controllers
1794
+ // By setting the partsList to itself, we trigger the change notification
1795
+ await this.aggregator.set({
1796
+ descriptor: {
1797
+ partsList,
1798
+ },
1799
+ });
1800
+ log.info(`Notified controllers of parts list change (${this.accessories.size} devices)`);
1801
+ }
1802
+ }
1803
+ catch (error) {
1804
+ // Non-fatal error - log but don't throw
1805
+ const errorMessage = error instanceof Error ? error.message : String(error);
1806
+ log.warn(`Failed to notify controllers of parts list change: ${errorMessage}`);
1807
+ }
1808
+ }
1809
+ }
1810
+ //# sourceMappingURL=server.js.map