@camstack/core 0.1.15 → 0.1.16

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 (382) hide show
  1. package/dist/addon/addon-api-factory.d.ts +36 -0
  2. package/dist/addon/addon-api-factory.d.ts.map +1 -0
  3. package/dist/addon-routes/addon-route-registry.d.ts +38 -0
  4. package/dist/addon-routes/addon-route-registry.d.ts.map +1 -0
  5. package/dist/auth/api-key-manager.d.ts +27 -0
  6. package/dist/auth/api-key-manager.d.ts.map +1 -0
  7. package/dist/auth/auth-manager.d.ts +47 -0
  8. package/dist/auth/auth-manager.d.ts.map +1 -0
  9. package/dist/auth/parse-record.d.ts +19 -0
  10. package/dist/auth/parse-record.d.ts.map +1 -0
  11. package/dist/auth/scoped-token-manager.d.ts +18 -0
  12. package/dist/auth/scoped-token-manager.d.ts.map +1 -0
  13. package/dist/auth/user-manager.d.ts +34 -0
  14. package/dist/auth/user-manager.d.ts.map +1 -0
  15. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.d.ts +54 -0
  16. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.d.ts.map +1 -0
  17. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +223 -217
  18. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js.map +1 -1
  19. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +216 -7
  20. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs.map +1 -1
  21. package/dist/builtins/addon-pages-aggregator/index.d.ts +2 -0
  22. package/dist/builtins/addon-pages-aggregator/index.d.ts.map +1 -0
  23. package/dist/builtins/addon-pages-aggregator/index.js +6 -221
  24. package/dist/builtins/addon-pages-aggregator/index.mjs +2 -9
  25. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.d.ts +33 -0
  26. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.d.ts.map +1 -0
  27. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +199 -197
  28. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js.map +1 -1
  29. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +192 -7
  30. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs.map +1 -1
  31. package/dist/builtins/addon-widgets-aggregator/index.d.ts +2 -0
  32. package/dist/builtins/addon-widgets-aggregator/index.d.ts.map +1 -0
  33. package/dist/builtins/addon-widgets-aggregator/index.js +6 -201
  34. package/dist/builtins/addon-widgets-aggregator/index.mjs +2 -9
  35. package/dist/builtins/alerts/alerts.addon.d.ts +82 -0
  36. package/dist/builtins/alerts/alerts.addon.d.ts.map +1 -0
  37. package/dist/builtins/alerts/alerts.addon.js +590 -430
  38. package/dist/builtins/alerts/alerts.addon.js.map +1 -1
  39. package/dist/builtins/alerts/alerts.addon.mjs +595 -7
  40. package/dist/builtins/alerts/alerts.addon.mjs.map +1 -1
  41. package/dist/builtins/alerts/index.d.ts +2 -0
  42. package/dist/builtins/alerts/index.d.ts.map +1 -0
  43. package/dist/builtins/alerts/index.js +3 -443
  44. package/dist/builtins/alerts/index.mjs +2 -8
  45. package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.d.ts +8 -0
  46. package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.d.ts.map +1 -0
  47. package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.js +56 -0
  48. package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.js.map +1 -0
  49. package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.mjs +50 -0
  50. package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.mjs.map +1 -0
  51. package/dist/builtins/auth-orchestrator/index.d.ts +2 -0
  52. package/dist/builtins/auth-orchestrator/index.d.ts.map +1 -0
  53. package/dist/builtins/auth-orchestrator/index.js +7 -0
  54. package/dist/builtins/auth-orchestrator/index.mjs +2 -0
  55. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.d.ts +148 -0
  56. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.d.ts.map +1 -0
  57. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +7639 -0
  58. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js.map +1 -0
  59. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +7627 -0
  60. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs.map +1 -0
  61. package/dist/builtins/backup-orchestrator/cron-helpers.d.ts +24 -0
  62. package/dist/builtins/backup-orchestrator/cron-helpers.d.ts.map +1 -0
  63. package/dist/builtins/backup-orchestrator/destination-policy.d.ts +73 -0
  64. package/dist/builtins/backup-orchestrator/destination-policy.d.ts.map +1 -0
  65. package/dist/builtins/backup-orchestrator/download-helpers.d.ts +13 -0
  66. package/dist/builtins/backup-orchestrator/download-helpers.d.ts.map +1 -0
  67. package/dist/builtins/backup-orchestrator/index.d.ts +3 -0
  68. package/dist/builtins/backup-orchestrator/index.d.ts.map +1 -0
  69. package/dist/builtins/backup-orchestrator/index.js +7 -0
  70. package/dist/builtins/backup-orchestrator/index.mjs +2 -0
  71. package/dist/builtins/backup-orchestrator/manifest-store.d.ts +78 -0
  72. package/dist/builtins/backup-orchestrator/manifest-store.d.ts.map +1 -0
  73. package/dist/builtins/console-logging/console-destination.d.ts +14 -0
  74. package/dist/builtins/console-logging/console-destination.d.ts.map +1 -0
  75. package/dist/builtins/console-logging/console-logging.addon.d.ts +26 -0
  76. package/dist/builtins/console-logging/console-logging.addon.d.ts.map +1 -0
  77. package/dist/builtins/console-logging/index.d.ts +4 -0
  78. package/dist/builtins/console-logging/index.d.ts.map +1 -0
  79. package/dist/builtins/console-logging/index.js +99 -235
  80. package/dist/builtins/console-logging/index.js.map +1 -1
  81. package/dist/builtins/console-logging/index.mjs +95 -9
  82. package/dist/builtins/console-logging/index.mjs.map +1 -1
  83. package/dist/builtins/device-manager/device-event-propagator.d.ts +27 -0
  84. package/dist/builtins/device-manager/device-event-propagator.d.ts.map +1 -0
  85. package/dist/builtins/device-manager/device-manager.addon.d.ts +259 -0
  86. package/dist/builtins/device-manager/device-manager.addon.d.ts.map +1 -0
  87. package/dist/builtins/device-manager/device-manager.addon.js +2125 -2127
  88. package/dist/builtins/device-manager/device-manager.addon.js.map +1 -1
  89. package/dist/builtins/device-manager/device-manager.addon.mjs +2145 -7
  90. package/dist/builtins/device-manager/device-manager.addon.mjs.map +1 -1
  91. package/dist/builtins/device-manager/index.d.ts +3 -0
  92. package/dist/builtins/device-manager/index.d.ts.map +1 -0
  93. package/dist/builtins/device-manager/index.js +6 -2156
  94. package/dist/builtins/device-manager/index.mjs +2 -10
  95. package/dist/builtins/hub-forwarder/hub-forwarder-destination.d.ts +45 -0
  96. package/dist/builtins/hub-forwarder/hub-forwarder-destination.d.ts.map +1 -0
  97. package/dist/builtins/hub-forwarder/hub-forwarder.addon.d.ts +16 -0
  98. package/dist/builtins/hub-forwarder/hub-forwarder.addon.d.ts.map +1 -0
  99. package/dist/builtins/hub-forwarder/index.d.ts +4 -0
  100. package/dist/builtins/hub-forwarder/index.d.ts.map +1 -0
  101. package/dist/builtins/hub-forwarder/index.js +150 -291
  102. package/dist/builtins/hub-forwarder/index.js.map +1 -1
  103. package/dist/builtins/hub-forwarder/index.mjs +145 -9
  104. package/dist/builtins/hub-forwarder/index.mjs.map +1 -1
  105. package/dist/builtins/local-auth/auth-schema.d.ts +12 -0
  106. package/dist/builtins/local-auth/auth-schema.d.ts.map +1 -0
  107. package/dist/builtins/local-auth/index.d.ts +2 -0
  108. package/dist/builtins/local-auth/index.d.ts.map +1 -0
  109. package/dist/builtins/local-auth/index.js +3 -623
  110. package/dist/builtins/local-auth/index.mjs +2 -8
  111. package/dist/builtins/local-auth/local-auth.addon.d.ts +17 -0
  112. package/dist/builtins/local-auth/local-auth.addon.d.ts.map +1 -0
  113. package/dist/builtins/local-auth/local-auth.addon.js +6861 -589
  114. package/dist/builtins/local-auth/local-auth.addon.js.map +1 -1
  115. package/dist/builtins/local-auth/local-auth.addon.mjs +6883 -7
  116. package/dist/builtins/local-auth/local-auth.addon.mjs.map +1 -1
  117. package/dist/builtins/local-network/index.d.ts +3 -0
  118. package/dist/builtins/local-network/index.d.ts.map +1 -0
  119. package/dist/builtins/local-network/index.js +9 -0
  120. package/dist/builtins/local-network/index.mjs +2 -0
  121. package/dist/builtins/local-network/local-network.addon.d.ts +102 -0
  122. package/dist/builtins/local-network/local-network.addon.d.ts.map +1 -0
  123. package/dist/builtins/local-network/local-network.addon.js +404 -0
  124. package/dist/builtins/local-network/local-network.addon.js.map +1 -0
  125. package/dist/builtins/local-network/local-network.addon.mjs +392 -0
  126. package/dist/builtins/local-network/local-network.addon.mjs.map +1 -0
  127. package/dist/builtins/mesh-orchestrator/index.d.ts +2 -0
  128. package/dist/builtins/mesh-orchestrator/index.d.ts.map +1 -0
  129. package/dist/builtins/mesh-orchestrator/index.js +7 -0
  130. package/dist/builtins/mesh-orchestrator/index.mjs +2 -0
  131. package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.d.ts +9 -0
  132. package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.d.ts.map +1 -0
  133. package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.js +83 -0
  134. package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.js.map +1 -0
  135. package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.mjs +77 -0
  136. package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.mjs.map +1 -0
  137. package/dist/builtins/native-metrics/index.d.ts +3 -0
  138. package/dist/builtins/native-metrics/index.d.ts.map +1 -0
  139. package/dist/builtins/native-metrics/native-metrics-provider.d.ts +49 -0
  140. package/dist/builtins/native-metrics/native-metrics-provider.d.ts.map +1 -0
  141. package/dist/builtins/native-metrics/native-metrics.addon.d.ts +74 -0
  142. package/dist/builtins/native-metrics/native-metrics.addon.d.ts.map +1 -0
  143. package/dist/builtins/native-metrics/native-metrics.addon.js +887 -861
  144. package/dist/builtins/native-metrics/native-metrics.addon.js.map +1 -1
  145. package/dist/builtins/native-metrics/native-metrics.addon.mjs +914 -5
  146. package/dist/builtins/native-metrics/native-metrics.addon.mjs.map +1 -1
  147. package/dist/builtins/platform-probe/index.d.ts +12 -0
  148. package/dist/builtins/platform-probe/index.d.ts.map +1 -0
  149. package/dist/builtins/platform-probe/index.js +539 -0
  150. package/dist/builtins/platform-probe/index.js.map +1 -0
  151. package/dist/builtins/platform-probe/index.mjs +530 -0
  152. package/dist/builtins/platform-probe/index.mjs.map +1 -0
  153. package/dist/builtins/platform-probe/inference-config-resolver.d.ts +30 -0
  154. package/dist/builtins/platform-probe/inference-config-resolver.d.ts.map +1 -0
  155. package/dist/builtins/platform-probe/platform-scorer.d.ts +22 -0
  156. package/dist/builtins/platform-probe/platform-scorer.d.ts.map +1 -0
  157. package/dist/builtins/remote-access-orchestrator/index.d.ts +2 -0
  158. package/dist/builtins/remote-access-orchestrator/index.d.ts.map +1 -0
  159. package/dist/builtins/remote-access-orchestrator/index.js +7 -0
  160. package/dist/builtins/remote-access-orchestrator/index.mjs +2 -0
  161. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.d.ts +9 -0
  162. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.d.ts.map +1 -0
  163. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +72 -0
  164. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js.map +1 -0
  165. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +66 -0
  166. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs.map +1 -0
  167. package/dist/builtins/snapshot/index.d.ts +3 -0
  168. package/dist/builtins/snapshot/index.d.ts.map +1 -0
  169. package/dist/builtins/snapshot/index.js +481 -491
  170. package/dist/builtins/snapshot/index.js.map +1 -1
  171. package/dist/builtins/snapshot/index.mjs +475 -464
  172. package/dist/builtins/snapshot/index.mjs.map +1 -1
  173. package/dist/builtins/snapshot/snapshot.addon.d.ts +121 -0
  174. package/dist/builtins/snapshot/snapshot.addon.d.ts.map +1 -0
  175. package/dist/builtins/sqlite-storage/config-store.d.ts +9 -0
  176. package/dist/builtins/sqlite-storage/config-store.d.ts.map +1 -0
  177. package/dist/builtins/sqlite-storage/device-store.d.ts +24 -0
  178. package/dist/builtins/sqlite-storage/device-store.d.ts.map +1 -0
  179. package/dist/builtins/sqlite-storage/filesystem-storage-provider.d.ts +87 -0
  180. package/dist/builtins/sqlite-storage/filesystem-storage-provider.d.ts.map +1 -0
  181. package/dist/builtins/sqlite-storage/filesystem-storage.addon.d.ts +32 -0
  182. package/dist/builtins/sqlite-storage/filesystem-storage.addon.d.ts.map +1 -0
  183. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +312 -56
  184. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js.map +1 -1
  185. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +305 -7
  186. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs.map +1 -1
  187. package/dist/builtins/sqlite-storage/index.d.ts +12 -0
  188. package/dist/builtins/sqlite-storage/index.d.ts.map +1 -0
  189. package/dist/builtins/sqlite-storage/index.js +229 -1001
  190. package/dist/builtins/sqlite-storage/index.js.map +1 -1
  191. package/dist/builtins/sqlite-storage/index.mjs +268 -26
  192. package/dist/builtins/sqlite-storage/index.mjs.map +1 -1
  193. package/dist/builtins/sqlite-storage/integration-registry.d.ts +28 -0
  194. package/dist/builtins/sqlite-storage/integration-registry.d.ts.map +1 -0
  195. package/dist/builtins/sqlite-storage/settings-store.d.ts +40 -0
  196. package/dist/builtins/sqlite-storage/settings-store.d.ts.map +1 -0
  197. package/dist/builtins/sqlite-storage/sql-schema.d.ts +33 -0
  198. package/dist/builtins/sqlite-storage/sql-schema.d.ts.map +1 -0
  199. package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +94 -0
  200. package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts.map +1 -0
  201. package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.ts +15 -0
  202. package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.ts.map +1 -0
  203. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +586 -653
  204. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js.map +1 -1
  205. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +582 -7
  206. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs.map +1 -1
  207. package/dist/builtins/storage-orchestrator/index.d.ts +7 -0
  208. package/dist/builtins/storage-orchestrator/index.d.ts.map +1 -0
  209. package/dist/builtins/storage-orchestrator/index.js +9 -0
  210. package/dist/builtins/storage-orchestrator/index.mjs +2 -0
  211. package/dist/builtins/storage-orchestrator/location-store.d.ts +50 -0
  212. package/dist/builtins/storage-orchestrator/location-store.d.ts.map +1 -0
  213. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +60 -0
  214. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts.map +1 -0
  215. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +755 -0
  216. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js.map +1 -0
  217. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +746 -0
  218. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs.map +1 -0
  219. package/dist/builtins/storage-orchestrator/storage-orchestrator.service.d.ts +121 -0
  220. package/dist/builtins/storage-orchestrator/storage-orchestrator.service.d.ts.map +1 -0
  221. package/dist/builtins/system-backup/system-backup.service.d.ts +138 -0
  222. package/dist/builtins/system-backup/system-backup.service.d.ts.map +1 -0
  223. package/dist/builtins/system-config/index.d.ts +2 -0
  224. package/dist/builtins/system-config/index.d.ts.map +1 -0
  225. package/dist/builtins/system-config/index.js +6 -188
  226. package/dist/builtins/system-config/index.mjs +2 -10
  227. package/dist/builtins/system-config/system-config.addon.d.ts +11 -0
  228. package/dist/builtins/system-config/system-config.addon.d.ts.map +1 -0
  229. package/dist/builtins/system-config/system-config.addon.js +227 -180
  230. package/dist/builtins/system-config/system-config.addon.js.map +1 -1
  231. package/dist/builtins/system-config/system-config.addon.mjs +226 -7
  232. package/dist/builtins/system-config/system-config.addon.mjs.map +1 -1
  233. package/dist/builtins/turn-orchestrator/index.d.ts +2 -0
  234. package/dist/builtins/turn-orchestrator/index.d.ts.map +1 -0
  235. package/dist/builtins/turn-orchestrator/index.js +7 -0
  236. package/dist/builtins/turn-orchestrator/index.mjs +2 -0
  237. package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.d.ts +10 -0
  238. package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.d.ts.map +1 -0
  239. package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.js +78 -0
  240. package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.js.map +1 -0
  241. package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.mjs +72 -0
  242. package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.mjs.map +1 -0
  243. package/dist/builtins/winston-logging/index.d.ts +4 -0
  244. package/dist/builtins/winston-logging/index.d.ts.map +1 -0
  245. package/dist/builtins/winston-logging/index.js +153 -300
  246. package/dist/builtins/winston-logging/index.js.map +1 -1
  247. package/dist/builtins/winston-logging/index.mjs +144 -9
  248. package/dist/builtins/winston-logging/index.mjs.map +1 -1
  249. package/dist/builtins/winston-logging/winston-destination.d.ts +22 -0
  250. package/dist/builtins/winston-logging/winston-destination.d.ts.map +1 -0
  251. package/dist/builtins/winston-logging/winston-logging.addon.d.ts +20 -0
  252. package/dist/builtins/winston-logging/winston-logging.addon.d.ts.map +1 -0
  253. package/dist/chunk-C13QxCFV.js +50 -0
  254. package/dist/chunk-hT5z_Zn9.mjs +35 -0
  255. package/dist/download/model-download-service.d.ts +42 -0
  256. package/dist/download/model-download-service.d.ts.map +1 -0
  257. package/dist/download/model-downloader.d.ts +32 -0
  258. package/dist/download/model-downloader.d.ts.map +1 -0
  259. package/dist/events/event-bus.d.ts +11 -0
  260. package/dist/events/event-bus.d.ts.map +1 -0
  261. package/dist/events/system-event-bus.d.ts +15 -0
  262. package/dist/events/system-event-bus.d.ts.map +1 -0
  263. package/dist/feature/feature-manager.d.ts +12 -0
  264. package/dist/feature/feature-manager.d.ts.map +1 -0
  265. package/dist/formatter-C-5An4Bl.mjs +164 -0
  266. package/dist/formatter-C-5An4Bl.mjs.map +1 -0
  267. package/dist/formatter-Dr_6NNZc.js +169 -0
  268. package/dist/formatter-Dr_6NNZc.js.map +1 -0
  269. package/dist/index.d.ts +76 -1696
  270. package/dist/index.d.ts.map +1 -0
  271. package/dist/index.js +7780 -8035
  272. package/dist/index.js.map +1 -1
  273. package/dist/index.mjs +7707 -2148
  274. package/dist/index.mjs.map +1 -1
  275. package/dist/lifecycle/lifecycle-state-machine.d.ts +29 -0
  276. package/dist/lifecycle/lifecycle-state-machine.d.ts.map +1 -0
  277. package/dist/logging/formatter.d.ts +31 -0
  278. package/dist/logging/formatter.d.ts.map +1 -0
  279. package/dist/logging/log-manager.d.ts +52 -0
  280. package/dist/logging/log-manager.d.ts.map +1 -0
  281. package/dist/logging/log-ring-buffer.d.ts +48 -0
  282. package/dist/logging/log-ring-buffer.d.ts.map +1 -0
  283. package/dist/logging/scoped-logger.d.ts +18 -0
  284. package/dist/logging/scoped-logger.d.ts.map +1 -0
  285. package/dist/network/network-quality.d.ts +12 -0
  286. package/dist/network/network-quality.d.ts.map +1 -0
  287. package/dist/notification/notification-service.d.ts +38 -0
  288. package/dist/notification/notification-service.d.ts.map +1 -0
  289. package/dist/notification/toast-service.d.ts +23 -0
  290. package/dist/notification/toast-service.d.ts.map +1 -0
  291. package/dist/pipeline/engine-manager-resolver.d.ts +16 -0
  292. package/dist/pipeline/engine-manager-resolver.d.ts.map +1 -0
  293. package/dist/pipeline/pipeline-runner.d.ts +9 -0
  294. package/dist/pipeline/pipeline-runner.d.ts.map +1 -0
  295. package/dist/pipeline/pipeline-validator.d.ts +14 -0
  296. package/dist/pipeline/pipeline-validator.d.ts.map +1 -0
  297. package/dist/process/resource-monitor.d.ts +12 -0
  298. package/dist/process/resource-monitor.d.ts.map +1 -0
  299. package/dist/python/python-env-manager.d.ts +13 -0
  300. package/dist/python/python-env-manager.d.ts.map +1 -0
  301. package/dist/repl/interfaces.d.ts +32 -0
  302. package/dist/repl/interfaces.d.ts.map +1 -0
  303. package/dist/repl/repl-engine.d.ts +9 -0
  304. package/dist/repl/repl-engine.d.ts.map +1 -0
  305. package/dist/resource-monitor-CmuWlmap.js +76 -0
  306. package/dist/resource-monitor-CmuWlmap.js.map +1 -0
  307. package/dist/resource-monitor-DcQdKGYU.mjs +59 -0
  308. package/dist/resource-monitor-DcQdKGYU.mjs.map +1 -0
  309. package/dist/storage/fs-storage-backend.d.ts +41 -0
  310. package/dist/storage/fs-storage-backend.d.ts.map +1 -0
  311. package/dist/storage/storage-location-manager.d.ts +24 -0
  312. package/dist/storage/storage-location-manager.d.ts.map +1 -0
  313. package/dist/storage/storage-manager.d.ts +77 -0
  314. package/dist/storage/storage-manager.d.ts.map +1 -0
  315. package/dist/tls/cert-manager.d.ts +27 -0
  316. package/dist/tls/cert-manager.d.ts.map +1 -0
  317. package/dist/tls/index.d.ts +2 -0
  318. package/dist/tls/index.d.ts.map +1 -0
  319. package/package.json +119 -23
  320. package/dist/builtins/addon-pages-aggregator/index.js.map +0 -1
  321. package/dist/builtins/addon-pages-aggregator/index.mjs.map +0 -1
  322. package/dist/builtins/addon-widgets-aggregator/index.js.map +0 -1
  323. package/dist/builtins/addon-widgets-aggregator/index.mjs.map +0 -1
  324. package/dist/builtins/alerts/index.js.map +0 -1
  325. package/dist/builtins/alerts/index.mjs.map +0 -1
  326. package/dist/builtins/device-manager/index.js.map +0 -1
  327. package/dist/builtins/device-manager/index.mjs.map +0 -1
  328. package/dist/builtins/local-auth/index.js.map +0 -1
  329. package/dist/builtins/local-auth/index.mjs.map +0 -1
  330. package/dist/builtins/local-backup/index.js +0 -173
  331. package/dist/builtins/local-backup/index.js.map +0 -1
  332. package/dist/builtins/local-backup/index.mjs +0 -10
  333. package/dist/builtins/local-backup/index.mjs.map +0 -1
  334. package/dist/builtins/system-config/index.js.map +0 -1
  335. package/dist/builtins/system-config/index.mjs.map +0 -1
  336. package/dist/chunk-2CIYKDRN.mjs +0 -1
  337. package/dist/chunk-2CIYKDRN.mjs.map +0 -1
  338. package/dist/chunk-2F76X6NL.mjs +0 -136
  339. package/dist/chunk-2F76X6NL.mjs.map +0 -1
  340. package/dist/chunk-2QUFBZ7M.mjs +0 -1
  341. package/dist/chunk-2QUFBZ7M.mjs.map +0 -1
  342. package/dist/chunk-3BK2Y7GY.mjs +0 -593
  343. package/dist/chunk-3BK2Y7GY.mjs.map +0 -1
  344. package/dist/chunk-4OOHFJHT.mjs +0 -421
  345. package/dist/chunk-4OOHFJHT.mjs.map +0 -1
  346. package/dist/chunk-4XHB7IHT.mjs +0 -809
  347. package/dist/chunk-4XHB7IHT.mjs.map +0 -1
  348. package/dist/chunk-6M2HSSTQ.mjs +0 -98
  349. package/dist/chunk-6M2HSSTQ.mjs.map +0 -1
  350. package/dist/chunk-7FI7SQS7.mjs +0 -135
  351. package/dist/chunk-7FI7SQS7.mjs.map +0 -1
  352. package/dist/chunk-ED57RCQE.mjs +0 -171
  353. package/dist/chunk-ED57RCQE.mjs.map +0 -1
  354. package/dist/chunk-FZN56HGQ.mjs +0 -626
  355. package/dist/chunk-FZN56HGQ.mjs.map +0 -1
  356. package/dist/chunk-GL4OOB25.mjs +0 -51
  357. package/dist/chunk-GL4OOB25.mjs.map +0 -1
  358. package/dist/chunk-KDG2NTDB.mjs +0 -137
  359. package/dist/chunk-KDG2NTDB.mjs.map +0 -1
  360. package/dist/chunk-NRBQWBDM.mjs +0 -191
  361. package/dist/chunk-NRBQWBDM.mjs.map +0 -1
  362. package/dist/chunk-O4V246GG.mjs +0 -2137
  363. package/dist/chunk-O4V246GG.mjs.map +0 -1
  364. package/dist/chunk-QT57H266.mjs +0 -163
  365. package/dist/chunk-QT57H266.mjs.map +0 -1
  366. package/dist/chunk-QX4RH25I.mjs +0 -141
  367. package/dist/chunk-QX4RH25I.mjs.map +0 -1
  368. package/dist/chunk-TB562PZX.mjs +0 -86
  369. package/dist/chunk-TB562PZX.mjs.map +0 -1
  370. package/dist/chunk-TDYPZXK5.mjs +0 -1
  371. package/dist/chunk-TDYPZXK5.mjs.map +0 -1
  372. package/dist/chunk-UJI4LN5P.mjs +0 -36
  373. package/dist/chunk-UJI4LN5P.mjs.map +0 -1
  374. package/dist/chunk-W6RTHQGP.mjs +0 -1
  375. package/dist/chunk-W6RTHQGP.mjs.map +0 -1
  376. package/dist/chunk-ZELBCPDC.mjs +0 -369
  377. package/dist/chunk-ZELBCPDC.mjs.map +0 -1
  378. package/dist/index.d.mts +0 -1696
  379. package/dist/resource-monitor-UZUGPIAU.mjs +0 -9
  380. package/dist/resource-monitor-UZUGPIAU.mjs.map +0 -1
  381. package/dist/storage-location-manager-HFNB3PCS.mjs +0 -7
  382. package/dist/storage-location-manager-HFNB3PCS.mjs.map +0 -1
@@ -1,2155 +1,2153 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
-
20
- // src/builtins/device-manager/device-manager.addon.ts
21
- var device_manager_addon_exports = {};
22
- __export(device_manager_addon_exports, {
23
- DeviceManagerAddon: () => DeviceManagerAddon,
24
- default: () => device_manager_addon_default
1
+ Object.defineProperties(exports, {
2
+ __esModule: { value: true },
3
+ [Symbol.toStringTag]: { value: "Module" }
25
4
  });
26
- module.exports = __toCommonJS(device_manager_addon_exports);
27
- var import_node_crypto = require("crypto");
28
- var import_types = require("@camstack/types");
29
-
30
- // src/builtins/device-manager/device-event-propagator.ts
5
+ require("../../chunk-C13QxCFV.js");
6
+ let node_crypto = require("node:crypto");
7
+ let _camstack_types = require("@camstack/types");
8
+ //#region src/builtins/device-manager/device-event-propagator.ts
9
+ /**
10
+ * Walks the parent chain for every device-sourced event and re-emits a
11
+ * copy on each ancestor scope with `via[]` populated.
12
+ *
13
+ * Design goals:
14
+ * - Transparent: drivers emit once on their own device scope; the
15
+ * framework handles fan-out. Zero provider boilerplate.
16
+ * - Anti-loop: events that already carry `via[]` are skipped (we only
17
+ * propagate ORIGINAL emissions).
18
+ * - Anti-cycle: the parent chain is bounded — if the device registry
19
+ * is corrupt and has a cycle, the walker caps at `MAX_CHAIN_DEPTH`
20
+ * and logs a warning.
21
+ * - Lazy: parent chain is resolved on-demand per event (no cached
22
+ * topology). The lookup is O(depth) which is ≤2 in practice.
23
+ *
24
+ * `via` contract (from SystemEvent.via JSDoc):
25
+ * - `via[0]` is the originating source (the device that produced the
26
+ * event). Subsequent entries walk up the parent chain.
27
+ * - On the re-emission, `source` is the ancestor at that level and
28
+ * `via[0..i]` is the prefix of the chain up to and including the
29
+ * first N ancestors below the current one.
30
+ *
31
+ * Example (grandchild → parent → grandparent):
32
+ * Original: { source: {id: 7}, data: {...}, via: undefined }
33
+ * Re-emit 1: { source: {id: 4}, data: {...}, via: [{id: 7}] }
34
+ * Re-emit 2: { source: {id: 1}, data: {...}, via: [{id: 7}, {id: 4}] }
35
+ *
36
+ * A consumer listening at `source.id === 1` receives re-emit 2 (with
37
+ * `via` showing the chain). A consumer listening at `source.id === 7`
38
+ * with `via === undefined` receives the original only.
39
+ */
40
+ /** Bounded walk — paranoia against corrupt device registries with cycles. */
31
41
  var MAX_CHAIN_DEPTH = 16;
32
42
  var DeviceEventPropagator = class {
33
- constructor(opts) {
34
- this.opts = opts;
35
- }
36
- opts;
37
- unsubscribe = null;
38
- start() {
39
- if (this.unsubscribe) return;
40
- const unsub = this.opts.eventBus.subscribe({}, (ev) => this.handle(ev));
41
- this.unsubscribe = unsub;
42
- }
43
- stop() {
44
- if (!this.unsubscribe) return;
45
- this.unsubscribe();
46
- this.unsubscribe = null;
47
- }
48
- /** Exposed for tests — lets them inject events without the full bus. */
49
- handle(ev) {
50
- if (ev.via !== void 0) return;
51
- if (ev.source.type !== "device") return;
52
- const rawId = ev.source.id;
53
- const deviceId = typeof rawId === "number" ? rawId : Number(rawId);
54
- if (!Number.isFinite(deviceId)) return;
55
- const chain = this.resolveParentChain(deviceId);
56
- if (chain.length === 0) return;
57
- const origin = ev.source;
58
- const via = [origin];
59
- for (const ancestorId of chain) {
60
- const reEmission = {
61
- ...ev,
62
- source: { type: "device", id: ancestorId },
63
- via: [...via]
64
- };
65
- this.opts.eventBus.emit(reEmission);
66
- via.push({ type: "device", id: ancestorId });
67
- }
68
- }
69
- resolveParentChain(deviceId) {
70
- const chain = [];
71
- const seen = /* @__PURE__ */ new Set([deviceId]);
72
- let current = this.opts.getParentOf(deviceId);
73
- while (current != null) {
74
- if (seen.has(current)) {
75
- this.opts.logger.warn(
76
- "device-event-propagator: cycle detected in parent chain \u2014 aborting propagation",
77
- { tags: { deviceId }, meta: { cycleAt: current, chainSoFar: [...chain] } }
78
- );
79
- return chain;
80
- }
81
- seen.add(current);
82
- chain.push(current);
83
- if (chain.length >= MAX_CHAIN_DEPTH) {
84
- this.opts.logger.warn(
85
- "device-event-propagator: chain depth limit hit \u2014 truncating",
86
- { tags: { deviceId }, meta: { depth: chain.length, max: MAX_CHAIN_DEPTH } }
87
- );
88
- break;
89
- }
90
- current = this.opts.getParentOf(current);
91
- }
92
- return chain;
93
- }
43
+ unsubscribe = null;
44
+ constructor(opts) {
45
+ this.opts = opts;
46
+ }
47
+ start() {
48
+ if (this.unsubscribe) return;
49
+ const unsub = this.opts.eventBus.subscribe({}, (ev) => this.handle(ev));
50
+ this.unsubscribe = unsub;
51
+ }
52
+ stop() {
53
+ if (!this.unsubscribe) return;
54
+ this.unsubscribe();
55
+ this.unsubscribe = null;
56
+ }
57
+ /** Exposed for tests — lets them inject events without the full bus. */
58
+ handle(ev) {
59
+ if (ev.via !== void 0) return;
60
+ if (ev.source.type !== "device") return;
61
+ const rawId = ev.source.id;
62
+ const deviceId = typeof rawId === "number" ? rawId : Number(rawId);
63
+ if (!Number.isFinite(deviceId)) return;
64
+ const chain = this.resolveParentChain(deviceId);
65
+ if (chain.length === 0) return;
66
+ const via = [ev.source];
67
+ for (const ancestorId of chain) {
68
+ const reEmission = {
69
+ ...ev,
70
+ source: {
71
+ type: "device",
72
+ id: ancestorId
73
+ },
74
+ via: [...via]
75
+ };
76
+ this.opts.eventBus.emit(reEmission);
77
+ via.push({
78
+ type: "device",
79
+ id: ancestorId
80
+ });
81
+ }
82
+ }
83
+ resolveParentChain(deviceId) {
84
+ const chain = [];
85
+ const seen = new Set([deviceId]);
86
+ let current = this.opts.getParentOf(deviceId);
87
+ while (current != null) {
88
+ if (seen.has(current)) {
89
+ this.opts.logger.warn("device-event-propagator: cycle detected in parent chain — aborting propagation", {
90
+ tags: { deviceId },
91
+ meta: {
92
+ cycleAt: current,
93
+ chainSoFar: [...chain]
94
+ }
95
+ });
96
+ return chain;
97
+ }
98
+ seen.add(current);
99
+ chain.push(current);
100
+ if (chain.length >= MAX_CHAIN_DEPTH) {
101
+ this.opts.logger.warn("device-event-propagator: chain depth limit hit — truncating", {
102
+ tags: { deviceId },
103
+ meta: {
104
+ depth: chain.length,
105
+ max: MAX_CHAIN_DEPTH
106
+ }
107
+ });
108
+ break;
109
+ }
110
+ current = this.opts.getParentOf(current);
111
+ }
112
+ return chain;
113
+ }
94
114
  };
95
-
96
- // src/builtins/device-manager/device-manager.addon.ts
97
- var import_types2 = require("@camstack/types");
98
- var import_types3 = require("@camstack/types");
99
- var import_types4 = require("@camstack/types");
115
+ //#endregion
116
+ //#region src/builtins/device-manager/device-manager.addon.ts
117
+ /**
118
+ * Device Manager addon — hub-side singleton that unifies device persistence,
119
+ * live registry queries, and all management operations into a single
120
+ * tRPC-routable capability.
121
+ *
122
+ * Persistence strategy: all device data is stored via `ctx.settings`, the same
123
+ * settings API every other addon uses. No raw SQLite access.
124
+ *
125
+ * Addon store layout:
126
+ * deviceIndex → Record<addonId, stableId[]> (which devices exist per addon)
127
+ * deviceMeta → Record<"addonId:stableId", DeviceMeta> (type, name, parentDeviceId, id)
128
+ *
129
+ * Device store (per-device config):
130
+ * readDeviceStore(numericDeviceId) → config blob
131
+ * writeDeviceStore(numericDeviceId, patch)
132
+ *
133
+ * Live registry: resolved from the kernel capability registry after Phase 2.
134
+ * This gives direct access to in-memory IDevice instances registered by provider addons.
135
+ * The DeviceManagerAddon is the single owner of the live device operations API.
136
+ *
137
+ * Replaces:
138
+ * - `device-persistence` capability (absorbed here)
139
+ * - live operations previously served by `device-management.router.ts`
140
+ */
100
141
  function shallowEqual(a, b) {
101
- const ak = Object.keys(a);
102
- const bk = Object.keys(b);
103
- if (ak.length !== bk.length) return false;
104
- for (const k of ak) {
105
- if (a[k] !== b[k]) return false;
106
- }
107
- return true;
142
+ const ak = Object.keys(a);
143
+ const bk = Object.keys(b);
144
+ if (ak.length !== bk.length) return false;
145
+ for (const k of ak) if (a[k] !== b[k]) return false;
146
+ return true;
108
147
  }
109
148
  function deviceKey(addonId, stableId) {
110
- return `${addonId}:${stableId}`;
149
+ return `${addonId}:${stableId}`;
111
150
  }
112
151
  function isCameraDevice(device) {
113
- return "getStreamSources" in device && typeof device.getStreamSources === "function";
152
+ return "getStreamSources" in device && typeof device.getStreamSources === "function";
114
153
  }
115
- var DEVICE_FEATURE_VALUES = new Set(Object.values(import_types.DeviceFeature));
154
+ var DEVICE_FEATURE_VALUES = new Set(Object.values(_camstack_types.DeviceFeature));
155
+ /**
156
+ * Validate persisted feature strings against the `DeviceFeature` enum
157
+ * — workers serialise the live `device.features` array (so every entry
158
+ * is a valid enum value at write time) but the persisted blob is loose
159
+ * `string[]` on the wire. The narrow keeps unknown values out of the
160
+ * `getDevice` response without losing the enum-typed contract.
161
+ */
116
162
  function persistedFeatures(features) {
117
- if (!features) return [];
118
- const out = [];
119
- for (const f of features) {
120
- if (DEVICE_FEATURE_VALUES.has(f)) out.push(f);
121
- }
122
- return out;
163
+ if (!features) return [];
164
+ const out = [];
165
+ for (const f of features) if (DEVICE_FEATURE_VALUES.has(f)) out.push(f);
166
+ return out;
123
167
  }
124
168
  function toDeviceInfo(addonId, device, metadata = null, metaRow = null) {
125
- const configValues = {};
126
- for (const entry of device.config.entries()) {
127
- configValues[entry.key] = entry.value;
128
- }
129
- const name = metaRow?.name ?? device.name;
130
- const location = metaRow?.location !== void 0 ? metaRow.location : device.location;
131
- const disabled = metaRow?.disabled ?? device.disabled;
132
- return {
133
- // Progressive system-wide id — bound on the live `IDevice` at
134
- // construction time (see `DeviceContext.id` / `BaseDevice.id`). The
135
- // hub allocates via `allocateDeviceId` BEFORE the device is
136
- // instantiated, so every live device exposes one.
137
- id: device.id,
138
- stableId: device.stableId,
139
- addonId,
140
- type: device.type,
141
- name,
142
- location,
143
- disabled,
144
- parentDeviceId: device.parentDeviceId,
145
- // `role` is set by the provider on accessory children (synthesized
146
- // via `getAccessoryChildren()` — Reolink siren/floodlight/PIR,
147
- // Hikvision supplemental light/siren). Top-level devices and
148
- // first-class hub-adopted children leave it `undefined`. Surfacing
149
- // it on `DeviceInfo` lets the UI distinguish accessory rows from
150
- // adopted-camera rows without re-deriving the heuristic from the
151
- // provider's `deviceClasses` map.
152
- role: device.role ?? null,
153
- online: device.online,
154
- features: [...device.features],
155
- isCamera: isCameraDevice(device),
156
- config: configValues,
157
- metadata
158
- };
169
+ const configValues = {};
170
+ for (const entry of device.config.entries()) configValues[entry.key] = entry.value;
171
+ const name = metaRow?.name ?? device.name;
172
+ const location = metaRow?.location !== void 0 ? metaRow.location : device.location;
173
+ const disabled = metaRow?.disabled ?? device.disabled;
174
+ return {
175
+ id: device.id,
176
+ stableId: device.stableId,
177
+ addonId,
178
+ type: device.type,
179
+ name,
180
+ location,
181
+ disabled,
182
+ parentDeviceId: device.parentDeviceId,
183
+ role: device.role ?? null,
184
+ online: device.online,
185
+ features: [...device.features],
186
+ isCamera: isCameraDevice(device),
187
+ config: configValues,
188
+ metadata
189
+ };
159
190
  }
160
191
  function resolveDeviceById(registry, deviceId) {
161
- const device = registry.getById(deviceId);
162
- if (!device) return null;
163
- const addonId = registry.getAddonId(deviceId);
164
- if (!addonId) return null;
165
- return { addonId, device };
192
+ const device = registry.getById(deviceId);
193
+ if (!device) return null;
194
+ const addonId = registry.getAddonId(deviceId);
195
+ if (!addonId) return null;
196
+ return {
197
+ addonId,
198
+ device
199
+ };
166
200
  }
201
+ /**
202
+ * Walk the sections/fields of a contribution and inject `writerCapName` +
203
+ * `writerAddonId` + `source` on each editable field. Readonly fields and
204
+ * structural fields (separator/info/button) pass through untouched. The
205
+ * aggregator is the single place that knows provenance — provider schemas
206
+ * stay clean, UI-bound metadata is attached once at the boundary.
207
+ */
167
208
  function tagContribution(contribution, capName, addonId, kind) {
168
- const source = kind === "settings" ? "settings" : "live";
169
- return {
170
- ...contribution.tabs ? { tabs: [...contribution.tabs] } : {},
171
- sections: contribution.sections.map((section) => ({
172
- ...section,
173
- fields: section.fields.map((field) => tagField(field, capName, addonId, source, kind))
174
- }))
175
- };
209
+ const source = kind === "settings" ? "settings" : "live";
210
+ return {
211
+ ...contribution.tabs ? { tabs: [...contribution.tabs] } : {},
212
+ sections: contribution.sections.map((section) => ({
213
+ ...section,
214
+ fields: section.fields.map((field) => tagField(field, capName, addonId, source, kind))
215
+ }))
216
+ };
176
217
  }
177
218
  function isFieldRecord(value) {
178
- return value !== null && typeof value === "object" && !Array.isArray(value);
219
+ return value !== null && typeof value === "object" && !Array.isArray(value);
179
220
  }
221
+ /**
222
+ * Convert a strict `ConfigUISchemaWithValues` (readonly arrays, typed
223
+ * field union) into the cap wire shape `ContributionShape` (mutable
224
+ * arrays, opaque field records). Required because the cap method z.infer
225
+ * uses mutable arrays — readonly arrays are not assignable to mutable
226
+ * even when structurally identical, so a structural copy bridges the gap
227
+ * without disabling the type checker.
228
+ */
180
229
  function toWireShape(input) {
181
- const out = {
182
- sections: input.sections.map((s) => ({
183
- id: s.id,
184
- title: s.title,
185
- ...s.description !== void 0 ? { description: s.description } : {},
186
- ...s.style !== void 0 ? { style: s.style } : {},
187
- ...s.defaultCollapsed !== void 0 ? { defaultCollapsed: s.defaultCollapsed } : {},
188
- ...s.columns !== void 0 ? { columns: s.columns } : {},
189
- ...s.tab !== void 0 ? { tab: s.tab } : {},
190
- // `location: 'top-tab'` hoists the section into a discovered top-
191
- // level device tab in DeviceDetail. Forgetting to forward it here
192
- // strips the marker before the client sees it, so the tab never
193
- // shows up — see `DeviceDetail.tsx` aggregate-driven tab discovery.
194
- ...s.location !== void 0 ? { location: s.location } : {},
195
- ...s.order !== void 0 ? { order: s.order } : {},
196
- fields: [...s.fields]
197
- }))
198
- };
199
- if (input.tabs) out.tabs = [...input.tabs];
200
- return out;
230
+ const out = { sections: input.sections.map((s) => ({
231
+ id: s.id,
232
+ title: s.title,
233
+ ...s.description !== void 0 ? { description: s.description } : {},
234
+ ...s.style !== void 0 ? { style: s.style } : {},
235
+ ...s.defaultCollapsed !== void 0 ? { defaultCollapsed: s.defaultCollapsed } : {},
236
+ ...s.columns !== void 0 ? { columns: s.columns } : {},
237
+ ...s.tab !== void 0 ? { tab: s.tab } : {},
238
+ ...s.location !== void 0 ? { location: s.location } : {},
239
+ ...s.order !== void 0 ? { order: s.order } : {},
240
+ fields: [...s.fields]
241
+ })) };
242
+ if (input.tabs) out.tabs = [...input.tabs];
243
+ return out;
201
244
  }
202
245
  function tagField(field, capName, addonId, source, kind) {
203
- if (!isFieldRecord(field)) return field;
204
- const f = field;
205
- const structuralTypes = /* @__PURE__ */ new Set(["separator", "info", "button"]);
206
- if (typeof f.type === "string" && structuralTypes.has(f.type)) return field;
207
- const tagged = { ...f, source };
208
- if (kind === "live" || f.readonlyField === true) {
209
- tagged.readonlyField = true;
210
- } else {
211
- tagged.writerCapName = capName;
212
- tagged.writerAddonId = addonId;
213
- }
214
- if (f.type === "group") {
215
- const children = Array.isArray(f.fields) ? f.fields : [];
216
- if (children.length > 0) {
217
- tagged.fields = children.map((child) => tagField(child, capName, addonId, source, kind));
218
- }
219
- } else if (f.type === "sub-tabs") {
220
- const rawTabs = Array.isArray(f.tabs) ? f.tabs : [];
221
- if (rawTabs.length > 0) {
222
- tagged.tabs = rawTabs.map((tab) => {
223
- if (!isFieldRecord(tab)) return tab;
224
- const tabChildren = Array.isArray(tab.fields) ? tab.fields : [];
225
- return {
226
- ...tab,
227
- fields: tabChildren.map((child) => tagField(child, capName, addonId, source, kind))
228
- };
229
- });
230
- }
231
- }
232
- return tagged;
246
+ if (!isFieldRecord(field)) return field;
247
+ const f = field;
248
+ const structuralTypes = new Set([
249
+ "separator",
250
+ "info",
251
+ "button"
252
+ ]);
253
+ if (typeof f.type === "string" && structuralTypes.has(f.type)) return field;
254
+ const tagged = {
255
+ ...f,
256
+ source
257
+ };
258
+ if (kind === "live" || f.readonlyField === true) tagged.readonlyField = true;
259
+ else {
260
+ tagged.writerCapName = capName;
261
+ tagged.writerAddonId = addonId;
262
+ }
263
+ if (f.type === "group") {
264
+ const children = Array.isArray(f.fields) ? f.fields : [];
265
+ if (children.length > 0) tagged.fields = children.map((child) => tagField(child, capName, addonId, source, kind));
266
+ } else if (f.type === "sub-tabs") {
267
+ const rawTabs = Array.isArray(f.tabs) ? f.tabs : [];
268
+ if (rawTabs.length > 0) tagged.tabs = rawTabs.map((tab) => {
269
+ if (!isFieldRecord(tab)) return tab;
270
+ const tabChildren = Array.isArray(tab.fields) ? tab.fields : [];
271
+ return {
272
+ ...tab,
273
+ fields: tabChildren.map((child) => tagField(child, capName, addonId, source, kind))
274
+ };
275
+ });
276
+ }
277
+ return tagged;
233
278
  }
234
279
  function mergeAggregates(parts) {
235
- const tabDecls = /* @__PURE__ */ new Map();
236
- const sections = [];
237
- for (const part of parts) {
238
- if (part.tabs) {
239
- for (const t of part.tabs) {
240
- if (!tabDecls.has(t.id)) tabDecls.set(t.id, t);
241
- }
242
- }
243
- for (const s of part.sections) {
244
- sections.push(s);
245
- }
246
- }
247
- for (const s of sections) {
248
- const tabId = s.tab ?? "general";
249
- if (tabDecls.has(tabId)) continue;
250
- const known = import_types3.WELL_KNOWN_TAB_MAP[tabId];
251
- if (known) {
252
- tabDecls.set(tabId, { id: known.id, label: known.label, icon: known.icon, order: known.order });
253
- } else {
254
- tabDecls.set(tabId, { id: tabId, label: tabId, icon: "wrench", order: 100 });
255
- }
256
- }
257
- sections.sort((a, b) => {
258
- const tabA = a.tab ?? "general";
259
- const tabB = b.tab ?? "general";
260
- if (tabA !== tabB) {
261
- const orderA = tabDecls.get(tabA)?.order ?? 100;
262
- const orderB = tabDecls.get(tabB)?.order ?? 100;
263
- if (orderA !== orderB) return orderA - orderB;
264
- return tabA.localeCompare(tabB);
265
- }
266
- return (a.order ?? 0) - (b.order ?? 0);
267
- });
268
- const sortedTabs = [...tabDecls.values()].sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
269
- const out = { sections };
270
- if (sortedTabs.length > 0) out.tabs = sortedTabs;
271
- return out;
280
+ const tabDecls = /* @__PURE__ */ new Map();
281
+ const sections = [];
282
+ for (const part of parts) {
283
+ if (part.tabs) {
284
+ for (const t of part.tabs) if (!tabDecls.has(t.id)) tabDecls.set(t.id, t);
285
+ }
286
+ for (const s of part.sections) sections.push(s);
287
+ }
288
+ for (const s of sections) {
289
+ const tabId = s.tab ?? "general";
290
+ if (tabDecls.has(tabId)) continue;
291
+ const known = _camstack_types.WELL_KNOWN_TAB_MAP[tabId];
292
+ if (known) tabDecls.set(tabId, {
293
+ id: known.id,
294
+ label: known.label,
295
+ icon: known.icon,
296
+ order: known.order
297
+ });
298
+ else tabDecls.set(tabId, {
299
+ id: tabId,
300
+ label: tabId,
301
+ icon: "wrench",
302
+ order: 100
303
+ });
304
+ }
305
+ sections.sort((a, b) => {
306
+ const tabA = a.tab ?? "general";
307
+ const tabB = b.tab ?? "general";
308
+ if (tabA !== tabB) {
309
+ const orderA = tabDecls.get(tabA)?.order ?? 100;
310
+ const orderB = tabDecls.get(tabB)?.order ?? 100;
311
+ if (orderA !== orderB) return orderA - orderB;
312
+ return tabA.localeCompare(tabB);
313
+ }
314
+ return (a.order ?? 0) - (b.order ?? 0);
315
+ });
316
+ const sortedTabs = [...tabDecls.values()].sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
317
+ const out = { sections };
318
+ if (sortedTabs.length > 0) out.tabs = sortedTabs;
319
+ return out;
272
320
  }
273
- var DeviceManagerAddon = class _DeviceManagerAddon extends import_types.BaseAddon {
274
- constructor() {
275
- super({});
276
- }
277
- /** Shorthand for the kernel-injected capability registry. */
278
- get capabilityRegistry() {
279
- return this.ctx.kernel.capabilityRegistry;
280
- }
281
- /**
282
- * Parent-chain event propagator. Started in `onInitialize` once the
283
- * hub's `deviceRegistry` is available; listens to every device-sourced
284
- * event and re-emits a copy on each ancestor scope with `via[]`
285
- * populated. Stopped in `onShutdown`.
286
- */
287
- propagator = null;
288
- /**
289
- * Hub-side mirror of every device's cap-keyed runtime state.
290
- * Populated whenever any caller writes via `deviceState.setCapSlice`
291
- * (the canonical cross-layer write entrypoint) and on first load
292
- * via `loadRuntimeState`. Cross-process consumers reach the mirror
293
- * through the `deviceState` cap router; per-cap event subscribers
294
- * (e.g. `battery.onStatusChanged`) get the same data via
295
- * cap-specific events still emitted by the owning device.
296
- *
297
- * Key: deviceId. Value: per-cap slice map. Empty by default —
298
- * slices show up as `setCapSlice` calls trickle in.
299
- */
300
- stateMirror = /* @__PURE__ */ new Map();
301
- /**
302
- * Per-device disk-write debouncer for runtime-state. `setCapSlice`
303
- * updates the in-memory mirror synchronously and emits the change
304
- * event immediately, but the disk write is coalesced — frequent
305
- * back-to-back writes (motion phase transitions, battery pushes,
306
- * etc.) collapse to one `writeDeviceRuntimeState` per
307
- * `RUNTIME_STATE_DEBOUNCE_MS` window. `flushRuntimeStateWrites`
308
- * awaits any in-flight write + scheduled flush so shutdown is
309
- * lossless.
310
- */
311
- runtimeStateDebounce = /* @__PURE__ */ new Map();
312
- static RUNTIME_STATE_DEBOUNCE_MS = 1e3;
313
- /**
314
- * Cross-process native-provider cache: deviceId (numeric) → capName → { addonId, nodeId }.
315
- * Kept in sync with `device.bindings-changed` events emitted by forked
316
- * workers. Union'd into `getBindings` so hub-side consumers see every
317
- * native cap regardless of which process owns the IDevice. No persistence
318
- * — entries re-register when the worker restarts. Purged on
319
- * `$node.disconnected` to avoid stale routing after a worker crash.
320
- */
321
- remoteNativeCaps = /* @__PURE__ */ new Map();
322
- /** Wait for a device-provider by addonId, returning null on timeout. */
323
- async waitDeviceProvider(addonId, timeoutMs = 5e3) {
324
- const provider = await this.capabilityRegistry?.waitForProvider("device-provider", addonId, timeoutMs);
325
- return provider ? provider : null;
326
- }
327
- /** Require a device-provider by addonId — throws if not found. */
328
- async requireDeviceProvider(addonId) {
329
- const dp = await this.waitDeviceProvider(addonId);
330
- if (!dp) {
331
- throw new Error(`Device provider "${addonId}" not found or not registered`);
332
- }
333
- return dp;
334
- }
335
- // ── Binding store (persisted wrapper activation) ───────────────────────────
336
- //
337
- // These methods are public so the device-manager capability router can
338
- // expose them as cap methods. They read/write through
339
- // ctx.settings.readAddonStore/writeAddonStore — the same persistence channel
340
- // the rest of the addon uses.
341
- async readBindingsStore() {
342
- const raw = await this.ctx.settings.readAddonStore();
343
- return { deviceBindings: raw.deviceBindings ?? {} };
344
- }
345
- async writeBindingsStore(next) {
346
- await this.ctx.settings.writeAddonStore({ deviceBindings: next.deviceBindings });
347
- }
348
- resolveWrapperNodeId(_wrapperAddonId) {
349
- return "hub";
350
- }
351
- /**
352
- * Active discovery of native caps registered on a worker node.
353
- * Called from the `$node.connected` handler to recover from lost
354
- * `DeviceBindingsChanged` broadcasts after worker restart.
355
- *
356
- * Iterates the broker's service registry, picks
357
- * `<addonId>.native-provider.<capName>` services owned by the
358
- * connected node, calls each service's `$listDeviceIds` action to
359
- * fetch the deviceIds the worker has registered that cap on, then
360
- * writes entries into `remoteNativeCaps`.
361
- *
362
- * Best-effort: per-service failures are logged and swallowed so a
363
- * single bad service doesn't abort the whole rebuild.
364
- */
365
- async discoverWorkerNativeCaps(connectedNodeId, _connectedAddonId) {
366
- const cluster = this.ctx.kernel.cluster;
367
- if (!cluster) return;
368
- const broker = cluster.broker;
369
- const services = broker.registry?.getServiceList?.({ onlyAvailable: true, withActions: false }) ?? [];
370
- const NATIVE_INFIX = ".native-provider.";
371
- const matched = services.filter(
372
- (s) => s.nodeID === connectedNodeId && s.name.includes(NATIVE_INFIX)
373
- );
374
- if (matched.length === 0) return;
375
- for (const svc of matched) {
376
- const idx = svc.name.indexOf(NATIVE_INFIX);
377
- if (idx <= 0) continue;
378
- const addonId = svc.name.slice(0, idx);
379
- const capName = svc.name.slice(idx + NATIVE_INFIX.length);
380
- if (!addonId || !capName) continue;
381
- try {
382
- const action = `${svc.name}.$listDeviceIds`;
383
- const deviceIds = await broker.call?.(action, {}, { nodeID: connectedNodeId });
384
- if (!deviceIds || deviceIds.length === 0) continue;
385
- for (const deviceId of deviceIds) {
386
- if (this.capabilityRegistry?.getNativeAddonId(capName, deviceId)) continue;
387
- let perDevice = this.remoteNativeCaps.get(deviceId);
388
- if (!perDevice) {
389
- perDevice = /* @__PURE__ */ new Map();
390
- this.remoteNativeCaps.set(deviceId, perDevice);
391
- }
392
- perDevice.set(capName, { addonId, nodeId: connectedNodeId });
393
- }
394
- this.ctx.logger.debug("worker native-cap discovered", {
395
- meta: { nodeId: connectedNodeId, addonId, capName, deviceIds }
396
- });
397
- } catch (err) {
398
- this.ctx.logger.debug("worker native-cap $listDeviceIds failed", {
399
- meta: { service: svc.name, nodeId: connectedNodeId, error: (0, import_types.errMsg)(err) }
400
- });
401
- }
402
- }
403
- this.ctx.logger.info("worker native-cap discovery completed", {
404
- meta: { nodeId: connectedNodeId, services: matched.length }
405
- });
406
- }
407
- async getBindings(input) {
408
- const storeKey = String(input.deviceId);
409
- const store = await this.readBindingsStore();
410
- const perDevice = store.deviceBindings[storeKey] ?? {};
411
- const entries = [];
412
- const seenCaps = /* @__PURE__ */ new Set();
413
- for (const [capName, { wrapperAddonId }] of Object.entries(perDevice)) {
414
- const hubLocalNative = this.capabilityRegistry?.getNativeAddonId(capName, input.deviceId) ?? null;
415
- const remoteNative = this.remoteNativeCaps.get(input.deviceId)?.get(capName) ?? null;
416
- const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
417
- const nativeNodeId = hubLocalNative ? this.ctx.kernel.localNodeId ?? "hub" : remoteNative?.nodeId ?? (this.ctx.kernel.localNodeId ?? "hub");
418
- if (wrapperAddonId === null && !nativeAddonId) {
419
- seenCaps.add(capName);
420
- continue;
421
- }
422
- entries.push({
423
- capName,
424
- kind: wrapperAddonId ? "wrapped" : "native",
425
- providerAddonId: wrapperAddonId ?? nativeAddonId,
426
- providerNodeId: wrapperAddonId ? this.resolveWrapperNodeId(wrapperAddonId) : nativeNodeId,
427
- nativeAddonId
428
- });
429
- seenCaps.add(capName);
430
- }
431
- const remote = this.remoteNativeCaps.get(input.deviceId);
432
- if (this.capabilityRegistry) {
433
- for (const capName of this.capabilityRegistry.getCapsWithDefaultWrapper()) {
434
- if (seenCaps.has(capName)) continue;
435
- const defaultWrapperAddonId = this.capabilityRegistry.getDefaultWrapperForCap(capName);
436
- if (!defaultWrapperAddonId) continue;
437
- const hubLocalNative = this.capabilityRegistry.getNativeAddonId(capName, input.deviceId) ?? null;
438
- const remoteNative = remote?.get(capName) ?? null;
439
- const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
440
- entries.push({
441
- capName,
442
- kind: "wrapped",
443
- providerAddonId: defaultWrapperAddonId,
444
- providerNodeId: this.resolveWrapperNodeId(defaultWrapperAddonId),
445
- nativeAddonId
446
- });
447
- seenCaps.add(capName);
448
- }
449
- }
450
- if (this.capabilityRegistry) {
451
- for (const capName of this.capabilityRegistry.getNativeCapsForDevice(input.deviceId)) {
452
- if (seenCaps.has(capName)) continue;
453
- const nativeAddonId = this.capabilityRegistry.getNativeAddonId(capName, input.deviceId) ?? "";
454
- entries.push({
455
- capName,
456
- kind: "native",
457
- providerAddonId: nativeAddonId,
458
- providerNodeId: this.ctx.kernel.localNodeId ?? "hub",
459
- nativeAddonId
460
- });
461
- seenCaps.add(capName);
462
- }
463
- }
464
- if (remote) {
465
- for (const [capName, info] of remote) {
466
- if (seenCaps.has(capName)) continue;
467
- entries.push({
468
- capName,
469
- kind: "native",
470
- providerAddonId: info.addonId,
471
- providerNodeId: info.nodeId,
472
- nativeAddonId: info.addonId
473
- });
474
- seenCaps.add(capName);
475
- }
476
- }
477
- return { deviceId: input.deviceId, entries };
478
- }
479
- /**
480
- * Whole-fleet binding dump. Iterates every device known to the
481
- * deviceRegistry and reuses the per-device `getBindings` resolver
482
- * for each same routing rules, single round-trip. Used by
483
- * `SystemManager.init()` for warm-boot.
484
- *
485
- * Bindings change rarely (wrapper toggle, device add/remove) so
486
- * clients invalidate via the existing
487
- * `capability.binding-changed` event rather than re-fetching this
488
- * payload periodically.
489
- */
490
- async getAllBindings() {
491
- const hubRegistry = this.ctx.kernel?.deviceRegistry;
492
- if (!hubRegistry) return [];
493
- const out = [];
494
- for (const device of hubRegistry.getAll()) {
495
- out.push(await this.getBindings({ deviceId: device.id }));
496
- }
497
- return out;
498
- }
499
- /**
500
- * Resolve a numeric deviceId to a stableId via persisted meta.
501
- * Used only by the device-identity section of the device-details
502
- * aggregator (see `buildBaseDeviceSection`) to surface the stableId as
503
- * a readonly display field. All runtime/registry lookups are keyed by
504
- * numeric deviceId; this helper is display-only.
505
- */
506
- async lookupPersistedStableId(deviceId) {
507
- const store = await this.ctx.settings.readAddonStore();
508
- const meta = store.deviceMeta ?? {};
509
- for (const [key, m] of Object.entries(meta)) {
510
- if (m.id === deviceId) {
511
- const sep = key.indexOf(":");
512
- if (sep < 0) continue;
513
- return key.slice(sep + 1);
514
- }
515
- }
516
- return void 0;
517
- }
518
- // ── Device-details aggregator ──────────────────────────────────────────────
519
- //
520
- // Walks the bindings for `deviceId` (native + wrapped), picks every
521
- // provider whose cap declares `exposesDeviceSettings: true`, and asks
522
- // each for its per-device contribution. Contributions are concatenated
523
- // into a single `ConfigUISchemaWithValues`; editable fields are tagged
524
- // with `writerCapName` + `writerAddonId` so `updateDeviceField` can
525
- // route the mutation back to the exact provider that produced them.
526
- //
527
- // `kind` selects settings vs live-info. The two aggregators share the
528
- // enumeration + tagging pipeline; only the method invoked on each
529
- // contributing provider differs.
530
- async getDeviceAggregate(deviceId, kind) {
531
- const registry = this.capabilityRegistry;
532
- if (!registry) {
533
- this.ctx.logger.debug("capability registry unavailable \u2014 aggregate empty", { meta: { kind } });
534
- return null;
535
- }
536
- const method = kind === "settings" ? "getDeviceSettingsContribution" : "getDeviceLiveContribution";
537
- const contributors = [];
538
- for (const info of registry.listCapabilities()) {
539
- const def = registry.getDefinition(info.name);
540
- if (!def?.exposesDeviceSettings) continue;
541
- const seen = /* @__PURE__ */ new Set();
542
- const sorted = [...info.providers].sort((a, b) => a.length - b.length);
543
- for (const addonId of sorted) {
544
- if (addonId.includes("::native-")) continue;
545
- const baseId = addonId.includes("@") ? addonId.slice(0, addonId.indexOf("@")) : addonId;
546
- if (seen.has(baseId)) continue;
547
- seen.add(baseId);
548
- contributors.push({ capName: info.name, addonId });
549
- }
550
- }
551
- const results = await Promise.all(contributors.map(async ({ capName, addonId }) => {
552
- const provider = registry.getProviderByAddon(capName, addonId);
553
- if (!provider) {
554
- throw new Error(
555
- `[device-manager] capability "${capName}" lists provider "${addonId}" but getProviderByAddon returned null \u2014 registry inconsistency`
556
- );
557
- }
558
- try {
559
- const contribution = await provider[method]({ deviceId });
560
- if (!contribution) return null;
561
- return {
562
- capName,
563
- addonId,
564
- contribution: tagContribution(toWireShape(contribution), capName, addonId, kind)
565
- };
566
- } catch (err) {
567
- const msg = err instanceof Error ? err.message : String(err);
568
- this.ctx.logger.warn("contribution method failed", { tags: { deviceId, addonId }, meta: { capName, method, error: msg } });
569
- return null;
570
- }
571
- }));
572
- const base = kind === "settings" ? await this.buildBaseDeviceSection(deviceId) : null;
573
- const parts = [
574
- ...base ? [tagContribution(base, "device-manager", "device-manager", kind)] : [],
575
- ...results.filter((r) => r !== null).map((r) => r.contribution)
576
- ];
577
- if (parts.length === 0) return null;
578
- return mergeAggregates(parts);
579
- }
580
- /**
581
- * Build the device-manager's own contribution to the aggregator — the
582
- * device identity (id, stableId, addonId, type, online) + the
583
- * driver-specific config exposed by the device class via
584
- * `zodEntriesToConfigUI`.
585
- *
586
- * Two paths, deliberately symmetric with `getSettingsSchema`:
587
- *
588
- * - Hub-local: device's IDevice instance lives in this process'
589
- * DeviceRegistry, we read config + schema directly by reference.
590
- * - Cross-process: device lives in a forked worker (RtspCamera on
591
- * provider-rtsp, ONVIF on provider-onvif, …). We ask the worker's
592
- * `device-ops.getSettingsSchema` native provider for a wire-
593
- * serializable ConfigUISchema and merge it in under the same
594
- * "Driver Config" section, so the UI sees the same shape regardless
595
- * of where the IDevice physically runs.
596
- *
597
- * Returns `null` only when the device genuinely doesn't exist anywhere
598
- * (no hub-local, no persisted ownership, no device-ops native). The
599
- * aggregator falls back to contributor sections only in that case.
600
- */
601
- async buildBaseDeviceSection(deviceId) {
602
- const hubRegistry = this.ctx.kernel?.deviceRegistry;
603
- const hubLocal = hubRegistry ? resolveDeviceById(hubRegistry, deviceId) : null;
604
- const stableId = hubLocal?.device.stableId ?? await this.lookupPersistedStableId(deviceId);
605
- const nativeOwner = this.resolveNativeDeviceOwner(deviceId);
606
- const addonId = hubLocal?.addonId ?? nativeOwner?.addonId ?? null;
607
- if (!hubLocal && !nativeOwner) return null;
608
- const identityFields = [
609
- {
610
- type: "text",
611
- key: "_deviceId",
612
- label: "Device ID",
613
- readonlyField: true,
614
- value: String(deviceId)
615
- },
616
- {
617
- type: "text",
618
- key: "_stableId",
619
- label: "Stable ID",
620
- readonlyField: true,
621
- value: stableId ?? ""
622
- },
623
- {
624
- type: "text",
625
- key: "_addonId",
626
- label: "Driver",
627
- readonlyField: true,
628
- value: addonId ?? "unknown"
629
- },
630
- ...hubLocal ? [
631
- {
632
- type: "text",
633
- key: "_type",
634
- label: "Type",
635
- readonlyField: true,
636
- value: hubLocal.device.type
637
- },
638
- {
639
- type: "text",
640
- key: "_online",
641
- label: "Online",
642
- readonlyField: true,
643
- value: hubLocal.device.online ? "yes" : "no"
644
- }
645
- ] : []
646
- ];
647
- const sections = [
648
- {
649
- id: "device-identity",
650
- title: "Identity",
651
- tab: "general",
652
- order: 0,
653
- fields: identityFields
654
- }
655
- ];
656
- const driverSchema = await this.resolveDriverConfigSchema(deviceId, hubLocal);
657
- if (driverSchema) {
658
- for (const section of driverSchema.sections) {
659
- sections.push({
660
- id: section.id,
661
- title: section.title,
662
- tab: section.tab ?? "general",
663
- order: section.order ?? 1,
664
- fields: [...section.fields],
665
- ...section.description !== void 0 ? { description: section.description } : {},
666
- ...section.columns !== void 0 ? { columns: section.columns } : {}
667
- });
668
- }
669
- }
670
- return {
671
- sections,
672
- ...driverSchema?.tabs ? { tabs: [...driverSchema.tabs] } : {}
673
- };
674
- }
675
- /**
676
- * Lookup the native owner for `device-ops` on `deviceId` the native-cap
677
- * registry (hub-local and remote) is keyed by numeric id.
678
- */
679
- resolveNativeDeviceOwner(deviceId) {
680
- const local = this.capabilityRegistry?.getNativeAddonId("device-ops", deviceId) ?? null;
681
- if (local) return { addonId: local, nodeId: this.ctx.kernel.localNodeId ?? "hub" };
682
- const remote = this.remoteNativeCaps.get(deviceId)?.get("device-ops") ?? null;
683
- return remote ? { addonId: remote.addonId, nodeId: remote.nodeId } : null;
684
- }
685
- /**
686
- * Aggregate `status` across every registered cap for a device.
687
- *
688
- * Walks the supplied cap list (or `CAP_NAMES_WITH_STATUS` when
689
- * omitted), looks up a native provider per cap via the capability
690
- * registry, calls `provider.getStatus({ deviceId })`, and validates
691
- * the return against the cap's own `status.schema`. Validation
692
- * failures log a warning and yield `null` for that cap so the
693
- * overall aggregate stays usable — a single misbehaving provider
694
- * must not blank out a device's entire status view.
695
- *
696
- * Returned shape is `Record<capName, unknown | null>`; the client-
697
- * side hook tightens this to `CapStatusTypeMap` via the generated
698
- * `cap-status-types.ts`.
699
- */
700
- async getDeviceStatusAggregate(input) {
701
- const capNames = input.caps ?? import_types2.CAP_NAMES_WITH_STATUS;
702
- const registry = this.capabilityRegistry;
703
- const out = {};
704
- if (!registry) {
705
- for (const name of capNames) out[name] = null;
706
- return out;
707
- }
708
- await Promise.all(capNames.map(async (capName) => {
709
- try {
710
- const def = registry.getDefinition(capName);
711
- if (!def?.status) {
712
- out[capName] = null;
713
- return;
714
- }
715
- const provider = registry.getNativeProvider(capName, input.deviceId);
716
- if (!provider || typeof provider.getStatus !== "function") {
717
- out[capName] = null;
718
- return;
719
- }
720
- const raw = await provider.getStatus({ deviceId: input.deviceId });
721
- if (raw == null) {
722
- out[capName] = null;
723
- return;
724
- }
725
- const parsed = def.status.schema.safeParse(raw);
726
- if (!parsed.success) {
727
- this.ctx.logger.warn("getDeviceStatusAggregate: provider returned invalid status, dropping", {
728
- tags: { deviceId: input.deviceId },
729
- meta: { capName, issues: parsed.error.issues.slice(0, 3) }
730
- });
731
- out[capName] = null;
732
- return;
733
- }
734
- out[capName] = parsed.data;
735
- } catch (err) {
736
- this.ctx.logger.warn("getDeviceStatusAggregate: provider threw, dropping", {
737
- tags: { deviceId: input.deviceId },
738
- meta: { capName, error: (0, import_types.errMsg)(err) }
739
- });
740
- out[capName] = null;
741
- }
742
- }));
743
- return out;
744
- }
745
- /**
746
- * Return the driver-specific device-settings contribution. Hub-local
747
- * devices call `getSettingsUISchema()` directly; forked-worker devices
748
- * go through the `device-ops.getSettingsSchema` cap method on the
749
- * numeric-id-keyed native registry.
750
- */
751
- async resolveDriverConfigSchema(deviceId, hubLocal) {
752
- if (hubLocal) {
753
- const schema = hubLocal.device.getSettingsUISchema();
754
- return schema.sections.length === 0 ? null : toWireShape(schema);
755
- }
756
- const ops = this.capabilityRegistry?.getNativeProvider("device-ops", deviceId);
757
- if (!ops) return null;
758
- try {
759
- const schema = await ops.getSettingsSchema({ deviceId });
760
- if (!schema) return null;
761
- const wire = schema;
762
- return wire.sections.length === 0 ? null : toWireShape(wire);
763
- } catch (err) {
764
- const msg = err instanceof Error ? err.message : String(err);
765
- this.ctx.logger.warn("cross-process getSettingsSchema failed", { tags: { deviceId }, meta: { error: msg } });
766
- return null;
767
- }
768
- }
769
- async updateDeviceField(input) {
770
- if (input.writerCapName === "device-manager") {
771
- const hubRegistry = this.ctx.kernel?.deviceRegistry;
772
- const found = hubRegistry ? resolveDeviceById(hubRegistry, input.deviceId) : null;
773
- if (found) {
774
- await found.device.applySettingsPatch({ [input.key]: input.value });
775
- return { success: true };
776
- }
777
- const ops = this.capabilityRegistry?.getNativeProvider("device-ops", input.deviceId);
778
- if (!ops) {
779
- throw new Error(`[device-manager] device "${input.deviceId}" not found (no hub-local entry, no device-ops native provider)`);
780
- }
781
- await ops.setConfig({ deviceId: input.deviceId, values: { [input.key]: input.value } });
782
- return { success: true };
783
- }
784
- const registry = this.capabilityRegistry;
785
- if (!registry) {
786
- throw new Error("[device-manager] updateDeviceField requires capability registry \u2014 unavailable on this node");
787
- }
788
- const def = registry.getDefinition(input.writerCapName);
789
- if (!def?.exposesDeviceSettings) {
790
- throw new Error(`[device-manager] cap "${input.writerCapName}" does not expose device settings`);
791
- }
792
- const provider = registry.getProviderByAddon(input.writerCapName, input.writerAddonId);
793
- if (!provider) {
794
- throw new Error(`[device-manager] provider "${input.writerAddonId}" not registered for cap "${input.writerCapName}"`);
795
- }
796
- await provider.applyDeviceSettingsPatch({
797
- deviceId: input.deviceId,
798
- patch: { [input.key]: input.value }
799
- });
800
- return { success: true };
801
- }
802
- /**
803
- * Batched counterpart of `updateDeviceField`. Groups changes by
804
- * `(writerCapName, writerAddonId)` so each contributor receives a
805
- * single `applyDeviceSettingsPatch` with all of its updates merged —
806
- * avoids N round-trips for simultaneous edits in the same save.
807
- *
808
- * Per-provider failures are captured in the `failures[]` output so the
809
- * admin UI can highlight which sections didn't persist; a failure on
810
- * one provider does NOT abort the others.
811
- */
812
- async updateDeviceFieldsBatch(input) {
813
- const groups = /* @__PURE__ */ new Map();
814
- for (const change of input.changes) {
815
- const key = `${change.writerCapName}::${change.writerAddonId}`;
816
- const existing = groups.get(key);
817
- if (existing) {
818
- existing.patch[change.key] = change.value;
819
- } else {
820
- groups.set(key, {
821
- writerCapName: change.writerCapName,
822
- writerAddonId: change.writerAddonId,
823
- patch: { [change.key]: change.value }
824
- });
825
- }
826
- }
827
- const failures = [];
828
- for (const group of groups.values()) {
829
- try {
830
- await this.applyGroupPatch(input.deviceId, group);
831
- } catch (err) {
832
- failures.push({
833
- writerCapName: group.writerCapName,
834
- writerAddonId: group.writerAddonId,
835
- error: err instanceof Error ? err.message : String(err)
836
- });
837
- }
838
- }
839
- return { success: true, failures };
840
- }
841
- /** Apply a single grouped patch to the appropriate provider. Mirrors
842
- * `updateDeviceField` routing (special-case device-manager, else
843
- * registry lookup). Used by `updateDeviceFieldsBatch`. */
844
- async applyGroupPatch(deviceId, group) {
845
- if (group.writerCapName === "device-manager") {
846
- const hubRegistry = this.ctx.kernel?.deviceRegistry;
847
- const found = hubRegistry ? resolveDeviceById(hubRegistry, deviceId) : null;
848
- if (found) {
849
- await found.device.applySettingsPatch(group.patch);
850
- return;
851
- }
852
- const ops = this.capabilityRegistry?.getNativeProvider("device-ops", deviceId);
853
- if (!ops) {
854
- throw new Error(`[device-manager] device "${deviceId}" not found (no hub-local entry, no device-ops native provider)`);
855
- }
856
- await ops.setConfig({ deviceId, values: group.patch });
857
- return;
858
- }
859
- const registry = this.capabilityRegistry;
860
- if (!registry) throw new Error("[device-manager] capability registry unavailable");
861
- const def = registry.getDefinition(group.writerCapName);
862
- if (!def?.exposesDeviceSettings) {
863
- throw new Error(`[device-manager] cap "${group.writerCapName}" does not expose device settings`);
864
- }
865
- const provider = registry.getProviderByAddon(group.writerCapName, group.writerAddonId);
866
- if (!provider) {
867
- throw new Error(`[device-manager] provider "${group.writerAddonId}" not registered for cap "${group.writerCapName}"`);
868
- }
869
- await provider.applyDeviceSettingsPatch({ deviceId, patch: group.patch });
870
- }
871
- async listWrappersForCap(input) {
872
- return [...this.capabilityRegistry?.getWrappersForCap(input.capName) ?? []];
873
- }
874
- async listBindableCapsForDeviceType(input) {
875
- const registry = this.capabilityRegistry;
876
- if (!registry) return [];
877
- const capNames = registry.listDeviceScopedCapsForType(input.deviceType);
878
- return capNames.map((capName) => ({
879
- capName,
880
- wrappers: [...registry.getWrappersForCap(capName)]
881
- }));
882
- }
883
- async setWrapperActive(input) {
884
- const storeKey = String(input.deviceId);
885
- const store = await this.readBindingsStore();
886
- const perDevice = { ...store.deviceBindings[storeKey] ?? {} };
887
- if (input.active) {
888
- perDevice[input.capName] = { wrapperAddonId: input.wrapperAddonId };
889
- } else {
890
- perDevice[input.capName] = { wrapperAddonId: null };
891
- }
892
- const nextDeviceBindings = Object.keys(perDevice).length > 0 ? { ...store.deviceBindings, [storeKey]: perDevice } : (() => {
893
- const { [storeKey]: _drop, ...rest } = store.deviceBindings;
894
- return rest;
895
- })();
896
- await this.writeBindingsStore({ deviceBindings: nextDeviceBindings });
897
- this.ctx.eventBus.emit({
898
- id: (0, import_node_crypto.randomUUID)(),
899
- timestamp: /* @__PURE__ */ new Date(),
900
- source: { type: "addon", id: this.ctx.id },
901
- category: import_types.EventCategory.DeviceBindingsChanged,
902
- data: {
903
- deviceId: input.deviceId,
904
- capName: input.capName,
905
- reason: input.active ? "wrapper-activated" : "wrapper-deactivated",
906
- addonId: input.wrapperAddonId,
907
- nodeId: this.resolveWrapperNodeId(input.wrapperAddonId)
908
- }
909
- });
910
- }
911
- async onInitialize() {
912
- const settings = this.ctx.settings;
913
- if (!settings) {
914
- this.ctx.logger.warn("ctx.settings not available \u2014 device persistence unavailable");
915
- return;
916
- }
917
- const registry = this.ctx.kernel.deviceRegistry ?? null;
918
- if (!registry) {
919
- this.ctx.logger.warn("device-registry not available \u2014 live operations will use persisted data only");
920
- }
921
- const localNodeId = this.ctx.kernel.localNodeId ?? "hub";
922
- this.ctx.eventBus.subscribe(
923
- { category: import_types.EventCategory.DeviceBindingsChanged },
924
- (event) => {
925
- const { deviceId, capName, reason, addonId, nodeId } = event.data;
926
- if (nodeId === localNodeId) return;
927
- if (reason === "native-registered") {
928
- let perDevice = this.remoteNativeCaps.get(deviceId);
929
- if (!perDevice) {
930
- perDevice = /* @__PURE__ */ new Map();
931
- this.remoteNativeCaps.set(deviceId, perDevice);
932
- }
933
- perDevice.set(capName, { addonId, nodeId });
934
- } else if (reason === "native-unregistered") {
935
- const perDevice = this.remoteNativeCaps.get(deviceId);
936
- if (!perDevice) return;
937
- perDevice.delete(capName);
938
- if (perDevice.size === 0) this.remoteNativeCaps.delete(deviceId);
939
- }
940
- }
941
- );
942
- const cluster = this.ctx.kernel.cluster;
943
- if (cluster) {
944
- cluster.broker.localBus.on("$node.disconnected", (payload) => {
945
- const gone = payload.node.id;
946
- const emptyDevices = [];
947
- for (const [deviceId, perDevice] of this.remoteNativeCaps) {
948
- const toDelete = [];
949
- for (const [capName, entry] of perDevice) {
950
- if (entry.nodeId === gone) toDelete.push(capName);
951
- }
952
- for (const capName of toDelete) perDevice.delete(capName);
953
- if (perDevice.size === 0) emptyDevices.push(deviceId);
954
- }
955
- for (const deviceId of emptyDevices) this.remoteNativeCaps.delete(deviceId);
956
- });
957
- }
958
- const requireDeviceOps = (deviceId) => {
959
- const ops = this.capabilityRegistry?.getNativeProvider("device-ops", deviceId);
960
- if (!ops) {
961
- throw new Error(`[device-manager] device-ops native provider not found for '${deviceId}'`);
962
- }
963
- return ops;
964
- };
965
- const readStore = async () => {
966
- return await settings.readAddonStore();
967
- };
968
- const readIndex = async () => {
969
- const store = await readStore();
970
- return store.deviceIndex ?? {};
971
- };
972
- const readMeta = async () => {
973
- const store = await readStore();
974
- return store.deviceMeta ?? {};
975
- };
976
- const readMetadataMap = async () => {
977
- const store = await readStore();
978
- return store.deviceMetadata ?? {};
979
- };
980
- let metaWriteChain = Promise.resolve();
981
- const withMetaWriteLock = async (fn) => {
982
- const previous = metaWriteChain;
983
- let release = () => {
984
- };
985
- const next = new Promise((resolve) => {
986
- release = resolve;
987
- });
988
- metaWriteChain = next;
989
- try {
990
- await previous.catch(() => {
991
- });
992
- return await fn();
993
- } finally {
994
- release();
995
- }
996
- };
997
- const resolvePersistedById = async (deviceId) => {
998
- const meta = await readMeta();
999
- for (const [key, m] of Object.entries(meta)) {
1000
- if (m.id === deviceId) {
1001
- const sep = key.indexOf(":");
1002
- if (sep < 0) continue;
1003
- return { addonId: key.slice(0, sep), stableId: key.slice(sep + 1), meta: m };
1004
- }
1005
- }
1006
- return null;
1007
- };
1008
- const idToAddonId = /* @__PURE__ */ new Map();
1009
- {
1010
- const meta = await readMeta();
1011
- for (const [key, m] of Object.entries(meta)) {
1012
- const sep = key.indexOf(":");
1013
- if (sep < 0) continue;
1014
- idToAddonId.set(m.id, key.slice(0, sep));
1015
- }
1016
- }
1017
- if (cluster) {
1018
- cluster.broker.localBus.on("$node.connected", (payload) => {
1019
- const connectedNodeId = payload.node.id;
1020
- const lastSlash = connectedNodeId.lastIndexOf("/");
1021
- if (lastSlash < 0) return;
1022
- const connectedAddonId = connectedNodeId.slice(lastSlash + 1);
1023
- if (connectedAddonId.length === 0) return;
1024
- for (const [deviceId, ownerAddonId] of idToAddonId) {
1025
- if (ownerAddonId !== connectedAddonId) continue;
1026
- if (this.capabilityRegistry?.getNativeAddonId("device-ops", deviceId)) continue;
1027
- let perDevice = this.remoteNativeCaps.get(deviceId);
1028
- if (!perDevice) {
1029
- perDevice = /* @__PURE__ */ new Map();
1030
- this.remoteNativeCaps.set(deviceId, perDevice);
1031
- }
1032
- if (!perDevice.has("device-ops")) {
1033
- perDevice.set("device-ops", { addonId: connectedAddonId, nodeId: connectedNodeId });
1034
- }
1035
- }
1036
- setTimeout(() => {
1037
- this.discoverWorkerNativeCaps(connectedNodeId, connectedAddonId).catch((err) => {
1038
- this.ctx.logger.warn("worker native-cap discovery failed", {
1039
- meta: { nodeId: connectedNodeId, addonId: connectedAddonId, error: (0, import_types.errMsg)(err) }
1040
- });
1041
- });
1042
- }, 500);
1043
- });
1044
- }
1045
- const allocateNextDeviceId = async () => {
1046
- const store = await readStore();
1047
- const current = store.nextDeviceId ?? 1;
1048
- await settings.writeAddonStore({ nextDeviceId: current + 1 });
1049
- return current;
1050
- };
1051
- const provider = {
1052
- /** Sync ownership lookup backing persistence fallbacks (e.g. remove()
1053
- * when the owning worker is offline). Ownership is keyed by numeric
1054
- * deviceId owning addonId as recorded in the persisted meta. NOT
1055
- * a native-cap lookup: an addon can own a device without registering
1056
- * every possible cap natively (e.g. RtspCamera without snapshotUrl
1057
- * doesn't register the snapshot cap). Use
1058
- * `resolveNativeCapOwnerSync` for cap-resolution paths.
1059
- */
1060
- resolveDeviceOwnerSync: (deviceId) => {
1061
- return idToAddonId.get(deviceId) ?? null;
1062
- },
1063
- /** Sync lookup for the addon that registered a native provider for
1064
- * `(capName, deviceId)`. Backs `CapabilityRegistry`'s native fallback
1065
- * so the hub only synthesizes a cross-process proxy when the cap is
1066
- * actually published — never on speculative device ownership.
1067
- *
1068
- * Consults hub-local registrations first (in-process natives),
1069
- * then the `remoteNativeCaps` map populated from
1070
- * `DeviceBindingsChanged` events emitted by forked-worker
1071
- * `registerNativeCap` calls. Both are generic: any addon that hosts
1072
- * devices and registers caps via the standard context API shows up
1073
- * here without per-addon branching.
1074
- */
1075
- resolveNativeCapOwnerSync: (capName, deviceId) => {
1076
- const localAddonId = this.capabilityRegistry?.getNativeAddonId(capName, deviceId) ?? null;
1077
- if (localAddonId) {
1078
- return { addonId: localAddonId, nodeId: this.ctx.kernel.localNodeId ?? "hub" };
1079
- }
1080
- const remote = this.remoteNativeCaps.get(deviceId)?.get(capName) ?? null;
1081
- if (remote) return { addonId: remote.addonId, nodeId: remote.nodeId };
1082
- return null;
1083
- },
1084
- // ── Persistence ────────────────────────────────────────────────────────
1085
- /** Idempotent numeric-id reservation. Callers invoke this before
1086
- * constructing the owning `IDevice` so `DeviceContext.id` is bound
1087
- * at construction time. A repeat call for the same `(addonId,
1088
- * stableId)` returns the already-persisted id — same physical
1089
- * device reconnecting after a driver restart keeps its original
1090
- * number. Fresh pairs burn one slot from the monotonic
1091
- * `nextDeviceId` counter and seed a meta placeholder so the
1092
- * `deviceMeta` → `id` invariant holds even before
1093
- * `registerDevice` completes. */
1094
- allocateDeviceId: async (input) => {
1095
- const { addonId, stableId } = input;
1096
- const key = deviceKey(addonId, stableId);
1097
- return await withMetaWriteLock(async () => {
1098
- const meta = await readMeta();
1099
- const existing = meta[key];
1100
- if (existing) return { id: existing.id };
1101
- const id = await allocateNextDeviceId();
1102
- await settings.writeAddonStore({
1103
- deviceMeta: {
1104
- ...meta,
1105
- [key]: {
1106
- type: "generic",
1107
- name: stableId,
1108
- location: null,
1109
- disabled: false,
1110
- parentDeviceId: null,
1111
- id
1112
- }
1113
- }
1114
- });
1115
- return { id };
1116
- });
1117
- },
1118
- registerDevice: async (input) => {
1119
- const { addonId, stableId, id, type, name, parentDeviceId, features, config } = input;
1120
- const key = deviceKey(addonId, stableId);
1121
- const featuresArr = Array.isArray(features) ? [...features] : [];
1122
- const { isFirstRegistration } = await withMetaWriteLock(async () => {
1123
- const index = await readIndex();
1124
- const existing = index[addonId] ?? [];
1125
- const wasInIndex = existing.includes(stableId);
1126
- if (!wasInIndex) {
1127
- await settings.writeAddonStore({
1128
- deviceIndex: { ...index, [addonId]: [...existing, stableId] }
1129
- });
1130
- }
1131
- const meta = await readMeta();
1132
- const existingMeta = meta[key];
1133
- const isFirst = !existingMeta || !wasInIndex;
1134
- await settings.writeAddonStore({
1135
- deviceMeta: {
1136
- ...meta,
1137
- [key]: {
1138
- type,
1139
- // Prefer the persisted name (set by `setName` pre-construct)
1140
- // over the construction-time fallback. Only when the meta
1141
- // row literally still carries `name === stableId` (the
1142
- // placeholder seeded by `allocateDeviceId`) do we fall
1143
- // back to the action arg.
1144
- name: existingMeta && existingMeta.name !== stableId ? existingMeta.name : name,
1145
- location: existingMeta?.location ?? null,
1146
- disabled: existingMeta?.disabled ?? false,
1147
- parentDeviceId,
1148
- id,
1149
- features: featuresArr
1150
- }
1151
- }
1152
- });
1153
- return { isFirstRegistration: isFirst };
1154
- });
1155
- if (Object.keys(config).length > 0) {
1156
- await settings.writeDeviceStore(id, config);
1157
- }
1158
- idToAddonId.set(id, addonId);
1159
- if (isFirstRegistration) {
1160
- this.ctx.eventBus.emit({
1161
- id: (0, import_node_crypto.randomUUID)(),
1162
- timestamp: /* @__PURE__ */ new Date(),
1163
- source: { type: "device", id },
1164
- category: import_types.EventCategory.DeviceRegistered,
1165
- data: {
1166
- deviceId: id,
1167
- name: name.length > 0 ? name : stableId,
1168
- providerId: addonId,
1169
- parentDeviceId: parentDeviceId ?? null
1170
- }
1171
- });
1172
- } else {
1173
- this.ctx.eventBus.emit({
1174
- id: (0, import_node_crypto.randomUUID)(),
1175
- timestamp: /* @__PURE__ */ new Date(),
1176
- source: { type: "device", id },
1177
- category: import_types.EventCategory.DeviceMetaChanged,
1178
- data: {
1179
- deviceId: id,
1180
- name: name.length > 0 ? name : stableId,
1181
- providerId: addonId,
1182
- parentDeviceId: parentDeviceId ?? null,
1183
- features: featuresArr
1184
- }
1185
- });
1186
- }
1187
- },
1188
- removeDevice: async (input) => {
1189
- const { deviceId } = input;
1190
- const persisted = await resolvePersistedById(deviceId);
1191
- if (!persisted) {
1192
- return;
1193
- }
1194
- const { addonId, stableId, meta: persistedMeta } = persisted;
1195
- const key = deviceKey(addonId, stableId);
1196
- const deviceName = persistedMeta.name;
1197
- await withMetaWriteLock(async () => {
1198
- const index = await readIndex();
1199
- const remaining = (index[addonId] ?? []).filter((sid) => sid !== stableId);
1200
- const updatedIndex = remaining.length > 0 ? { ...index, [addonId]: remaining } : (() => {
1201
- const { [addonId]: _removed, ...rest } = index;
1202
- return rest;
1203
- })();
1204
- await settings.writeAddonStore({ deviceIndex: updatedIndex });
1205
- const meta = await readMeta();
1206
- const { [key]: _removedMeta, ...restMeta } = meta;
1207
- await settings.writeAddonStore({ deviceMeta: restMeta });
1208
- const map = await readMetadataMap();
1209
- if (key in map) {
1210
- const { [key]: _removedMetadata, ...restMap } = map;
1211
- await settings.writeAddonStore({ deviceMetadata: restMap });
1212
- }
1213
- });
1214
- await settings.clearDeviceStore(deviceId);
1215
- const bindingsStore = await this.readBindingsStore();
1216
- const bindingKey = String(deviceId);
1217
- if (bindingsStore.deviceBindings[bindingKey]) {
1218
- const { [bindingKey]: _removedBindings, ...restBindings } = bindingsStore.deviceBindings;
1219
- await this.writeBindingsStore({ deviceBindings: restBindings });
1220
- }
1221
- this.remoteNativeCaps.delete(deviceId);
1222
- this.capabilityRegistry?.unregisterAllNativeForDevice(deviceId);
1223
- idToAddonId.delete(deviceId);
1224
- this.ctx.logger.info("removed device", { tags: { deviceId, deviceName: deviceName.length > 0 ? deviceName : stableId } });
1225
- this.ctx.eventBus.emit({
1226
- id: (0, import_node_crypto.randomUUID)(),
1227
- timestamp: /* @__PURE__ */ new Date(),
1228
- source: { type: "device", id: deviceId },
1229
- category: import_types.EventCategory.DeviceUnregistered,
1230
- data: {
1231
- deviceId,
1232
- providerId: addonId,
1233
- // Carry parentDeviceId so listeners can filter on "is
1234
- // this MY child?" without consulting an in-memory map
1235
- // they had to keep in sync. By the time this event
1236
- // fires the meta row is already gone — we read it from
1237
- // the resolved snapshot above.
1238
- parentDeviceId: persistedMeta.parentDeviceId ?? null
1239
- }
1240
- });
1241
- },
1242
- persistConfig: async (input) => {
1243
- const { deviceId, data } = input;
1244
- const persisted = await resolvePersistedById(deviceId);
1245
- if (!persisted) throw new Error(`[device-manager] persistConfig: unknown device id=${deviceId}`);
1246
- await settings.writeDeviceStore(deviceId, data);
1247
- },
1248
- loadConfig: async (input) => {
1249
- const { deviceId } = input;
1250
- const persisted = await resolvePersistedById(deviceId);
1251
- if (!persisted) return {};
1252
- return settings.readDeviceStore(deviceId);
1253
- },
1254
- /**
1255
- * Load the operator-organisational meta surface for one device
1256
- * (`name` / `location` / `disabled` / `type` / `parentDeviceId`
1257
- * / `addonId` + `id` / `stableId`). Used by the kernel proxy's
1258
- * device-context factory to populate `ctx.deviceMeta` before
1259
- * the device class constructor runs. Returns `null` when no
1260
- * persisted row exists for the id.
1261
- *
1262
- * Reads default `location` to `null` and `disabled` to `false`
1263
- * for legacy rows that predate the field — production code
1264
- * relies on the IDevice type contract that both are present.
1265
- */
1266
- loadMeta: async (input) => {
1267
- const { deviceId } = input;
1268
- const persisted = await resolvePersistedById(deviceId);
1269
- if (!persisted) return null;
1270
- const { addonId, stableId, meta: m } = persisted;
1271
- const key = deviceKey(addonId, stableId);
1272
- const map = await readMetadataMap();
1273
- const metadata = map[key] ?? null;
1274
- return {
1275
- id: m.id,
1276
- stableId,
1277
- addonId,
1278
- type: m.type,
1279
- name: m.name,
1280
- location: m.location ?? null,
1281
- disabled: m.disabled ?? false,
1282
- parentDeviceId: m.parentDeviceId,
1283
- metadata
1284
- };
1285
- },
1286
- /**
1287
- * Update the operator-edited display name. Writes the meta
1288
- * row, emits a `DeviceMetaChanged` event so live consumers
1289
- * (UI device list, alert center) see the rename without
1290
- * polling. The live `IDevice.name` mirror is updated by the
1291
- * kernel proxy on its side (`device-cap-proxy.ts`).
1292
- */
1293
- setName: async (input) => {
1294
- const { deviceId, name } = input;
1295
- await withMetaWriteLock(async () => {
1296
- const persisted = await resolvePersistedById(deviceId);
1297
- if (!persisted) throw new Error(`[device-manager] setName: unknown device id=${deviceId}`);
1298
- const { addonId, stableId, meta: m } = persisted;
1299
- const key = deviceKey(addonId, stableId);
1300
- const allMeta = await readMeta();
1301
- await settings.writeAddonStore({
1302
- deviceMeta: { ...allMeta, [key]: { ...m, name } }
1303
- });
1304
- });
1305
- this.ctx.eventBus.emit({
1306
- id: (0, import_node_crypto.randomUUID)(),
1307
- timestamp: /* @__PURE__ */ new Date(),
1308
- source: { type: "device", id: deviceId },
1309
- category: import_types.EventCategory.DeviceMetaChanged,
1310
- data: { deviceId, field: "name", value: name }
1311
- });
1312
- },
1313
- /**
1314
- * Update the operator-organisational location label. `null`
1315
- * clears it. Mirrors the same persist-then-emit shape as
1316
- * `setName`; consumers subscribe to `DeviceMetaChanged` and
1317
- * filter on `field: 'location'`.
1318
- */
1319
- setLocation: async (input) => {
1320
- const { deviceId, location } = input;
1321
- await withMetaWriteLock(async () => {
1322
- const persisted = await resolvePersistedById(deviceId);
1323
- if (!persisted) throw new Error(`[device-manager] setLocation: unknown device id=${deviceId}`);
1324
- const { addonId, stableId, meta: m } = persisted;
1325
- const key = deviceKey(addonId, stableId);
1326
- const allMeta = await readMeta();
1327
- await settings.writeAddonStore({
1328
- deviceMeta: { ...allMeta, [key]: { ...m, location } }
1329
- });
1330
- });
1331
- this.ctx.eventBus.emit({
1332
- id: (0, import_node_crypto.randomUUID)(),
1333
- timestamp: /* @__PURE__ */ new Date(),
1334
- source: { type: "device", id: deviceId },
1335
- category: import_types.EventCategory.DeviceMetaChanged,
1336
- data: { deviceId, field: "location", value: location }
1337
- });
1338
- },
1339
- /**
1340
- * Patch the device's hardware-identity metadata blob. Shallow
1341
- * merge `null` removes a key, anything else overwrites.
1342
- * Drivers populate factual fields on first probe; operators
1343
- * augment via the Device Info tab. Idempotent: a no-op patch
1344
- * (every key already present with the same value) doesn't emit
1345
- * the meta-changed event.
1346
- */
1347
- setMetadata: async (input) => {
1348
- const { deviceId, patch } = input;
1349
- const result = await withMetaWriteLock(async () => {
1350
- const persisted = await resolvePersistedById(deviceId);
1351
- if (!persisted) throw new Error(`[device-manager] setMetadata: unknown device id=${deviceId}`);
1352
- const { addonId, stableId } = persisted;
1353
- const key = deviceKey(addonId, stableId);
1354
- const map = await readMetadataMap();
1355
- const current = map[key] ?? {};
1356
- const next = { ...current };
1357
- let changed = false;
1358
- for (const [k, v] of Object.entries(patch)) {
1359
- if (v === null) {
1360
- if (k in next) {
1361
- delete next[k];
1362
- changed = true;
1363
- }
1364
- } else if (next[k] !== v) {
1365
- next[k] = v;
1366
- changed = true;
1367
- }
1368
- }
1369
- if (!changed) return { changed: false };
1370
- const hasFields = Object.keys(next).length > 0;
1371
- const updatedMap = { ...map };
1372
- if (hasFields) {
1373
- updatedMap[key] = next;
1374
- } else {
1375
- delete updatedMap[key];
1376
- }
1377
- await settings.writeAddonStore({ deviceMetadata: updatedMap });
1378
- return { changed: true, finalMeta: hasFields ? next : null };
1379
- });
1380
- if (!result.changed) return;
1381
- this.ctx.eventBus.emit({
1382
- id: (0, import_node_crypto.randomUUID)(),
1383
- timestamp: /* @__PURE__ */ new Date(),
1384
- source: { type: "device", id: deviceId },
1385
- category: import_types.EventCategory.DeviceMetaChanged,
1386
- data: { deviceId, field: "metadata", value: result.finalMeta }
1387
- });
1388
- },
1389
- /**
1390
- * Soft-disable the device. Persisted on the meta row;
1391
- * lifecycle gating is the driver's responsibility (BaseDevice
1392
- * exposes `this.disabled` for the driver to consult at the top
1393
- * of its lifecycle methods).
1394
- */
1395
- setDisabled: async (input) => {
1396
- const { deviceId, disabled } = input;
1397
- await withMetaWriteLock(async () => {
1398
- const persisted = await resolvePersistedById(deviceId);
1399
- if (!persisted) throw new Error(`[device-manager] setDisabled: unknown device id=${deviceId}`);
1400
- const { addonId, stableId, meta: m } = persisted;
1401
- const key = deviceKey(addonId, stableId);
1402
- const allMeta = await readMeta();
1403
- await settings.writeAddonStore({
1404
- deviceMeta: { ...allMeta, [key]: { ...m, disabled } }
1405
- });
1406
- });
1407
- this.ctx.eventBus.emit({
1408
- id: (0, import_node_crypto.randomUUID)(),
1409
- timestamp: /* @__PURE__ */ new Date(),
1410
- source: { type: "device", id: deviceId },
1411
- category: import_types.EventCategory.DeviceMetaChanged,
1412
- data: { deviceId, field: "disabled", value: disabled }
1413
- });
1414
- },
1415
- loadRuntimeState: async (input) => {
1416
- const { deviceId } = input;
1417
- const persisted = await resolvePersistedById(deviceId);
1418
- if (!persisted) return {};
1419
- const data = await settings.readDeviceRuntimeState(deviceId);
1420
- this.seedMirror(deviceId, data);
1421
- return data;
1422
- },
1423
- /**
1424
- * Union of (1) operator-curated location registry and (2) labels
1425
- * currently in use on persisted devices. Case-insensitive
1426
- * dedupe (preserves the first-seen casing). Sorted
1427
- * case-insensitively for stable UI. Drives the Device Info
1428
- * location autocomplete.
1429
- */
1430
- listLocations: async () => {
1431
- const store = await settings.readAddonStore();
1432
- const meta = store.deviceMeta ?? {};
1433
- const registry2 = store.locations ?? [];
1434
- const seen = /* @__PURE__ */ new Map();
1435
- const consider = (raw) => {
1436
- if (typeof raw !== "string") return;
1437
- const trimmed = raw.trim();
1438
- if (trimmed.length === 0) return;
1439
- const key = trimmed.toLowerCase();
1440
- if (!seen.has(key)) seen.set(key, trimmed);
1441
- };
1442
- for (const label of registry2) consider(label);
1443
- for (const m of Object.values(meta)) consider(m.location);
1444
- return [...seen.values()].sort(
1445
- (a, b) => a.localeCompare(b, void 0, { sensitivity: "base" })
1446
- );
1447
- },
1448
- /**
1449
- * Add a label to the curated location registry. Idempotent:
1450
- * existing entries (case-insensitive match) are silently kept.
1451
- * Empty / whitespace-only inputs throw — operators must supply a
1452
- * meaningful label.
1453
- */
1454
- addLocation: async (input) => {
1455
- const trimmed = input.name.trim();
1456
- if (trimmed.length === 0) {
1457
- throw new Error("[device-manager] addLocation: name must be non-empty");
1458
- }
1459
- const store = await settings.readAddonStore();
1460
- const current = store.locations ?? [];
1461
- if (current.some((l) => l.toLowerCase() === trimmed.toLowerCase())) {
1462
- return;
1463
- }
1464
- await settings.writeAddonStore({ locations: [...current, trimmed] });
1465
- },
1466
- /**
1467
- * Remove a label from the curated registry. Match is
1468
- * case-insensitive. Devices that still reference this label keep
1469
- * their `meta.location` value (the registry is a suggestion
1470
- * list, not a foreign key) — pass `cascade: true` to also clear
1471
- * `setLocation` on every device that referenced this exact
1472
- * label. Cascade only matches case-insensitively + trimmed, same
1473
- * as the registry equality check.
1474
- */
1475
- removeLocation: async (input) => {
1476
- const trimmed = input.name.trim();
1477
- if (trimmed.length === 0) return;
1478
- const store = await settings.readAddonStore();
1479
- const current = store.locations ?? [];
1480
- const remaining = current.filter((l) => l.toLowerCase() !== trimmed.toLowerCase());
1481
- if (remaining.length !== current.length) {
1482
- await settings.writeAddonStore({ locations: remaining });
1483
- }
1484
- if (input.cascade !== true) return;
1485
- const meta = store.deviceMeta ?? {};
1486
- const updates = { ...meta };
1487
- const cleared = [];
1488
- for (const [key, m] of Object.entries(meta)) {
1489
- if (typeof m.location !== "string") continue;
1490
- if (m.location.trim().toLowerCase() !== trimmed.toLowerCase()) continue;
1491
- updates[key] = { ...m, location: null };
1492
- cleared.push(m.id);
1493
- }
1494
- if (cleared.length === 0) return;
1495
- await settings.writeAddonStore({ deviceMeta: updates });
1496
- for (const deviceId of cleared) {
1497
- this.ctx.eventBus.emit({
1498
- id: (0, import_node_crypto.randomUUID)(),
1499
- timestamp: /* @__PURE__ */ new Date(),
1500
- source: { type: "device", id: deviceId },
1501
- category: import_types.EventCategory.DeviceMetaChanged,
1502
- data: { deviceId, field: "location", value: null }
1503
- });
1504
- }
1505
- },
1506
- listPersistedByAddon: async (input) => {
1507
- const { addonId } = input;
1508
- const [index, meta] = await Promise.all([readIndex(), readMeta()]);
1509
- const stableIds = index[addonId] ?? [];
1510
- return stableIds.map((stableId) => {
1511
- const m = meta[deviceKey(addonId, stableId)];
1512
- return {
1513
- id: m.id,
1514
- stableId,
1515
- type: m.type,
1516
- name: m.name,
1517
- location: m.location ?? null,
1518
- disabled: m.disabled ?? false,
1519
- parentDeviceId: m.parentDeviceId
1520
- };
1521
- });
1522
- },
1523
- // ── Live queries — serve from live registry when available ────────────
1524
- listAll: async (input) => {
1525
- const { addonId } = input;
1526
- const results = [];
1527
- const seen = /* @__PURE__ */ new Set();
1528
- const meta = await readMeta();
1529
- const metadataMap = await readMetadataMap();
1530
- if (registry) {
1531
- const liveEntries = addonId ? registry.getAllForAddon(addonId).map((device) => ({ addonId, device })) : registry.getAllWithAddonId();
1532
- for (const { addonId: aid, device } of liveEntries) {
1533
- const key = deviceKey(aid, device.stableId);
1534
- const metadata = metadataMap[key] ?? null;
1535
- const metaRow = meta[key] ?? null;
1536
- results.push(toDeviceInfo(aid, device, metadata, metaRow));
1537
- seen.add(key);
1538
- }
1539
- }
1540
- const index = await readIndex();
1541
- const targetAddons = addonId ? [addonId] : Object.keys(index);
1542
- for (const aid of targetAddons) {
1543
- for (const stableId of index[aid] ?? []) {
1544
- const key = deviceKey(aid, stableId);
1545
- if (seen.has(key)) continue;
1546
- const m = meta[key];
1547
- const persistedType = m.type;
1548
- const persistedConfig = await settings.readDeviceStore(m.id);
1549
- const metadata = metadataMap[key] ?? null;
1550
- results.push({
1551
- id: m.id,
1552
- stableId,
1553
- addonId: aid,
1554
- type: persistedType,
1555
- name: m?.name ?? stableId,
1556
- location: m?.location ?? null,
1557
- disabled: m?.disabled ?? false,
1558
- parentDeviceId: m?.parentDeviceId ?? null,
1559
- // role is not persisted to disk — only set on the live
1560
- // IDevice (`BaseDevice.role`) by accessory subclasses. The
1561
- // forked-worker fallback path can't read it, so emit null;
1562
- // accessories typically run in the same process as their
1563
- // parent provider, so the live path covers them in practice.
1564
- role: null,
1565
- // registry===null was the legacy "offline view" signal; keep it
1566
- // for that case so existing callers see online=false. When a
1567
- // registry is present and the device lives elsewhere (forked
1568
- // worker), default to online=true — persisted state implies it
1569
- // was successfully registered by its owning process.
1570
- online: registry !== null,
1571
- features: persistedFeatures(m?.features),
1572
- // Live IDevice isn't reachable from the hub, so infer
1573
- // isCamera from the persisted DeviceType rather than the
1574
- // method-presence check `toDeviceInfo` uses. Without this
1575
- // forked-worker cameras looked non-camera to the stream-broker
1576
- // and never got their RTSP restream wired up.
1577
- isCamera: persistedType === import_types.DeviceType.Camera,
1578
- config: persistedConfig ?? {},
1579
- metadata
1580
- });
1581
- }
1582
- }
1583
- return results;
1584
- },
1585
- getDevice: async (input) => {
1586
- const { deviceId } = input;
1587
- if (registry) {
1588
- const found = resolveDeviceById(registry, deviceId);
1589
- if (found) {
1590
- const key2 = deviceKey(found.addonId, found.device.stableId);
1591
- const [map2, metaMap] = await Promise.all([readMetadataMap(), readMeta()]);
1592
- const metadata2 = map2[key2] ?? null;
1593
- const metaRow = metaMap[key2] ?? null;
1594
- return toDeviceInfo(found.addonId, found.device, metadata2, metaRow);
1595
- }
1596
- }
1597
- const persisted = await resolvePersistedById(deviceId);
1598
- if (!persisted) return null;
1599
- const { addonId: aid, stableId, meta: m } = persisted;
1600
- const persistedConfig = await settings.readDeviceStore(m.id);
1601
- const key = deviceKey(aid, stableId);
1602
- const map = await readMetadataMap();
1603
- const metadata = map[key] ?? null;
1604
- return {
1605
- id: deviceId,
1606
- stableId,
1607
- addonId: aid,
1608
- type: m.type,
1609
- name: m.name,
1610
- location: m.location ?? null,
1611
- disabled: m.disabled ?? false,
1612
- parentDeviceId: m.parentDeviceId,
1613
- role: null,
1614
- online: true,
1615
- features: persistedFeatures(m.features),
1616
- isCamera: false,
1617
- config: persistedConfig ?? {},
1618
- metadata
1619
- };
1620
- },
1621
- getChildren: async (input) => {
1622
- const { parentDeviceId } = input;
1623
- let ownerAddonId = null;
1624
- if (registry) {
1625
- const liveParent = registry.getById(parentDeviceId);
1626
- if (liveParent) {
1627
- ownerAddonId = registry.getAddonId(parentDeviceId);
1628
- }
1629
- }
1630
- if (!ownerAddonId) {
1631
- const persisted = await resolvePersistedById(parentDeviceId);
1632
- if (!persisted) return [];
1633
- ownerAddonId = persisted.addonId;
1634
- }
1635
- const results = [];
1636
- const seen = /* @__PURE__ */ new Set();
1637
- const [index, meta, metadataMap] = await Promise.all([readIndex(), readMeta(), readMetadataMap()]);
1638
- if (registry) {
1639
- const liveChildren = registry.getChildren(parentDeviceId);
1640
- for (const device of liveChildren) {
1641
- const key = deviceKey(ownerAddonId, device.stableId);
1642
- const metadata = metadataMap[key] ?? null;
1643
- const metaRow = meta[key] ?? null;
1644
- results.push(toDeviceInfo(ownerAddonId, device, metadata, metaRow));
1645
- seen.add(key);
1646
- }
1647
- }
1648
- const stableIds = index[ownerAddonId] ?? [];
1649
- const persistedChildren = stableIds.filter(
1650
- (sid) => meta[deviceKey(ownerAddonId, sid)]?.parentDeviceId === parentDeviceId
1651
- );
1652
- for (const childStableId of persistedChildren) {
1653
- const key = deviceKey(ownerAddonId, childStableId);
1654
- if (seen.has(key)) continue;
1655
- const m = meta[key];
1656
- const persistedConfig = await settings.readDeviceStore(m.id);
1657
- const metadata = metadataMap[key] ?? null;
1658
- results.push({
1659
- id: m.id,
1660
- stableId: childStableId,
1661
- addonId: ownerAddonId,
1662
- type: m.type,
1663
- name: m.name,
1664
- location: m.location ?? null,
1665
- disabled: m.disabled ?? false,
1666
- parentDeviceId,
1667
- role: null,
1668
- online: registry !== null,
1669
- features: persistedFeatures(m.features),
1670
- isCamera: false,
1671
- config: persistedConfig ?? {},
1672
- metadata
1673
- });
1674
- }
1675
- return results;
1676
- },
1677
- // ── Live runtime methods ──────────────────────────────────────────────
1678
- getStreamSources: async (input) => {
1679
- const { deviceId } = input;
1680
- if (registry) {
1681
- const found = resolveDeviceById(registry, deviceId);
1682
- if (found) {
1683
- if (!isCameraDevice(found.device)) return [];
1684
- const sources2 = await found.device.getStreamSources();
1685
- return sources2.map((s) => ({
1686
- id: s.id,
1687
- label: s.label,
1688
- protocol: s.protocol,
1689
- url: s.url,
1690
- resolution: s.resolution,
1691
- fps: s.fps,
1692
- bitrate: s.bitrate,
1693
- codec: s.codec,
1694
- profileHint: s.profileHint
1695
- }));
1696
- }
1697
- }
1698
- const persisted = await resolvePersistedById(deviceId);
1699
- if (!persisted) throw new Error(`[device-manager] Device with id ${deviceId} not found`);
1700
- const ops = requireDeviceOps(deviceId);
1701
- const sources = await ops.getStreamSources({ deviceId });
1702
- return sources.map((s) => ({ ...s }));
1703
- },
1704
- getConfigSchema: async (input) => {
1705
- const { deviceId } = input;
1706
- if (registry) {
1707
- const found = resolveDeviceById(registry, deviceId);
1708
- if (found) {
1709
- return found.device.config.entries().map((entry) => ({
1710
- key: entry.key,
1711
- value: entry.value,
1712
- ...entry.description !== void 0 ? { description: entry.description } : {}
1713
- }));
1714
- }
1715
- }
1716
- const persisted = await resolvePersistedById(deviceId);
1717
- if (!persisted) throw new Error(`[device-manager] Device with id ${deviceId} not found`);
1718
- const ops = requireDeviceOps(deviceId);
1719
- const entries = await ops.getConfigEntries({ deviceId });
1720
- return entries.map((e) => ({ ...e }));
1721
- },
1722
- getSettingsSchema: async (input) => {
1723
- const { deviceId } = input;
1724
- if (registry) {
1725
- const found = resolveDeviceById(registry, deviceId);
1726
- if (found) {
1727
- return found.device.getSettingsUISchema();
1728
- }
1729
- }
1730
- const persisted = await resolvePersistedById(deviceId);
1731
- if (!persisted) return null;
1732
- const ops = requireDeviceOps(deviceId);
1733
- const schema = await ops.getSettingsSchema({ deviceId });
1734
- return schema ?? null;
1735
- },
1736
- updateConfig: async (input) => {
1737
- const { deviceId } = input;
1738
- if (registry) {
1739
- const found = resolveDeviceById(registry, deviceId);
1740
- if (found) {
1741
- await found.device.config.setAll(input.values);
1742
- return { success: true };
1743
- }
1744
- }
1745
- const persisted = await resolvePersistedById(deviceId);
1746
- if (!persisted) throw new Error(`[device-manager] Device with id ${deviceId} not found`);
1747
- const ops = requireDeviceOps(deviceId);
1748
- await ops.setConfig({ deviceId, values: input.values });
1749
- return { success: true };
1750
- },
1751
- // `enable` / `disable` write to deviceMeta.disabled (the canonical
1752
- // soft-disable flag that drivers consult via `BaseDevice.disabled`
1753
- // at lifecycle entrypoints, and that `toDeviceInfo` projects onto
1754
- // the wire by reading the LATEST persisted meta row — see the
1755
- // `metaRow` argument plumbed through `listAll` / `getDevice` /
1756
- // `getChildren`).
1757
- //
1758
- // Earlier these wrappers wrote `enabled: true/false` into the
1759
- // device CONFIG blob wrong store entirely. The config write
1760
- // succeeded but the flag the rest of the system reads
1761
- // (deviceMeta) stayed unchanged, so the UI's enable/disable
1762
- // toggle appeared to do nothing. Delegating to `setDisabled`
1763
- // routes through the correct withMetaWriteLock path and emits
1764
- // the `DeviceMetaChanged` event for live invalidation.
1765
- //
1766
- // NB: `BaseDevice.disabled` is `readonly` and bound at
1767
- // construction (base-device.ts:142). Drivers that gate behaviour
1768
- // off `this.disabled` only see the value the device was
1769
- // instantiated with flipping disabled at runtime updates the
1770
- // wire response immediately, but lifecycle gating inside the
1771
- // driver only takes effect on the next instantiation. That's
1772
- // acceptable today: the operator-facing semantic is "hide /
1773
- // mark inactive in the UI"; the heavyweight teardown semantic
1774
- // is `remove`.
1775
- enable: async (input) => {
1776
- await provider.setDisabled({ deviceId: input.deviceId, disabled: false });
1777
- return { success: true };
1778
- },
1779
- disable: async (input) => {
1780
- await provider.setDisabled({ deviceId: input.deviceId, disabled: true });
1781
- return { success: true };
1782
- },
1783
- remove: async (input) => {
1784
- const { deviceId } = input;
1785
- if (registry) {
1786
- const live = resolveDeviceById(registry, deviceId);
1787
- if (live) {
1788
- const deviceName = live.device.name;
1789
- await live.device.removeDevice();
1790
- registry.remove(deviceId);
1791
- await provider.removeDevice({ deviceId });
1792
- this.ctx.logger.info("removed hub-local device", { tags: { deviceId, deviceName } });
1793
- return { success: true };
1794
- }
1795
- }
1796
- const persisted = await resolvePersistedById(deviceId);
1797
- if (!persisted) throw new Error(`[device-manager] Device with id ${deviceId} not found`);
1798
- const { meta: persistedMeta } = persisted;
1799
- try {
1800
- const ops = requireDeviceOps(deviceId);
1801
- await ops.removeDevice({ deviceId });
1802
- } catch (err) {
1803
- this.ctx.logger.warn("remove via device-ops failed \u2014 clearing persistence anyway", { tags: { deviceId, deviceName: persistedMeta.name }, meta: { error: (0, import_types.errMsg)(err) } });
1804
- }
1805
- await provider.removeDevice({ deviceId });
1806
- return { success: true };
1807
- },
1808
- getStreamProfileMap: async (input) => {
1809
- if (!registry) return {};
1810
- const found = resolveDeviceById(registry, input.deviceId);
1811
- if (!found) return {};
1812
- const storedMap = found.device.config.entries().find((e) => e.key === "_profileMap")?.value;
1813
- if (storedMap !== void 0 && typeof storedMap === "object" && storedMap !== null) {
1814
- return storedMap;
1815
- }
1816
- if (!isCameraDevice(found.device)) return {};
1817
- const sources = await found.device.getStreamSources();
1818
- const profileMap = {};
1819
- for (const s of sources) {
1820
- if (s.profileHint && s.id) {
1821
- profileMap[s.profileHint] = s.id;
1822
- }
1823
- }
1824
- return profileMap;
1825
- },
1826
- setStreamProfileMap: async (input) => {
1827
- const { deviceId } = input;
1828
- if (registry) {
1829
- const found = resolveDeviceById(registry, deviceId);
1830
- if (found) {
1831
- await found.device.config.setAll({ _profileMap: input.profileMap });
1832
- return { success: true };
1833
- }
1834
- }
1835
- const persisted = await resolvePersistedById(deviceId);
1836
- if (!persisted) throw new Error(`[device-manager] Device with id ${deviceId} not found`);
1837
- const ops = requireDeviceOps(deviceId);
1838
- await ops.setConfig({ deviceId, values: { _profileMap: input.profileMap } });
1839
- return { success: true };
1840
- },
1841
- probeStreams: async (input) => {
1842
- const streamProbe = this.ctx.kernel.streamProbe;
1843
- if (!streamProbe) return [];
1844
- const sources = await provider.getStreamSources({ deviceId: input.deviceId });
1845
- const results = [];
1846
- for (const s of sources) {
1847
- if (!s.url) continue;
1848
- try {
1849
- const metadata = await streamProbe.probe(s.url, { force: true });
1850
- results.push({
1851
- streamId: s.id,
1852
- width: metadata.width,
1853
- height: metadata.height,
1854
- codec: metadata.codec,
1855
- fps: metadata.fps,
1856
- bitrateKbps: metadata.bitrateKbps
1857
- });
1858
- } catch (err) {
1859
- this.ctx.logger.debug("streamProbe.probe failed \u2014 returning placeholder", {
1860
- meta: {
1861
- deviceId: input.deviceId,
1862
- streamId: s.id,
1863
- error: err instanceof Error ? err.message : String(err)
1864
- }
1865
- });
1866
- results.push({ streamId: s.id });
1867
- }
1868
- }
1869
- return results;
1870
- },
1871
- // ── Device-provider operations (routed via CapabilityRegistry) ───────
1872
- discoverDevices: async (input) => {
1873
- const dp = await this.requireDeviceProvider(input.addonId);
1874
- const supported = await dp.supportsDiscovery({});
1875
- if (!supported) {
1876
- throw new Error(`Addon "${input.addonId}" does not support device discovery`);
1877
- }
1878
- const discovered = await dp.discoverDevices({});
1879
- return discovered.map((d) => ({
1880
- stableId: d.stableId,
1881
- type: d.type,
1882
- suggestedName: d.suggestedName,
1883
- prefilledConfig: d.prefilledConfig
1884
- }));
1885
- },
1886
- adoptDevice: async (input) => {
1887
- const dp = await this.requireDeviceProvider(input.addonId);
1888
- const supported = await dp.supportsDiscovery({});
1889
- if (!supported) {
1890
- throw new Error(`Addon "${input.addonId}" does not support device adoption`);
1891
- }
1892
- return dp.adoptDiscoveredDevice({
1893
- candidate: input.candidate
1894
- });
1895
- },
1896
- getCreationSchema: async (input) => {
1897
- const dp = await this.requireDeviceProvider(input.addonId);
1898
- const supported = await dp.supportsManualCreation({});
1899
- if (!supported) return null;
1900
- const schema = await dp.getChildCreationSchema({ type: input.type });
1901
- return schema ?? null;
1902
- },
1903
- createDevice: async (input) => {
1904
- const dp = await this.requireDeviceProvider(input.addonId);
1905
- const supported = await dp.supportsManualCreation({});
1906
- if (!supported) {
1907
- throw new Error(`Addon "${input.addonId}" does not support manual device creation`);
1908
- }
1909
- return dp.createDevice({ type: input.type, config: input.config });
1910
- },
1911
- testCreationField: async (input) => {
1912
- const dp = await this.requireDeviceProvider(input.addonId);
1913
- return dp.testCreationField({
1914
- type: input.type,
1915
- key: input.key,
1916
- value: input.value,
1917
- ...input.formValues !== void 0 ? { formValues: input.formValues } : {}
1918
- });
1919
- },
1920
- testField: async (input) => {
1921
- const { deviceId } = input;
1922
- let owningAddonId = null;
1923
- if (registry) {
1924
- owningAddonId = registry.getAddonId(deviceId);
1925
- }
1926
- if (!owningAddonId) {
1927
- const persisted = await resolvePersistedById(deviceId);
1928
- owningAddonId = persisted?.addonId ?? null;
1929
- }
1930
- if (!owningAddonId) {
1931
- throw new Error(`Device with id ${deviceId} not found`);
1932
- }
1933
- const dp = await this.waitDeviceProvider(owningAddonId);
1934
- if (!dp) {
1935
- return { status: "ok", labels: [], error: void 0 };
1936
- }
1937
- if (typeof dp.testCreationField !== "function") {
1938
- return { status: "ok", labels: [], error: void 0 };
1939
- }
1940
- return dp.testCreationField({ type: import_types.DeviceType.Camera, key: input.key, value: input.value });
1941
- },
1942
- // ── Bindings — per-device capability provider map ────────────────────
1943
- getBindings: async (input) => {
1944
- const result = await this.getBindings({ deviceId: input.deviceId });
1945
- return { deviceId: input.deviceId, entries: result.entries };
1946
- },
1947
- getAllBindings: async () => {
1948
- return this.getAllBindings();
1949
- },
1950
- setWrapperActive: async (input) => {
1951
- return this.setWrapperActive({
1952
- deviceId: input.deviceId,
1953
- capName: input.capName,
1954
- wrapperAddonId: input.wrapperAddonId,
1955
- active: input.active
1956
- });
1957
- },
1958
- listWrappersForCap: async (input) => this.listWrappersForCap(input),
1959
- listBindableCapsForDeviceType: async (input) => this.listBindableCapsForDeviceType(input),
1960
- // ── Device-details aggregators ──────────────────────────────────────
1961
- getDeviceSettingsAggregate: async (input) => {
1962
- return this.getDeviceAggregate(input.deviceId, "settings");
1963
- },
1964
- getDeviceLiveInfoAggregate: async (input) => {
1965
- return this.getDeviceAggregate(input.deviceId, "live");
1966
- },
1967
- getDeviceAggregate: async (input) => {
1968
- const [settings2, live] = await Promise.all([
1969
- this.getDeviceAggregate(input.deviceId, "settings"),
1970
- this.getDeviceAggregate(input.deviceId, "live")
1971
- ]);
1972
- return { settings: settings2, live };
1973
- },
1974
- updateDeviceField: async (input) => {
1975
- return this.updateDeviceField({
1976
- deviceId: input.deviceId,
1977
- writerCapName: input.writerCapName,
1978
- writerAddonId: input.writerAddonId,
1979
- key: input.key,
1980
- value: input.value
1981
- });
1982
- },
1983
- updateDeviceFieldsBatch: async (input) => {
1984
- return this.updateDeviceFieldsBatch({ deviceId: input.deviceId, changes: input.changes });
1985
- },
1986
- getDeviceStatusAggregate: async (input) => this.getDeviceStatusAggregate(input)
1987
- };
1988
- this.ctx.logger.info("registered device-manager capability", { meta: { liveRegistry: registry !== null } });
1989
- if (registry) {
1990
- this.propagator = new DeviceEventPropagator({
1991
- eventBus: this.ctx.eventBus,
1992
- getParentOf: (id) => registry.getById(id)?.parentDeviceId ?? null,
1993
- logger: {
1994
- warn: (msg, meta) => this.ctx.logger.warn(msg, meta ?? {}),
1995
- debug: (msg, meta) => this.ctx.logger.debug(msg, meta ?? {})
1996
- }
1997
- });
1998
- this.propagator.start();
1999
- this.ctx.logger.info("device-event-propagator started");
2000
- }
2001
- const deviceStateProvider = {
2002
- getSnapshot: async (input) => {
2003
- return this.snapshotForDevice(input.deviceId);
2004
- },
2005
- getCapSlice: async (input) => {
2006
- const perCap = this.stateMirror.get(input.deviceId);
2007
- const slice = perCap?.get(input.capName);
2008
- return slice ? { ...slice } : null;
2009
- },
2010
- // Whole-system warm-boot dump. Iterates the mirror once and
2011
- // returns a deeply-cloned `{[deviceId]: {[capName]: slice}}`
2012
- // payload so callers (SystemManager, dashboards) seed their
2013
- // local mirror with one round-trip instead of N per-device
2014
- // calls. Empty inner record for devices with no slices yet.
2015
- getAllSnapshots: async () => {
2016
- const out = {};
2017
- for (const [deviceId, perCap] of this.stateMirror) {
2018
- const dev = {};
2019
- for (const [capName, slice] of perCap) dev[capName] = { ...slice };
2020
- out[String(deviceId)] = dev;
2021
- }
2022
- return out;
2023
- },
2024
- // Canonical cross-layer write — the single entrypoint every
2025
- // caller funnels through. In-process providers reach it via
2026
- // `BaseDevice.runtimeState.setCapState` (which delegates here);
2027
- // system-scope addons (e.g. pipeline runner writing the motion
2028
- // slice on phase transitions) call this cap method directly
2029
- // through `ctx.api.deviceState.setCapSlice.mutate(...)`.
2030
- setCapSlice: async (input) => {
2031
- const { deviceId, capName, slice } = input;
2032
- const persisted = await resolvePersistedById(deviceId);
2033
- if (!persisted) throw new Error(`[device-manager] setCapSlice: unknown device id=${deviceId}`);
2034
- this.applySingleCapUpdate(deviceId, capName, slice);
2035
- this.scheduleRuntimeStateDiskWrite(deviceId, settings);
2036
- }
2037
- };
2038
- return [
2039
- { capability: import_types4.deviceManagerCapability, provider },
2040
- { capability: import_types4.deviceStateCapability, provider: deviceStateProvider }
2041
- ];
2042
- }
2043
- /**
2044
- * Single-cap mirror update — diff against the current mirror,
2045
- * persist the new slice in-memory, emit `DeviceStateChanged` for
2046
- * this cap. No-op on identical writes (both same shape and same
2047
- * values). Called by `setCapSlice` provider.
2048
- */
2049
- applySingleCapUpdate(deviceId, capName, slice) {
2050
- let perCap = this.stateMirror.get(deviceId);
2051
- if (!perCap) {
2052
- perCap = /* @__PURE__ */ new Map();
2053
- this.stateMirror.set(deviceId, perCap);
2054
- }
2055
- const prior = perCap.get(capName);
2056
- if (prior && shallowEqual(prior, slice)) return;
2057
- perCap.set(capName, { ...slice });
2058
- this.emitStateChanged(deviceId, capName, slice);
2059
- }
2060
- /**
2061
- * Debounced disk writer. Coalesces frequent writes (motion phase
2062
- * transitions, battery pushes) into one `writeDeviceRuntimeState`
2063
- * per `RUNTIME_STATE_DEBOUNCE_MS` window. Reads the per-device
2064
- * blob from the live mirror at flush time so the disk picture is
2065
- * always the latest state — no risk of writing a stale snapshot.
2066
- */
2067
- scheduleRuntimeStateDiskWrite(deviceId, settings) {
2068
- let slot = this.runtimeStateDebounce.get(deviceId);
2069
- if (!slot) {
2070
- slot = { timer: null, inFlight: null };
2071
- this.runtimeStateDebounce.set(deviceId, slot);
2072
- }
2073
- if (slot.timer) return;
2074
- slot.timer = setTimeout(() => {
2075
- slot.timer = null;
2076
- const blob = this.snapshotForDevice(deviceId);
2077
- const write = (async () => {
2078
- try {
2079
- await settings.writeDeviceRuntimeState(deviceId, blob);
2080
- } catch (err) {
2081
- this.ctx.logger.warn("writeDeviceRuntimeState failed", {
2082
- tags: { deviceId },
2083
- meta: { error: err instanceof Error ? err.message : String(err) }
2084
- });
2085
- } finally {
2086
- slot.inFlight = null;
2087
- }
2088
- })();
2089
- slot.inFlight = write;
2090
- }, _DeviceManagerAddon.RUNTIME_STATE_DEBOUNCE_MS);
2091
- }
2092
- /**
2093
- * One-shot mirror seed used by `loadRuntimeState` at boot so the
2094
- * hub knows about every persisted slice without waiting for the
2095
- * first `setCapSlice` call. No events emitted — this is
2096
- * initial-state population, not a transition.
2097
- */
2098
- seedMirror(deviceId, blob) {
2099
- let perCap = this.stateMirror.get(deviceId);
2100
- if (!perCap) {
2101
- perCap = /* @__PURE__ */ new Map();
2102
- this.stateMirror.set(deviceId, perCap);
2103
- }
2104
- for (const [capName, raw] of Object.entries(blob)) {
2105
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
2106
- perCap.set(capName, { ...raw });
2107
- }
2108
- }
2109
- snapshotForDevice(deviceId) {
2110
- const perCap = this.stateMirror.get(deviceId);
2111
- if (!perCap) return {};
2112
- const out = {};
2113
- for (const [k, v] of perCap) out[k] = { ...v };
2114
- return out;
2115
- }
2116
- emitStateChanged(deviceId, capName, slice) {
2117
- this.ctx.eventBus.emit({
2118
- id: (0, import_node_crypto.randomUUID)(),
2119
- timestamp: /* @__PURE__ */ new Date(),
2120
- source: { type: "device", id: deviceId },
2121
- category: import_types.EventCategory.DeviceStateChanged,
2122
- data: { deviceId, capName, slice }
2123
- });
2124
- }
2125
- async onShutdown() {
2126
- this.propagator?.stop();
2127
- this.propagator = null;
2128
- const settings = this.ctx.settings;
2129
- const pending = [];
2130
- for (const [deviceId, slot] of this.runtimeStateDebounce) {
2131
- if (slot.timer) {
2132
- clearTimeout(slot.timer);
2133
- slot.timer = null;
2134
- if (settings) {
2135
- const blob = this.snapshotForDevice(deviceId);
2136
- pending.push(settings.writeDeviceRuntimeState(deviceId, blob).catch((err) => {
2137
- this.ctx.logger.warn("shutdown writeDeviceRuntimeState failed", {
2138
- tags: { deviceId },
2139
- meta: { error: err instanceof Error ? err.message : String(err) }
2140
- });
2141
- }));
2142
- }
2143
- }
2144
- if (slot.inFlight) pending.push(slot.inFlight);
2145
- }
2146
- await Promise.all(pending);
2147
- this.runtimeStateDebounce.clear();
2148
- }
321
+ var DeviceManagerAddon = class DeviceManagerAddon extends _camstack_types.BaseAddon {
322
+ constructor() {
323
+ super({});
324
+ }
325
+ /** Shorthand for the kernel-injected capability registry. */
326
+ get capabilityRegistry() {
327
+ return this.ctx.kernel.capabilityRegistry;
328
+ }
329
+ /**
330
+ * Parent-chain event propagator. Started in `onInitialize` once the
331
+ * hub's `deviceRegistry` is available; listens to every device-sourced
332
+ * event and re-emits a copy on each ancestor scope with `via[]`
333
+ * populated. Stopped in `onShutdown`.
334
+ */
335
+ propagator = null;
336
+ /**
337
+ * Hub-side mirror of every device's cap-keyed runtime state.
338
+ * Populated whenever any caller writes via `deviceState.setCapSlice`
339
+ * (the canonical cross-layer write entrypoint) and on first load
340
+ * via `loadRuntimeState`. Cross-process consumers reach the mirror
341
+ * through the `deviceState` cap router; per-cap event subscribers
342
+ * (e.g. `battery.onStatusChanged`) get the same data via
343
+ * cap-specific events still emitted by the owning device.
344
+ *
345
+ * Key: deviceId. Value: per-cap slice map. Empty by default —
346
+ * slices show up as `setCapSlice` calls trickle in.
347
+ */
348
+ stateMirror = /* @__PURE__ */ new Map();
349
+ /**
350
+ * Per-device disk-write debouncer for runtime-state. `setCapSlice`
351
+ * updates the in-memory mirror synchronously and emits the change
352
+ * event immediately, but the disk write is coalesced — frequent
353
+ * back-to-back writes (motion phase transitions, battery pushes,
354
+ * etc.) collapse to one `writeDeviceRuntimeState` per
355
+ * `RUNTIME_STATE_DEBOUNCE_MS` window. `flushRuntimeStateWrites`
356
+ * awaits any in-flight write + scheduled flush so shutdown is
357
+ * lossless.
358
+ */
359
+ runtimeStateDebounce = /* @__PURE__ */ new Map();
360
+ static RUNTIME_STATE_DEBOUNCE_MS = 1e3;
361
+ /**
362
+ * Cross-process native-provider cache: deviceId (numeric) → capName → { addonId, nodeId }.
363
+ * Kept in sync with `device.bindings-changed` events emitted by forked
364
+ * workers. Union'd into `getBindings` so hub-side consumers see every
365
+ * native cap regardless of which process owns the IDevice. No persistence
366
+ * — entries re-register when the worker restarts. Purged on
367
+ * `$node.disconnected` to avoid stale routing after a worker crash.
368
+ */
369
+ remoteNativeCaps = /* @__PURE__ */ new Map();
370
+ /** Wait for a device-provider by addonId, returning null on timeout. */
371
+ async waitDeviceProvider(addonId, timeoutMs = 5e3) {
372
+ const provider = await this.capabilityRegistry?.waitForProvider("device-provider", addonId, timeoutMs);
373
+ return provider ? provider : null;
374
+ }
375
+ /** Require a device-provider by addonId — throws if not found. */
376
+ async requireDeviceProvider(addonId) {
377
+ const dp = await this.waitDeviceProvider(addonId);
378
+ if (!dp) throw new Error(`Device provider "${addonId}" not found or not registered`);
379
+ return dp;
380
+ }
381
+ async readBindingsStore() {
382
+ return { deviceBindings: (await this.ctx.settings.readAddonStore()).deviceBindings ?? {} };
383
+ }
384
+ async writeBindingsStore(next) {
385
+ await this.ctx.settings.writeAddonStore({ deviceBindings: next.deviceBindings });
386
+ }
387
+ resolveWrapperNodeId(_wrapperAddonId) {
388
+ return "hub";
389
+ }
390
+ /**
391
+ * Active discovery of native caps registered on a worker node.
392
+ * Called from the `$node.connected` handler to recover from lost
393
+ * `DeviceBindingsChanged` broadcasts after worker restart.
394
+ *
395
+ * Iterates the broker's service registry, picks
396
+ * `<addonId>.native-provider.<capName>` services owned by the
397
+ * connected node, calls each service's `$listDeviceIds` action to
398
+ * fetch the deviceIds the worker has registered that cap on, then
399
+ * writes entries into `remoteNativeCaps`.
400
+ *
401
+ * Best-effort: per-service failures are logged and swallowed so a
402
+ * single bad service doesn't abort the whole rebuild.
403
+ */
404
+ async discoverWorkerNativeCaps(connectedNodeId, _connectedAddonId) {
405
+ const cluster = this.ctx.kernel.cluster;
406
+ if (!cluster) return;
407
+ const broker = cluster.broker;
408
+ const services = broker.registry?.getServiceList?.({
409
+ onlyAvailable: true,
410
+ withActions: false
411
+ }) ?? [];
412
+ const NATIVE_INFIX = ".native-provider.";
413
+ const matched = services.filter((s) => s.nodeID === connectedNodeId && s.name.includes(NATIVE_INFIX));
414
+ if (matched.length === 0) return;
415
+ for (const svc of matched) {
416
+ const idx = svc.name.indexOf(NATIVE_INFIX);
417
+ if (idx <= 0) continue;
418
+ const addonId = svc.name.slice(0, idx);
419
+ const capName = svc.name.slice(idx + 17);
420
+ if (!addonId || !capName) continue;
421
+ try {
422
+ const action = `${svc.name}.$listDeviceIds`;
423
+ const deviceIds = await broker.call?.(action, {}, { nodeID: connectedNodeId });
424
+ if (!deviceIds || deviceIds.length === 0) continue;
425
+ for (const deviceId of deviceIds) {
426
+ if (this.capabilityRegistry?.getNativeAddonId(capName, deviceId)) continue;
427
+ let perDevice = this.remoteNativeCaps.get(deviceId);
428
+ if (!perDevice) {
429
+ perDevice = /* @__PURE__ */ new Map();
430
+ this.remoteNativeCaps.set(deviceId, perDevice);
431
+ }
432
+ perDevice.set(capName, {
433
+ addonId,
434
+ nodeId: connectedNodeId
435
+ });
436
+ }
437
+ this.ctx.logger.debug("worker native-cap discovered", { meta: {
438
+ nodeId: connectedNodeId,
439
+ addonId,
440
+ capName,
441
+ deviceIds
442
+ } });
443
+ } catch (err) {
444
+ this.ctx.logger.debug("worker native-cap $listDeviceIds failed", { meta: {
445
+ service: svc.name,
446
+ nodeId: connectedNodeId,
447
+ error: (0, _camstack_types.errMsg)(err)
448
+ } });
449
+ }
450
+ }
451
+ this.ctx.logger.info("worker native-cap discovery completed", { meta: {
452
+ nodeId: connectedNodeId,
453
+ services: matched.length
454
+ } });
455
+ }
456
+ async getBindings(input) {
457
+ const storeKey = String(input.deviceId);
458
+ const perDevice = (await this.readBindingsStore()).deviceBindings[storeKey] ?? {};
459
+ const entries = [];
460
+ const seenCaps = /* @__PURE__ */ new Set();
461
+ for (const [capName, { wrapperAddonId }] of Object.entries(perDevice)) {
462
+ const hubLocalNative = this.capabilityRegistry?.getNativeAddonId(capName, input.deviceId) ?? null;
463
+ const remoteNative = this.remoteNativeCaps.get(input.deviceId)?.get(capName) ?? null;
464
+ const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
465
+ const nativeNodeId = hubLocalNative ? this.ctx.kernel.localNodeId ?? "hub" : remoteNative?.nodeId ?? this.ctx.kernel.localNodeId ?? "hub";
466
+ if (wrapperAddonId === null && !nativeAddonId) {
467
+ seenCaps.add(capName);
468
+ continue;
469
+ }
470
+ entries.push({
471
+ capName,
472
+ kind: wrapperAddonId ? "wrapped" : "native",
473
+ providerAddonId: wrapperAddonId ?? nativeAddonId,
474
+ providerNodeId: wrapperAddonId ? this.resolveWrapperNodeId(wrapperAddonId) : nativeNodeId,
475
+ nativeAddonId
476
+ });
477
+ seenCaps.add(capName);
478
+ }
479
+ const remote = this.remoteNativeCaps.get(input.deviceId);
480
+ if (this.capabilityRegistry) for (const capName of this.capabilityRegistry.getCapsWithDefaultWrapper()) {
481
+ if (seenCaps.has(capName)) continue;
482
+ const defaultWrapperAddonId = this.capabilityRegistry.getDefaultWrapperForCap(capName);
483
+ if (!defaultWrapperAddonId) continue;
484
+ const hubLocalNative = this.capabilityRegistry.getNativeAddonId(capName, input.deviceId) ?? null;
485
+ const remoteNative = remote?.get(capName) ?? null;
486
+ const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
487
+ entries.push({
488
+ capName,
489
+ kind: "wrapped",
490
+ providerAddonId: defaultWrapperAddonId,
491
+ providerNodeId: this.resolveWrapperNodeId(defaultWrapperAddonId),
492
+ nativeAddonId
493
+ });
494
+ seenCaps.add(capName);
495
+ }
496
+ if (this.capabilityRegistry) for (const capName of this.capabilityRegistry.getNativeCapsForDevice(input.deviceId)) {
497
+ if (seenCaps.has(capName)) continue;
498
+ const nativeAddonId = this.capabilityRegistry.getNativeAddonId(capName, input.deviceId) ?? "";
499
+ entries.push({
500
+ capName,
501
+ kind: "native",
502
+ providerAddonId: nativeAddonId,
503
+ providerNodeId: this.ctx.kernel.localNodeId ?? "hub",
504
+ nativeAddonId
505
+ });
506
+ seenCaps.add(capName);
507
+ }
508
+ if (remote) for (const [capName, info] of remote) {
509
+ if (seenCaps.has(capName)) continue;
510
+ entries.push({
511
+ capName,
512
+ kind: "native",
513
+ providerAddonId: info.addonId,
514
+ providerNodeId: info.nodeId,
515
+ nativeAddonId: info.addonId
516
+ });
517
+ seenCaps.add(capName);
518
+ }
519
+ return {
520
+ deviceId: input.deviceId,
521
+ entries
522
+ };
523
+ }
524
+ /**
525
+ * Whole-fleet binding dump. Iterates every device known to the
526
+ * deviceRegistry and reuses the per-device `getBindings` resolver
527
+ * for each — same routing rules, single round-trip. Used by
528
+ * `SystemManager.init()` for warm-boot.
529
+ *
530
+ * Bindings change rarely (wrapper toggle, device add/remove) so
531
+ * clients invalidate via the existing
532
+ * `capability.binding-changed` event rather than re-fetching this
533
+ * payload periodically.
534
+ */
535
+ async getAllBindings() {
536
+ const hubRegistry = this.ctx.kernel?.deviceRegistry;
537
+ if (!hubRegistry) return [];
538
+ const out = [];
539
+ for (const device of hubRegistry.getAll()) out.push(await this.getBindings({ deviceId: device.id }));
540
+ return out;
541
+ }
542
+ /**
543
+ * Resolve a numeric deviceId to a stableId via persisted meta.
544
+ * Used only by the device-identity section of the device-details
545
+ * aggregator (see `buildBaseDeviceSection`) to surface the stableId as
546
+ * a readonly display field. All runtime/registry lookups are keyed by
547
+ * numeric deviceId; this helper is display-only.
548
+ */
549
+ async lookupPersistedStableId(deviceId) {
550
+ const meta = (await this.ctx.settings.readAddonStore()).deviceMeta ?? {};
551
+ for (const [key, m] of Object.entries(meta)) if (m.id === deviceId) {
552
+ const sep = key.indexOf(":");
553
+ if (sep < 0) continue;
554
+ return key.slice(sep + 1);
555
+ }
556
+ }
557
+ async getDeviceAggregate(deviceId, kind) {
558
+ const registry = this.capabilityRegistry;
559
+ if (!registry) {
560
+ this.ctx.logger.debug("capability registry unavailable — aggregate empty", { meta: { kind } });
561
+ return null;
562
+ }
563
+ const method = kind === "settings" ? "getDeviceSettingsContribution" : "getDeviceLiveContribution";
564
+ const contributors = [];
565
+ for (const info of registry.listCapabilities()) {
566
+ if (!registry.getDefinition(info.name)?.exposesDeviceSettings) continue;
567
+ const seen = /* @__PURE__ */ new Set();
568
+ const sorted = [...info.providers].sort((a, b) => a.length - b.length);
569
+ for (const addonId of sorted) {
570
+ if (addonId.includes("::native-")) continue;
571
+ const baseId = addonId.includes("@") ? addonId.slice(0, addonId.indexOf("@")) : addonId;
572
+ if (seen.has(baseId)) continue;
573
+ seen.add(baseId);
574
+ contributors.push({
575
+ capName: info.name,
576
+ addonId
577
+ });
578
+ }
579
+ }
580
+ const results = await Promise.all(contributors.map(async ({ capName, addonId }) => {
581
+ const provider = registry.getProviderByAddon(capName, addonId);
582
+ if (!provider) throw new Error(`[device-manager] capability "${capName}" lists provider "${addonId}" but getProviderByAddon returned null — registry inconsistency`);
583
+ try {
584
+ const contribution = await provider[method]({ deviceId });
585
+ if (!contribution) return null;
586
+ return {
587
+ capName,
588
+ addonId,
589
+ contribution: tagContribution(toWireShape(contribution), capName, addonId, kind)
590
+ };
591
+ } catch (err) {
592
+ const msg = err instanceof Error ? err.message : String(err);
593
+ this.ctx.logger.warn("contribution method failed", {
594
+ tags: {
595
+ deviceId,
596
+ addonId
597
+ },
598
+ meta: {
599
+ capName,
600
+ method,
601
+ error: msg
602
+ }
603
+ });
604
+ return null;
605
+ }
606
+ }));
607
+ const base = kind === "settings" ? await this.buildBaseDeviceSection(deviceId) : null;
608
+ const parts = [...base ? [tagContribution(base, "device-manager", "device-manager", kind)] : [], ...results.filter((r) => r !== null).map((r) => r.contribution)];
609
+ if (parts.length === 0) return null;
610
+ return mergeAggregates(parts);
611
+ }
612
+ /**
613
+ * Build the device-manager's own contribution to the aggregator — the
614
+ * device identity (id, stableId, addonId, type, online) + the
615
+ * driver-specific config exposed by the device class via
616
+ * `zodEntriesToConfigUI`.
617
+ *
618
+ * Two paths, deliberately symmetric with `getSettingsSchema`:
619
+ *
620
+ * - Hub-local: device's IDevice instance lives in this process'
621
+ * DeviceRegistry, we read config + schema directly by reference.
622
+ * - Cross-process: device lives in a forked worker (RtspCamera on
623
+ * provider-rtsp, ONVIF on provider-onvif, ). We ask the worker's
624
+ * `device-ops.getSettingsSchema` native provider for a wire-
625
+ * serializable ConfigUISchema and merge it in under the same
626
+ * "Driver Config" section, so the UI sees the same shape regardless
627
+ * of where the IDevice physically runs.
628
+ *
629
+ * Returns `null` only when the device genuinely doesn't exist anywhere
630
+ * (no hub-local, no persisted ownership, no device-ops native). The
631
+ * aggregator falls back to contributor sections only in that case.
632
+ */
633
+ async buildBaseDeviceSection(deviceId) {
634
+ const hubRegistry = this.ctx.kernel?.deviceRegistry;
635
+ const hubLocal = hubRegistry ? resolveDeviceById(hubRegistry, deviceId) : null;
636
+ const stableId = hubLocal?.device.stableId ?? await this.lookupPersistedStableId(deviceId);
637
+ const nativeOwner = this.resolveNativeDeviceOwner(deviceId);
638
+ const addonId = hubLocal?.addonId ?? nativeOwner?.addonId ?? null;
639
+ if (!hubLocal && !nativeOwner) return null;
640
+ const sections = [{
641
+ id: "device-identity",
642
+ title: "Identity",
643
+ tab: "general",
644
+ order: 0,
645
+ fields: [
646
+ {
647
+ type: "text",
648
+ key: "_deviceId",
649
+ label: "Device ID",
650
+ readonlyField: true,
651
+ value: String(deviceId)
652
+ },
653
+ {
654
+ type: "text",
655
+ key: "_stableId",
656
+ label: "Stable ID",
657
+ readonlyField: true,
658
+ value: stableId ?? ""
659
+ },
660
+ {
661
+ type: "text",
662
+ key: "_addonId",
663
+ label: "Driver",
664
+ readonlyField: true,
665
+ value: addonId ?? "unknown"
666
+ },
667
+ ...hubLocal ? [{
668
+ type: "text",
669
+ key: "_type",
670
+ label: "Type",
671
+ readonlyField: true,
672
+ value: hubLocal.device.type
673
+ }, {
674
+ type: "text",
675
+ key: "_online",
676
+ label: "Online",
677
+ readonlyField: true,
678
+ value: hubLocal.device.online ? "yes" : "no"
679
+ }] : []
680
+ ]
681
+ }];
682
+ const driverSchema = await this.resolveDriverConfigSchema(deviceId, hubLocal);
683
+ if (driverSchema) for (const section of driverSchema.sections) sections.push({
684
+ id: section.id,
685
+ title: section.title,
686
+ tab: section.tab ?? "general",
687
+ order: section.order ?? 1,
688
+ fields: [...section.fields],
689
+ ...section.description !== void 0 ? { description: section.description } : {},
690
+ ...section.columns !== void 0 ? { columns: section.columns } : {}
691
+ });
692
+ return {
693
+ sections,
694
+ ...driverSchema?.tabs ? { tabs: [...driverSchema.tabs] } : {}
695
+ };
696
+ }
697
+ /**
698
+ * Lookup the native owner for `device-ops` on `deviceId` — the native-cap
699
+ * registry (hub-local and remote) is keyed by numeric id.
700
+ */
701
+ resolveNativeDeviceOwner(deviceId) {
702
+ const local = this.capabilityRegistry?.getNativeAddonId("device-ops", deviceId) ?? null;
703
+ if (local) return {
704
+ addonId: local,
705
+ nodeId: this.ctx.kernel.localNodeId ?? "hub"
706
+ };
707
+ const remote = this.remoteNativeCaps.get(deviceId)?.get("device-ops") ?? null;
708
+ return remote ? {
709
+ addonId: remote.addonId,
710
+ nodeId: remote.nodeId
711
+ } : null;
712
+ }
713
+ /**
714
+ * Aggregate `status` across every registered cap for a device.
715
+ *
716
+ * Walks the supplied cap list (or `CAP_NAMES_WITH_STATUS` when
717
+ * omitted), looks up a native provider per cap via the capability
718
+ * registry, calls `provider.getStatus({ deviceId })`, and validates
719
+ * the return against the cap's own `status.schema`. Validation
720
+ * failures log a warning and yield `null` for that cap so the
721
+ * overall aggregate stays usable — a single misbehaving provider
722
+ * must not blank out a device's entire status view.
723
+ *
724
+ * Returned shape is `Record<capName, unknown | null>`; the client-
725
+ * side hook tightens this to `CapStatusTypeMap` via the generated
726
+ * `cap-status-types.ts`.
727
+ */
728
+ async getDeviceStatusAggregate(input) {
729
+ const capNames = input.caps ?? _camstack_types.CAP_NAMES_WITH_STATUS;
730
+ const registry = this.capabilityRegistry;
731
+ const out = {};
732
+ if (!registry) {
733
+ for (const name of capNames) out[name] = null;
734
+ return out;
735
+ }
736
+ await Promise.all(capNames.map(async (capName) => {
737
+ try {
738
+ const def = registry.getDefinition(capName);
739
+ if (!def?.status) {
740
+ out[capName] = null;
741
+ return;
742
+ }
743
+ const provider = registry.getNativeProvider(capName, input.deviceId);
744
+ if (!provider || typeof provider.getStatus !== "function") {
745
+ out[capName] = null;
746
+ return;
747
+ }
748
+ const raw = await provider.getStatus({ deviceId: input.deviceId });
749
+ if (raw == null) {
750
+ out[capName] = null;
751
+ return;
752
+ }
753
+ const parsed = def.status.schema.safeParse(raw);
754
+ if (!parsed.success) {
755
+ this.ctx.logger.warn("getDeviceStatusAggregate: provider returned invalid status, dropping", {
756
+ tags: { deviceId: input.deviceId },
757
+ meta: {
758
+ capName,
759
+ issues: parsed.error.issues.slice(0, 3)
760
+ }
761
+ });
762
+ out[capName] = null;
763
+ return;
764
+ }
765
+ out[capName] = parsed.data;
766
+ } catch (err) {
767
+ this.ctx.logger.warn("getDeviceStatusAggregate: provider threw, dropping", {
768
+ tags: { deviceId: input.deviceId },
769
+ meta: {
770
+ capName,
771
+ error: (0, _camstack_types.errMsg)(err)
772
+ }
773
+ });
774
+ out[capName] = null;
775
+ }
776
+ }));
777
+ return out;
778
+ }
779
+ /**
780
+ * Return the driver-specific device-settings contribution. Hub-local
781
+ * devices call `getSettingsUISchema()` directly; forked-worker devices
782
+ * go through the `device-ops.getSettingsSchema` cap method on the
783
+ * numeric-id-keyed native registry.
784
+ */
785
+ async resolveDriverConfigSchema(deviceId, hubLocal) {
786
+ if (hubLocal) {
787
+ const schema = hubLocal.device.getSettingsUISchema();
788
+ return schema.sections.length === 0 ? null : toWireShape(schema);
789
+ }
790
+ const ops = this.capabilityRegistry?.getNativeProvider("device-ops", deviceId);
791
+ if (!ops) return null;
792
+ try {
793
+ const schema = await ops.getSettingsSchema({ deviceId });
794
+ if (!schema) return null;
795
+ const wire = schema;
796
+ return wire.sections.length === 0 ? null : toWireShape(wire);
797
+ } catch (err) {
798
+ const msg = err instanceof Error ? err.message : String(err);
799
+ this.ctx.logger.warn("cross-process getSettingsSchema failed", {
800
+ tags: { deviceId },
801
+ meta: { error: msg }
802
+ });
803
+ return null;
804
+ }
805
+ }
806
+ async updateDeviceField(input) {
807
+ if (input.writerCapName === "device-manager") {
808
+ const hubRegistry = this.ctx.kernel?.deviceRegistry;
809
+ const found = hubRegistry ? resolveDeviceById(hubRegistry, input.deviceId) : null;
810
+ if (found) {
811
+ await found.device.applySettingsPatch({ [input.key]: input.value });
812
+ return { success: true };
813
+ }
814
+ const ops = this.capabilityRegistry?.getNativeProvider("device-ops", input.deviceId);
815
+ if (!ops) throw new Error(`[device-manager] device "${input.deviceId}" not found (no hub-local entry, no device-ops native provider)`);
816
+ await ops.setConfig({
817
+ deviceId: input.deviceId,
818
+ values: { [input.key]: input.value }
819
+ });
820
+ return { success: true };
821
+ }
822
+ const registry = this.capabilityRegistry;
823
+ if (!registry) throw new Error("[device-manager] updateDeviceField requires capability registry — unavailable on this node");
824
+ if (!registry.getDefinition(input.writerCapName)?.exposesDeviceSettings) throw new Error(`[device-manager] cap "${input.writerCapName}" does not expose device settings`);
825
+ const provider = registry.getProviderByAddon(input.writerCapName, input.writerAddonId);
826
+ if (!provider) throw new Error(`[device-manager] provider "${input.writerAddonId}" not registered for cap "${input.writerCapName}"`);
827
+ await provider.applyDeviceSettingsPatch({
828
+ deviceId: input.deviceId,
829
+ patch: { [input.key]: input.value }
830
+ });
831
+ return { success: true };
832
+ }
833
+ /**
834
+ * Batched counterpart of `updateDeviceField`. Groups changes by
835
+ * `(writerCapName, writerAddonId)` so each contributor receives a
836
+ * single `applyDeviceSettingsPatch` with all of its updates merged —
837
+ * avoids N round-trips for simultaneous edits in the same save.
838
+ *
839
+ * Per-provider failures are captured in the `failures[]` output so the
840
+ * admin UI can highlight which sections didn't persist; a failure on
841
+ * one provider does NOT abort the others.
842
+ */
843
+ async updateDeviceFieldsBatch(input) {
844
+ const groups = /* @__PURE__ */ new Map();
845
+ for (const change of input.changes) {
846
+ const key = `${change.writerCapName}::${change.writerAddonId}`;
847
+ const existing = groups.get(key);
848
+ if (existing) existing.patch[change.key] = change.value;
849
+ else groups.set(key, {
850
+ writerCapName: change.writerCapName,
851
+ writerAddonId: change.writerAddonId,
852
+ patch: { [change.key]: change.value }
853
+ });
854
+ }
855
+ const failures = [];
856
+ for (const group of groups.values()) try {
857
+ await this.applyGroupPatch(input.deviceId, group);
858
+ } catch (err) {
859
+ failures.push({
860
+ writerCapName: group.writerCapName,
861
+ writerAddonId: group.writerAddonId,
862
+ error: err instanceof Error ? err.message : String(err)
863
+ });
864
+ }
865
+ return {
866
+ success: true,
867
+ failures
868
+ };
869
+ }
870
+ /** Apply a single grouped patch to the appropriate provider. Mirrors
871
+ * `updateDeviceField` routing (special-case device-manager, else
872
+ * registry lookup). Used by `updateDeviceFieldsBatch`. */
873
+ async applyGroupPatch(deviceId, group) {
874
+ if (group.writerCapName === "device-manager") {
875
+ const hubRegistry = this.ctx.kernel?.deviceRegistry;
876
+ const found = hubRegistry ? resolveDeviceById(hubRegistry, deviceId) : null;
877
+ if (found) {
878
+ await found.device.applySettingsPatch(group.patch);
879
+ return;
880
+ }
881
+ const ops = this.capabilityRegistry?.getNativeProvider("device-ops", deviceId);
882
+ if (!ops) throw new Error(`[device-manager] device "${deviceId}" not found (no hub-local entry, no device-ops native provider)`);
883
+ await ops.setConfig({
884
+ deviceId,
885
+ values: group.patch
886
+ });
887
+ return;
888
+ }
889
+ const registry = this.capabilityRegistry;
890
+ if (!registry) throw new Error("[device-manager] capability registry unavailable");
891
+ if (!registry.getDefinition(group.writerCapName)?.exposesDeviceSettings) throw new Error(`[device-manager] cap "${group.writerCapName}" does not expose device settings`);
892
+ const provider = registry.getProviderByAddon(group.writerCapName, group.writerAddonId);
893
+ if (!provider) throw new Error(`[device-manager] provider "${group.writerAddonId}" not registered for cap "${group.writerCapName}"`);
894
+ await provider.applyDeviceSettingsPatch({
895
+ deviceId,
896
+ patch: group.patch
897
+ });
898
+ }
899
+ async listWrappersForCap(input) {
900
+ return [...this.capabilityRegistry?.getWrappersForCap(input.capName) ?? []];
901
+ }
902
+ async listBindableCapsForDeviceType(input) {
903
+ const registry = this.capabilityRegistry;
904
+ if (!registry) return [];
905
+ return registry.listDeviceScopedCapsForType(input.deviceType).map((capName) => ({
906
+ capName,
907
+ wrappers: [...registry.getWrappersForCap(capName)]
908
+ }));
909
+ }
910
+ async setWrapperActive(input) {
911
+ const storeKey = String(input.deviceId);
912
+ const store = await this.readBindingsStore();
913
+ const perDevice = { ...store.deviceBindings[storeKey] ?? {} };
914
+ if (input.active) perDevice[input.capName] = { wrapperAddonId: input.wrapperAddonId };
915
+ else perDevice[input.capName] = { wrapperAddonId: null };
916
+ const nextDeviceBindings = Object.keys(perDevice).length > 0 ? {
917
+ ...store.deviceBindings,
918
+ [storeKey]: perDevice
919
+ } : (() => {
920
+ const { [storeKey]: _drop, ...rest } = store.deviceBindings;
921
+ return rest;
922
+ })();
923
+ await this.writeBindingsStore({ deviceBindings: nextDeviceBindings });
924
+ this.ctx.eventBus.emit({
925
+ id: (0, node_crypto.randomUUID)(),
926
+ timestamp: /* @__PURE__ */ new Date(),
927
+ source: {
928
+ type: "addon",
929
+ id: this.ctx.id
930
+ },
931
+ category: _camstack_types.EventCategory.DeviceBindingsChanged,
932
+ data: {
933
+ deviceId: input.deviceId,
934
+ capName: input.capName,
935
+ reason: input.active ? "wrapper-activated" : "wrapper-deactivated",
936
+ addonId: input.wrapperAddonId,
937
+ nodeId: this.resolveWrapperNodeId(input.wrapperAddonId)
938
+ }
939
+ });
940
+ }
941
+ async onInitialize() {
942
+ const settings = this.ctx.settings;
943
+ if (!settings) {
944
+ this.ctx.logger.warn("ctx.settings not available — device persistence unavailable");
945
+ return;
946
+ }
947
+ const registry = this.ctx.kernel.deviceRegistry ?? null;
948
+ if (!registry) this.ctx.logger.warn("device-registry not available — live operations will use persisted data only");
949
+ const localNodeId = this.ctx.kernel.localNodeId ?? "hub";
950
+ this.ctx.eventBus.subscribe({ category: _camstack_types.EventCategory.DeviceBindingsChanged }, (event) => {
951
+ const { deviceId, capName, reason, addonId, nodeId } = event.data;
952
+ if (nodeId === localNodeId) return;
953
+ if (reason === "native-registered") {
954
+ let perDevice = this.remoteNativeCaps.get(deviceId);
955
+ if (!perDevice) {
956
+ perDevice = /* @__PURE__ */ new Map();
957
+ this.remoteNativeCaps.set(deviceId, perDevice);
958
+ }
959
+ perDevice.set(capName, {
960
+ addonId,
961
+ nodeId
962
+ });
963
+ } else if (reason === "native-unregistered") {
964
+ const perDevice = this.remoteNativeCaps.get(deviceId);
965
+ if (!perDevice) return;
966
+ perDevice.delete(capName);
967
+ if (perDevice.size === 0) this.remoteNativeCaps.delete(deviceId);
968
+ }
969
+ });
970
+ const cluster = this.ctx.kernel.cluster;
971
+ if (cluster) cluster.broker.localBus.on("$node.disconnected", (payload) => {
972
+ const gone = payload.node.id;
973
+ const emptyDevices = [];
974
+ for (const [deviceId, perDevice] of this.remoteNativeCaps) {
975
+ const toDelete = [];
976
+ for (const [capName, entry] of perDevice) if (entry.nodeId === gone) toDelete.push(capName);
977
+ for (const capName of toDelete) perDevice.delete(capName);
978
+ if (perDevice.size === 0) emptyDevices.push(deviceId);
979
+ }
980
+ for (const deviceId of emptyDevices) this.remoteNativeCaps.delete(deviceId);
981
+ });
982
+ const requireDeviceOps = (deviceId) => {
983
+ const ops = this.capabilityRegistry?.getNativeProvider("device-ops", deviceId);
984
+ if (!ops) throw new Error(`[device-manager] device-ops native provider not found for '${deviceId}'`);
985
+ return ops;
986
+ };
987
+ const readStore = async () => {
988
+ return await settings.readAddonStore();
989
+ };
990
+ const readIndex = async () => {
991
+ return (await readStore()).deviceIndex ?? {};
992
+ };
993
+ const readMeta = async () => {
994
+ return (await readStore()).deviceMeta ?? {};
995
+ };
996
+ /** Hardware-identity metadata map. Lives in a sibling key on the
997
+ * device-manager addon store so its writers (`setMetadata`) never
998
+ * collide with the lifecycle writers on `deviceMeta`
999
+ * (`registerDevice` / `setName` / `setLocation` / `setDisabled`).
1000
+ * Single-writer per row eliminates the "writer X clobbers writer
1001
+ * Y's field" bug class — `setMetadata` is the only producer. */
1002
+ const readMetadataMap = async () => {
1003
+ return (await readStore()).deviceMetadata ?? {};
1004
+ };
1005
+ let metaWriteChain = Promise.resolve();
1006
+ const withMetaWriteLock = async (fn) => {
1007
+ const previous = metaWriteChain;
1008
+ let release = () => {};
1009
+ metaWriteChain = new Promise((resolve) => {
1010
+ release = resolve;
1011
+ });
1012
+ try {
1013
+ await previous.catch(() => {});
1014
+ return await fn();
1015
+ } finally {
1016
+ release();
1017
+ }
1018
+ };
1019
+ /**
1020
+ * Resolve a numeric deviceId to the owning `(addonId, stableId)` pair.
1021
+ * Scans persisted meta — live IDevice lookup (hub registry) is handled
1022
+ * separately per call site so callers can decide whether to route to
1023
+ * an in-process driver or to the cross-process `device-ops` bridge.
1024
+ * Returns null when no device with that id is known to the hub.
1025
+ */
1026
+ const resolvePersistedById = async (deviceId) => {
1027
+ const meta = await readMeta();
1028
+ for (const [key, m] of Object.entries(meta)) if (m.id === deviceId) {
1029
+ const sep = key.indexOf(":");
1030
+ if (sep < 0) continue;
1031
+ return {
1032
+ addonId: key.slice(0, sep),
1033
+ stableId: key.slice(sep + 1),
1034
+ meta: m
1035
+ };
1036
+ }
1037
+ return null;
1038
+ };
1039
+ const idToAddonId = /* @__PURE__ */ new Map();
1040
+ {
1041
+ const meta = await readMeta();
1042
+ for (const [key, m] of Object.entries(meta)) {
1043
+ const sep = key.indexOf(":");
1044
+ if (sep < 0) continue;
1045
+ idToAddonId.set(m.id, key.slice(0, sep));
1046
+ }
1047
+ }
1048
+ if (cluster) cluster.broker.localBus.on("$node.connected", (payload) => {
1049
+ const connectedNodeId = payload.node.id;
1050
+ const lastSlash = connectedNodeId.lastIndexOf("/");
1051
+ if (lastSlash < 0) return;
1052
+ const connectedAddonId = connectedNodeId.slice(lastSlash + 1);
1053
+ if (connectedAddonId.length === 0) return;
1054
+ for (const [deviceId, ownerAddonId] of idToAddonId) {
1055
+ if (ownerAddonId !== connectedAddonId) continue;
1056
+ if (this.capabilityRegistry?.getNativeAddonId("device-ops", deviceId)) continue;
1057
+ let perDevice = this.remoteNativeCaps.get(deviceId);
1058
+ if (!perDevice) {
1059
+ perDevice = /* @__PURE__ */ new Map();
1060
+ this.remoteNativeCaps.set(deviceId, perDevice);
1061
+ }
1062
+ if (!perDevice.has("device-ops")) perDevice.set("device-ops", {
1063
+ addonId: connectedAddonId,
1064
+ nodeId: connectedNodeId
1065
+ });
1066
+ }
1067
+ setTimeout(() => {
1068
+ this.discoverWorkerNativeCaps(connectedNodeId, connectedAddonId).catch((err) => {
1069
+ this.ctx.logger.warn("worker native-cap discovery failed", { meta: {
1070
+ nodeId: connectedNodeId,
1071
+ addonId: connectedAddonId,
1072
+ error: (0, _camstack_types.errMsg)(err)
1073
+ } });
1074
+ });
1075
+ }, 500);
1076
+ });
1077
+ const allocateNextDeviceId = async () => {
1078
+ const current = (await readStore()).nextDeviceId ?? 1;
1079
+ await settings.writeAddonStore({ nextDeviceId: current + 1 });
1080
+ return current;
1081
+ };
1082
+ const provider = {
1083
+ /** Sync ownership lookup backing persistence fallbacks (e.g. remove()
1084
+ * when the owning worker is offline). Ownership is keyed by numeric
1085
+ * deviceId → owning addonId as recorded in the persisted meta. NOT
1086
+ * a native-cap lookup: an addon can own a device without registering
1087
+ * every possible cap natively (e.g. RtspCamera without snapshotUrl
1088
+ * doesn't register the snapshot cap). Use
1089
+ * `resolveNativeCapOwnerSync` for cap-resolution paths.
1090
+ */
1091
+ resolveDeviceOwnerSync: (deviceId) => {
1092
+ return idToAddonId.get(deviceId) ?? null;
1093
+ },
1094
+ /** Sync lookup for the addon that registered a native provider for
1095
+ * `(capName, deviceId)`. Backs `CapabilityRegistry`'s native fallback
1096
+ * so the hub only synthesizes a cross-process proxy when the cap is
1097
+ * actually published — never on speculative device ownership.
1098
+ *
1099
+ * Consults hub-local registrations first (in-process natives),
1100
+ * then the `remoteNativeCaps` map populated from
1101
+ * `DeviceBindingsChanged` events emitted by forked-worker
1102
+ * `registerNativeCap` calls. Both are generic: any addon that hosts
1103
+ * devices and registers caps via the standard context API shows up
1104
+ * here without per-addon branching.
1105
+ */
1106
+ resolveNativeCapOwnerSync: (capName, deviceId) => {
1107
+ const localAddonId = this.capabilityRegistry?.getNativeAddonId(capName, deviceId) ?? null;
1108
+ if (localAddonId) return {
1109
+ addonId: localAddonId,
1110
+ nodeId: this.ctx.kernel.localNodeId ?? "hub"
1111
+ };
1112
+ const remote = this.remoteNativeCaps.get(deviceId)?.get(capName) ?? null;
1113
+ if (remote) return {
1114
+ addonId: remote.addonId,
1115
+ nodeId: remote.nodeId
1116
+ };
1117
+ return null;
1118
+ },
1119
+ /** Idempotent numeric-id reservation. Callers invoke this before
1120
+ * constructing the owning `IDevice` so `DeviceContext.id` is bound
1121
+ * at construction time. A repeat call for the same `(addonId,
1122
+ * stableId)` returns the already-persisted id — same physical
1123
+ * device reconnecting after a driver restart keeps its original
1124
+ * number. Fresh pairs burn one slot from the monotonic
1125
+ * `nextDeviceId` counter and seed a meta placeholder so the
1126
+ * `deviceMeta` `id` invariant holds even before
1127
+ * `registerDevice` completes. */
1128
+ allocateDeviceId: async (input) => {
1129
+ const { addonId, stableId } = input;
1130
+ const key = deviceKey(addonId, stableId);
1131
+ return await withMetaWriteLock(async () => {
1132
+ const meta = await readMeta();
1133
+ const existing = meta[key];
1134
+ if (existing) return { id: existing.id };
1135
+ const id = await allocateNextDeviceId();
1136
+ await settings.writeAddonStore({ deviceMeta: {
1137
+ ...meta,
1138
+ [key]: {
1139
+ type: "generic",
1140
+ name: stableId,
1141
+ location: null,
1142
+ disabled: false,
1143
+ parentDeviceId: null,
1144
+ id
1145
+ }
1146
+ } });
1147
+ return { id };
1148
+ });
1149
+ },
1150
+ registerDevice: async (input) => {
1151
+ const { addonId, stableId, id, type, name, parentDeviceId, features, config } = input;
1152
+ const key = deviceKey(addonId, stableId);
1153
+ const featuresArr = Array.isArray(features) ? [...features] : [];
1154
+ const { isFirstRegistration } = await withMetaWriteLock(async () => {
1155
+ const index = await readIndex();
1156
+ const existing = index[addonId] ?? [];
1157
+ const wasInIndex = existing.includes(stableId);
1158
+ if (!wasInIndex) await settings.writeAddonStore({ deviceIndex: {
1159
+ ...index,
1160
+ [addonId]: [...existing, stableId]
1161
+ } });
1162
+ const meta = await readMeta();
1163
+ const existingMeta = meta[key];
1164
+ const isFirst = !existingMeta || !wasInIndex;
1165
+ await settings.writeAddonStore({ deviceMeta: {
1166
+ ...meta,
1167
+ [key]: {
1168
+ type,
1169
+ name: existingMeta && existingMeta.name !== stableId ? existingMeta.name : name,
1170
+ location: existingMeta?.location ?? null,
1171
+ disabled: existingMeta?.disabled ?? false,
1172
+ parentDeviceId,
1173
+ id,
1174
+ features: featuresArr
1175
+ }
1176
+ } });
1177
+ return { isFirstRegistration: isFirst };
1178
+ });
1179
+ if (Object.keys(config).length > 0) await settings.writeDeviceStore(id, config);
1180
+ idToAddonId.set(id, addonId);
1181
+ if (isFirstRegistration) this.ctx.eventBus.emit({
1182
+ id: (0, node_crypto.randomUUID)(),
1183
+ timestamp: /* @__PURE__ */ new Date(),
1184
+ source: {
1185
+ type: "device",
1186
+ id
1187
+ },
1188
+ category: _camstack_types.EventCategory.DeviceRegistered,
1189
+ data: {
1190
+ deviceId: id,
1191
+ name: name.length > 0 ? name : stableId,
1192
+ providerId: addonId,
1193
+ parentDeviceId: parentDeviceId ?? null
1194
+ }
1195
+ });
1196
+ else this.ctx.eventBus.emit({
1197
+ id: (0, node_crypto.randomUUID)(),
1198
+ timestamp: /* @__PURE__ */ new Date(),
1199
+ source: {
1200
+ type: "device",
1201
+ id
1202
+ },
1203
+ category: _camstack_types.EventCategory.DeviceMetaChanged,
1204
+ data: {
1205
+ deviceId: id,
1206
+ name: name.length > 0 ? name : stableId,
1207
+ providerId: addonId,
1208
+ parentDeviceId: parentDeviceId ?? null,
1209
+ features: featuresArr
1210
+ }
1211
+ });
1212
+ },
1213
+ removeDevice: async (input) => {
1214
+ const { deviceId } = input;
1215
+ const persisted = await resolvePersistedById(deviceId);
1216
+ if (!persisted) return;
1217
+ const { addonId, stableId, meta: persistedMeta } = persisted;
1218
+ const key = deviceKey(addonId, stableId);
1219
+ const deviceName = persistedMeta.name;
1220
+ await withMetaWriteLock(async () => {
1221
+ const index = await readIndex();
1222
+ const remaining = (index[addonId] ?? []).filter((sid) => sid !== stableId);
1223
+ const updatedIndex = remaining.length > 0 ? {
1224
+ ...index,
1225
+ [addonId]: remaining
1226
+ } : (() => {
1227
+ const { [addonId]: _removed, ...rest } = index;
1228
+ return rest;
1229
+ })();
1230
+ await settings.writeAddonStore({ deviceIndex: updatedIndex });
1231
+ const { [key]: _removedMeta, ...restMeta } = await readMeta();
1232
+ await settings.writeAddonStore({ deviceMeta: restMeta });
1233
+ const map = await readMetadataMap();
1234
+ if (key in map) {
1235
+ const { [key]: _removedMetadata, ...restMap } = map;
1236
+ await settings.writeAddonStore({ deviceMetadata: restMap });
1237
+ }
1238
+ });
1239
+ await settings.clearDeviceStore(deviceId);
1240
+ const bindingsStore = await this.readBindingsStore();
1241
+ const bindingKey = String(deviceId);
1242
+ if (bindingsStore.deviceBindings[bindingKey]) {
1243
+ const { [bindingKey]: _removedBindings, ...restBindings } = bindingsStore.deviceBindings;
1244
+ await this.writeBindingsStore({ deviceBindings: restBindings });
1245
+ }
1246
+ this.remoteNativeCaps.delete(deviceId);
1247
+ this.capabilityRegistry?.unregisterAllNativeForDevice(deviceId);
1248
+ idToAddonId.delete(deviceId);
1249
+ this.ctx.logger.info("removed device", { tags: {
1250
+ deviceId,
1251
+ deviceName: deviceName.length > 0 ? deviceName : stableId
1252
+ } });
1253
+ this.ctx.eventBus.emit({
1254
+ id: (0, node_crypto.randomUUID)(),
1255
+ timestamp: /* @__PURE__ */ new Date(),
1256
+ source: {
1257
+ type: "device",
1258
+ id: deviceId
1259
+ },
1260
+ category: _camstack_types.EventCategory.DeviceUnregistered,
1261
+ data: {
1262
+ deviceId,
1263
+ providerId: addonId,
1264
+ parentDeviceId: persistedMeta.parentDeviceId ?? null
1265
+ }
1266
+ });
1267
+ },
1268
+ persistConfig: async (input) => {
1269
+ const { deviceId, data } = input;
1270
+ if (!await resolvePersistedById(deviceId)) throw new Error(`[device-manager] persistConfig: unknown device id=${deviceId}`);
1271
+ await settings.writeDeviceStore(deviceId, data);
1272
+ },
1273
+ loadConfig: async (input) => {
1274
+ const { deviceId } = input;
1275
+ if (!await resolvePersistedById(deviceId)) return {};
1276
+ return settings.readDeviceStore(deviceId);
1277
+ },
1278
+ /**
1279
+ * Load the operator-organisational meta surface for one device
1280
+ * (`name` / `location` / `disabled` / `type` / `parentDeviceId`
1281
+ * / `addonId` + `id` / `stableId`). Used by the kernel proxy's
1282
+ * device-context factory to populate `ctx.deviceMeta` before
1283
+ * the device class constructor runs. Returns `null` when no
1284
+ * persisted row exists for the id.
1285
+ *
1286
+ * Reads default `location` to `null` and `disabled` to `false`
1287
+ * for legacy rows that predate the field — production code
1288
+ * relies on the IDevice type contract that both are present.
1289
+ */
1290
+ loadMeta: async (input) => {
1291
+ const { deviceId } = input;
1292
+ const persisted = await resolvePersistedById(deviceId);
1293
+ if (!persisted) return null;
1294
+ const { addonId, stableId, meta: m } = persisted;
1295
+ const key = deviceKey(addonId, stableId);
1296
+ const metadata = (await readMetadataMap())[key] ?? null;
1297
+ return {
1298
+ id: m.id,
1299
+ stableId,
1300
+ addonId,
1301
+ type: m.type,
1302
+ name: m.name,
1303
+ location: m.location ?? null,
1304
+ disabled: m.disabled ?? false,
1305
+ parentDeviceId: m.parentDeviceId,
1306
+ metadata
1307
+ };
1308
+ },
1309
+ /**
1310
+ * Update the operator-edited display name. Writes the meta
1311
+ * row, emits a `DeviceMetaChanged` event so live consumers
1312
+ * (UI device list, alert center) see the rename without
1313
+ * polling. The live `IDevice.name` mirror is updated by the
1314
+ * kernel proxy on its side (`device-cap-proxy.ts`).
1315
+ */
1316
+ setName: async (input) => {
1317
+ const { deviceId, name } = input;
1318
+ await withMetaWriteLock(async () => {
1319
+ const persisted = await resolvePersistedById(deviceId);
1320
+ if (!persisted) throw new Error(`[device-manager] setName: unknown device id=${deviceId}`);
1321
+ const { addonId, stableId, meta: m } = persisted;
1322
+ const key = deviceKey(addonId, stableId);
1323
+ const allMeta = await readMeta();
1324
+ await settings.writeAddonStore({ deviceMeta: {
1325
+ ...allMeta,
1326
+ [key]: {
1327
+ ...m,
1328
+ name
1329
+ }
1330
+ } });
1331
+ });
1332
+ this.ctx.eventBus.emit({
1333
+ id: (0, node_crypto.randomUUID)(),
1334
+ timestamp: /* @__PURE__ */ new Date(),
1335
+ source: {
1336
+ type: "device",
1337
+ id: deviceId
1338
+ },
1339
+ category: _camstack_types.EventCategory.DeviceMetaChanged,
1340
+ data: {
1341
+ deviceId,
1342
+ field: "name",
1343
+ value: name
1344
+ }
1345
+ });
1346
+ },
1347
+ /**
1348
+ * Update the operator-organisational location label. `null`
1349
+ * clears it. Mirrors the same persist-then-emit shape as
1350
+ * `setName`; consumers subscribe to `DeviceMetaChanged` and
1351
+ * filter on `field: 'location'`.
1352
+ */
1353
+ setLocation: async (input) => {
1354
+ const { deviceId, location } = input;
1355
+ await withMetaWriteLock(async () => {
1356
+ const persisted = await resolvePersistedById(deviceId);
1357
+ if (!persisted) throw new Error(`[device-manager] setLocation: unknown device id=${deviceId}`);
1358
+ const { addonId, stableId, meta: m } = persisted;
1359
+ const key = deviceKey(addonId, stableId);
1360
+ const allMeta = await readMeta();
1361
+ await settings.writeAddonStore({ deviceMeta: {
1362
+ ...allMeta,
1363
+ [key]: {
1364
+ ...m,
1365
+ location
1366
+ }
1367
+ } });
1368
+ });
1369
+ this.ctx.eventBus.emit({
1370
+ id: (0, node_crypto.randomUUID)(),
1371
+ timestamp: /* @__PURE__ */ new Date(),
1372
+ source: {
1373
+ type: "device",
1374
+ id: deviceId
1375
+ },
1376
+ category: _camstack_types.EventCategory.DeviceMetaChanged,
1377
+ data: {
1378
+ deviceId,
1379
+ field: "location",
1380
+ value: location
1381
+ }
1382
+ });
1383
+ },
1384
+ /**
1385
+ * Patch the device's hardware-identity metadata blob. Shallow
1386
+ * merge — `null` removes a key, anything else overwrites.
1387
+ * Drivers populate factual fields on first probe; operators
1388
+ * augment via the Device Info tab. Idempotent: a no-op patch
1389
+ * (every key already present with the same value) doesn't emit
1390
+ * the meta-changed event.
1391
+ */
1392
+ setMetadata: async (input) => {
1393
+ const { deviceId, patch } = input;
1394
+ const result = await withMetaWriteLock(async () => {
1395
+ const persisted = await resolvePersistedById(deviceId);
1396
+ if (!persisted) throw new Error(`[device-manager] setMetadata: unknown device id=${deviceId}`);
1397
+ const { addonId, stableId } = persisted;
1398
+ const key = deviceKey(addonId, stableId);
1399
+ const map = await readMetadataMap();
1400
+ const next = { ...map[key] ?? {} };
1401
+ let changed = false;
1402
+ for (const [k, v] of Object.entries(patch)) if (v === null) {
1403
+ if (k in next) {
1404
+ delete next[k];
1405
+ changed = true;
1406
+ }
1407
+ } else if (next[k] !== v) {
1408
+ next[k] = v;
1409
+ changed = true;
1410
+ }
1411
+ if (!changed) return { changed: false };
1412
+ const hasFields = Object.keys(next).length > 0;
1413
+ const updatedMap = { ...map };
1414
+ if (hasFields) updatedMap[key] = next;
1415
+ else delete updatedMap[key];
1416
+ await settings.writeAddonStore({ deviceMetadata: updatedMap });
1417
+ return {
1418
+ changed: true,
1419
+ finalMeta: hasFields ? next : null
1420
+ };
1421
+ });
1422
+ if (!result.changed) return;
1423
+ this.ctx.eventBus.emit({
1424
+ id: (0, node_crypto.randomUUID)(),
1425
+ timestamp: /* @__PURE__ */ new Date(),
1426
+ source: {
1427
+ type: "device",
1428
+ id: deviceId
1429
+ },
1430
+ category: _camstack_types.EventCategory.DeviceMetaChanged,
1431
+ data: {
1432
+ deviceId,
1433
+ field: "metadata",
1434
+ value: result.finalMeta
1435
+ }
1436
+ });
1437
+ },
1438
+ /**
1439
+ * Soft-disable the device. Persisted on the meta row;
1440
+ * lifecycle gating is the driver's responsibility (BaseDevice
1441
+ * exposes `this.disabled` for the driver to consult at the top
1442
+ * of its lifecycle methods).
1443
+ */
1444
+ setDisabled: async (input) => {
1445
+ const { deviceId, disabled } = input;
1446
+ await withMetaWriteLock(async () => {
1447
+ const persisted = await resolvePersistedById(deviceId);
1448
+ if (!persisted) throw new Error(`[device-manager] setDisabled: unknown device id=${deviceId}`);
1449
+ const { addonId, stableId, meta: m } = persisted;
1450
+ const key = deviceKey(addonId, stableId);
1451
+ const allMeta = await readMeta();
1452
+ await settings.writeAddonStore({ deviceMeta: {
1453
+ ...allMeta,
1454
+ [key]: {
1455
+ ...m,
1456
+ disabled
1457
+ }
1458
+ } });
1459
+ });
1460
+ this.ctx.eventBus.emit({
1461
+ id: (0, node_crypto.randomUUID)(),
1462
+ timestamp: /* @__PURE__ */ new Date(),
1463
+ source: {
1464
+ type: "device",
1465
+ id: deviceId
1466
+ },
1467
+ category: _camstack_types.EventCategory.DeviceMetaChanged,
1468
+ data: {
1469
+ deviceId,
1470
+ field: "disabled",
1471
+ value: disabled
1472
+ }
1473
+ });
1474
+ },
1475
+ loadRuntimeState: async (input) => {
1476
+ const { deviceId } = input;
1477
+ if (!await resolvePersistedById(deviceId)) return {};
1478
+ const data = await settings.readDeviceRuntimeState(deviceId);
1479
+ this.seedMirror(deviceId, data);
1480
+ return data;
1481
+ },
1482
+ /**
1483
+ * Union of (1) operator-curated location registry and (2) labels
1484
+ * currently in use on persisted devices. Case-insensitive
1485
+ * dedupe (preserves the first-seen casing). Sorted
1486
+ * case-insensitively for stable UI. Drives the Device Info
1487
+ * location autocomplete.
1488
+ */
1489
+ listLocations: async () => {
1490
+ const store = await settings.readAddonStore();
1491
+ const meta = store.deviceMeta ?? {};
1492
+ const registry = store.locations ?? [];
1493
+ const seen = /* @__PURE__ */ new Map();
1494
+ const consider = (raw) => {
1495
+ if (typeof raw !== "string") return;
1496
+ const trimmed = raw.trim();
1497
+ if (trimmed.length === 0) return;
1498
+ const key = trimmed.toLowerCase();
1499
+ if (!seen.has(key)) seen.set(key, trimmed);
1500
+ };
1501
+ for (const label of registry) consider(label);
1502
+ for (const m of Object.values(meta)) consider(m.location);
1503
+ return [...seen.values()].sort((a, b) => a.localeCompare(b, void 0, { sensitivity: "base" }));
1504
+ },
1505
+ /**
1506
+ * Add a label to the curated location registry. Idempotent:
1507
+ * existing entries (case-insensitive match) are silently kept.
1508
+ * Empty / whitespace-only inputs throw — operators must supply a
1509
+ * meaningful label.
1510
+ */
1511
+ addLocation: async (input) => {
1512
+ const trimmed = input.name.trim();
1513
+ if (trimmed.length === 0) throw new Error("[device-manager] addLocation: name must be non-empty");
1514
+ const current = (await settings.readAddonStore()).locations ?? [];
1515
+ if (current.some((l) => l.toLowerCase() === trimmed.toLowerCase())) return;
1516
+ await settings.writeAddonStore({ locations: [...current, trimmed] });
1517
+ },
1518
+ /**
1519
+ * Remove a label from the curated registry. Match is
1520
+ * case-insensitive. Devices that still reference this label keep
1521
+ * their `meta.location` value (the registry is a suggestion
1522
+ * list, not a foreign key) — pass `cascade: true` to also clear
1523
+ * `setLocation` on every device that referenced this exact
1524
+ * label. Cascade only matches case-insensitively + trimmed, same
1525
+ * as the registry equality check.
1526
+ */
1527
+ removeLocation: async (input) => {
1528
+ const trimmed = input.name.trim();
1529
+ if (trimmed.length === 0) return;
1530
+ const store = await settings.readAddonStore();
1531
+ const current = store.locations ?? [];
1532
+ const remaining = current.filter((l) => l.toLowerCase() !== trimmed.toLowerCase());
1533
+ if (remaining.length !== current.length) await settings.writeAddonStore({ locations: remaining });
1534
+ if (input.cascade !== true) return;
1535
+ const meta = store.deviceMeta ?? {};
1536
+ const updates = { ...meta };
1537
+ const cleared = [];
1538
+ for (const [key, m] of Object.entries(meta)) {
1539
+ if (typeof m.location !== "string") continue;
1540
+ if (m.location.trim().toLowerCase() !== trimmed.toLowerCase()) continue;
1541
+ updates[key] = {
1542
+ ...m,
1543
+ location: null
1544
+ };
1545
+ cleared.push(m.id);
1546
+ }
1547
+ if (cleared.length === 0) return;
1548
+ await settings.writeAddonStore({ deviceMeta: updates });
1549
+ for (const deviceId of cleared) this.ctx.eventBus.emit({
1550
+ id: (0, node_crypto.randomUUID)(),
1551
+ timestamp: /* @__PURE__ */ new Date(),
1552
+ source: {
1553
+ type: "device",
1554
+ id: deviceId
1555
+ },
1556
+ category: _camstack_types.EventCategory.DeviceMetaChanged,
1557
+ data: {
1558
+ deviceId,
1559
+ field: "location",
1560
+ value: null
1561
+ }
1562
+ });
1563
+ },
1564
+ listPersistedByAddon: async (input) => {
1565
+ const { addonId } = input;
1566
+ const [index, meta] = await Promise.all([readIndex(), readMeta()]);
1567
+ return (index[addonId] ?? []).map((stableId) => {
1568
+ const m = meta[deviceKey(addonId, stableId)];
1569
+ return {
1570
+ id: m.id,
1571
+ stableId,
1572
+ type: m.type,
1573
+ name: m.name,
1574
+ location: m.location ?? null,
1575
+ disabled: m.disabled ?? false,
1576
+ parentDeviceId: m.parentDeviceId
1577
+ };
1578
+ });
1579
+ },
1580
+ listAll: async (input) => {
1581
+ const { addonId } = input;
1582
+ const results = [];
1583
+ const seen = /* @__PURE__ */ new Set();
1584
+ const meta = await readMeta();
1585
+ const metadataMap = await readMetadataMap();
1586
+ if (registry) {
1587
+ const liveEntries = addonId ? registry.getAllForAddon(addonId).map((device) => ({
1588
+ addonId,
1589
+ device
1590
+ })) : registry.getAllWithAddonId();
1591
+ for (const { addonId: aid, device } of liveEntries) {
1592
+ const key = deviceKey(aid, device.stableId);
1593
+ const metadata = metadataMap[key] ?? null;
1594
+ const metaRow = meta[key] ?? null;
1595
+ results.push(toDeviceInfo(aid, device, metadata, metaRow));
1596
+ seen.add(key);
1597
+ }
1598
+ }
1599
+ const index = await readIndex();
1600
+ const targetAddons = addonId ? [addonId] : Object.keys(index);
1601
+ for (const aid of targetAddons) for (const stableId of index[aid] ?? []) {
1602
+ const key = deviceKey(aid, stableId);
1603
+ if (seen.has(key)) continue;
1604
+ const m = meta[key];
1605
+ const persistedType = m.type;
1606
+ const persistedConfig = await settings.readDeviceStore(m.id);
1607
+ const metadata = metadataMap[key] ?? null;
1608
+ results.push({
1609
+ id: m.id,
1610
+ stableId,
1611
+ addonId: aid,
1612
+ type: persistedType,
1613
+ name: m?.name ?? stableId,
1614
+ location: m?.location ?? null,
1615
+ disabled: m?.disabled ?? false,
1616
+ parentDeviceId: m?.parentDeviceId ?? null,
1617
+ role: null,
1618
+ online: registry !== null,
1619
+ features: persistedFeatures(m?.features),
1620
+ isCamera: persistedType === _camstack_types.DeviceType.Camera,
1621
+ config: persistedConfig ?? {},
1622
+ metadata
1623
+ });
1624
+ }
1625
+ return results;
1626
+ },
1627
+ getDevice: async (input) => {
1628
+ const { deviceId } = input;
1629
+ if (registry) {
1630
+ const found = resolveDeviceById(registry, deviceId);
1631
+ if (found) {
1632
+ const key = deviceKey(found.addonId, found.device.stableId);
1633
+ const [map, metaMap] = await Promise.all([readMetadataMap(), readMeta()]);
1634
+ const metadata = map[key] ?? null;
1635
+ const metaRow = metaMap[key] ?? null;
1636
+ return toDeviceInfo(found.addonId, found.device, metadata, metaRow);
1637
+ }
1638
+ }
1639
+ const persisted = await resolvePersistedById(deviceId);
1640
+ if (!persisted) return null;
1641
+ const { addonId: aid, stableId, meta: m } = persisted;
1642
+ const persistedConfig = await settings.readDeviceStore(m.id);
1643
+ const key = deviceKey(aid, stableId);
1644
+ const metadata = (await readMetadataMap())[key] ?? null;
1645
+ return {
1646
+ id: deviceId,
1647
+ stableId,
1648
+ addonId: aid,
1649
+ type: m.type,
1650
+ name: m.name,
1651
+ location: m.location ?? null,
1652
+ disabled: m.disabled ?? false,
1653
+ parentDeviceId: m.parentDeviceId,
1654
+ role: null,
1655
+ online: true,
1656
+ features: persistedFeatures(m.features),
1657
+ isCamera: false,
1658
+ config: persistedConfig ?? {},
1659
+ metadata
1660
+ };
1661
+ },
1662
+ getChildren: async (input) => {
1663
+ const { parentDeviceId } = input;
1664
+ let ownerAddonId = null;
1665
+ if (registry) {
1666
+ if (registry.getById(parentDeviceId)) ownerAddonId = registry.getAddonId(parentDeviceId);
1667
+ }
1668
+ if (!ownerAddonId) {
1669
+ const persisted = await resolvePersistedById(parentDeviceId);
1670
+ if (!persisted) return [];
1671
+ ownerAddonId = persisted.addonId;
1672
+ }
1673
+ const results = [];
1674
+ const seen = /* @__PURE__ */ new Set();
1675
+ const [index, meta, metadataMap] = await Promise.all([
1676
+ readIndex(),
1677
+ readMeta(),
1678
+ readMetadataMap()
1679
+ ]);
1680
+ if (registry) {
1681
+ const liveChildren = registry.getChildren(parentDeviceId);
1682
+ for (const device of liveChildren) {
1683
+ const key = deviceKey(ownerAddonId, device.stableId);
1684
+ const metadata = metadataMap[key] ?? null;
1685
+ const metaRow = meta[key] ?? null;
1686
+ results.push(toDeviceInfo(ownerAddonId, device, metadata, metaRow));
1687
+ seen.add(key);
1688
+ }
1689
+ }
1690
+ const persistedChildren = (index[ownerAddonId] ?? []).filter((sid) => meta[deviceKey(ownerAddonId, sid)]?.parentDeviceId === parentDeviceId);
1691
+ for (const childStableId of persistedChildren) {
1692
+ const key = deviceKey(ownerAddonId, childStableId);
1693
+ if (seen.has(key)) continue;
1694
+ const m = meta[key];
1695
+ const persistedConfig = await settings.readDeviceStore(m.id);
1696
+ const metadata = metadataMap[key] ?? null;
1697
+ results.push({
1698
+ id: m.id,
1699
+ stableId: childStableId,
1700
+ addonId: ownerAddonId,
1701
+ type: m.type,
1702
+ name: m.name,
1703
+ location: m.location ?? null,
1704
+ disabled: m.disabled ?? false,
1705
+ parentDeviceId,
1706
+ role: null,
1707
+ online: registry !== null,
1708
+ features: persistedFeatures(m.features),
1709
+ isCamera: false,
1710
+ config: persistedConfig ?? {},
1711
+ metadata
1712
+ });
1713
+ }
1714
+ return results;
1715
+ },
1716
+ getStreamSources: async (input) => {
1717
+ const { deviceId } = input;
1718
+ if (registry) {
1719
+ const found = resolveDeviceById(registry, deviceId);
1720
+ if (found) {
1721
+ if (!isCameraDevice(found.device)) return [];
1722
+ return (await found.device.getStreamSources()).map((s) => ({
1723
+ id: s.id,
1724
+ label: s.label,
1725
+ protocol: s.protocol,
1726
+ url: s.url,
1727
+ resolution: s.resolution,
1728
+ fps: s.fps,
1729
+ bitrate: s.bitrate,
1730
+ codec: s.codec,
1731
+ profileHint: s.profileHint
1732
+ }));
1733
+ }
1734
+ }
1735
+ if (!await resolvePersistedById(deviceId)) throw new Error(`[device-manager] Device with id ${deviceId} not found`);
1736
+ return (await requireDeviceOps(deviceId).getStreamSources({ deviceId })).map((s) => ({ ...s }));
1737
+ },
1738
+ getConfigSchema: async (input) => {
1739
+ const { deviceId } = input;
1740
+ if (registry) {
1741
+ const found = resolveDeviceById(registry, deviceId);
1742
+ if (found) return found.device.config.entries().map((entry) => ({
1743
+ key: entry.key,
1744
+ value: entry.value,
1745
+ ...entry.description !== void 0 ? { description: entry.description } : {}
1746
+ }));
1747
+ }
1748
+ if (!await resolvePersistedById(deviceId)) throw new Error(`[device-manager] Device with id ${deviceId} not found`);
1749
+ return (await requireDeviceOps(deviceId).getConfigEntries({ deviceId })).map((e) => ({ ...e }));
1750
+ },
1751
+ getSettingsSchema: async (input) => {
1752
+ const { deviceId } = input;
1753
+ if (registry) {
1754
+ const found = resolveDeviceById(registry, deviceId);
1755
+ if (found) return found.device.getSettingsUISchema();
1756
+ }
1757
+ if (!await resolvePersistedById(deviceId)) return null;
1758
+ return await requireDeviceOps(deviceId).getSettingsSchema({ deviceId }) ?? null;
1759
+ },
1760
+ updateConfig: async (input) => {
1761
+ const { deviceId } = input;
1762
+ if (registry) {
1763
+ const found = resolveDeviceById(registry, deviceId);
1764
+ if (found) {
1765
+ await found.device.config.setAll(input.values);
1766
+ return { success: true };
1767
+ }
1768
+ }
1769
+ if (!await resolvePersistedById(deviceId)) throw new Error(`[device-manager] Device with id ${deviceId} not found`);
1770
+ await requireDeviceOps(deviceId).setConfig({
1771
+ deviceId,
1772
+ values: input.values
1773
+ });
1774
+ return { success: true };
1775
+ },
1776
+ enable: async (input) => {
1777
+ await provider.setDisabled({
1778
+ deviceId: input.deviceId,
1779
+ disabled: false
1780
+ });
1781
+ return { success: true };
1782
+ },
1783
+ disable: async (input) => {
1784
+ await provider.setDisabled({
1785
+ deviceId: input.deviceId,
1786
+ disabled: true
1787
+ });
1788
+ return { success: true };
1789
+ },
1790
+ remove: async (input) => {
1791
+ const { deviceId } = input;
1792
+ if (registry) {
1793
+ const live = resolveDeviceById(registry, deviceId);
1794
+ if (live) {
1795
+ const deviceName = live.device.name;
1796
+ await live.device.removeDevice();
1797
+ registry.remove(deviceId);
1798
+ await provider.removeDevice({ deviceId });
1799
+ this.ctx.logger.info("removed hub-local device", { tags: {
1800
+ deviceId,
1801
+ deviceName
1802
+ } });
1803
+ return { success: true };
1804
+ }
1805
+ }
1806
+ const persisted = await resolvePersistedById(deviceId);
1807
+ if (!persisted) throw new Error(`[device-manager] Device with id ${deviceId} not found`);
1808
+ const { meta: persistedMeta } = persisted;
1809
+ try {
1810
+ await requireDeviceOps(deviceId).removeDevice({ deviceId });
1811
+ } catch (err) {
1812
+ this.ctx.logger.warn("remove via device-ops failed clearing persistence anyway", {
1813
+ tags: {
1814
+ deviceId,
1815
+ deviceName: persistedMeta.name
1816
+ },
1817
+ meta: { error: (0, _camstack_types.errMsg)(err) }
1818
+ });
1819
+ }
1820
+ await provider.removeDevice({ deviceId });
1821
+ return { success: true };
1822
+ },
1823
+ getStreamProfileMap: async (input) => {
1824
+ if (!registry) return {};
1825
+ const found = resolveDeviceById(registry, input.deviceId);
1826
+ if (!found) return {};
1827
+ const storedMap = found.device.config.entries().find((e) => e.key === "_profileMap")?.value;
1828
+ if (storedMap !== void 0 && typeof storedMap === "object" && storedMap !== null) return storedMap;
1829
+ if (!isCameraDevice(found.device)) return {};
1830
+ const sources = await found.device.getStreamSources();
1831
+ const profileMap = {};
1832
+ for (const s of sources) if (s.profileHint && s.id) profileMap[s.profileHint] = s.id;
1833
+ return profileMap;
1834
+ },
1835
+ setStreamProfileMap: async (input) => {
1836
+ const { deviceId } = input;
1837
+ if (registry) {
1838
+ const found = resolveDeviceById(registry, deviceId);
1839
+ if (found) {
1840
+ await found.device.config.setAll({ _profileMap: input.profileMap });
1841
+ return { success: true };
1842
+ }
1843
+ }
1844
+ if (!await resolvePersistedById(deviceId)) throw new Error(`[device-manager] Device with id ${deviceId} not found`);
1845
+ await requireDeviceOps(deviceId).setConfig({
1846
+ deviceId,
1847
+ values: { _profileMap: input.profileMap }
1848
+ });
1849
+ return { success: true };
1850
+ },
1851
+ probeStreams: async (input) => {
1852
+ const streamProbe = this.ctx.kernel.streamProbe;
1853
+ if (!streamProbe) return [];
1854
+ const sources = await provider.getStreamSources({ deviceId: input.deviceId });
1855
+ const results = [];
1856
+ for (const s of sources) {
1857
+ if (!s.url) continue;
1858
+ try {
1859
+ const metadata = await streamProbe.probe(s.url, { force: true });
1860
+ results.push({
1861
+ streamId: s.id,
1862
+ width: metadata.width,
1863
+ height: metadata.height,
1864
+ codec: metadata.codec,
1865
+ fps: metadata.fps,
1866
+ bitrateKbps: metadata.bitrateKbps
1867
+ });
1868
+ } catch (err) {
1869
+ this.ctx.logger.debug("streamProbe.probe failed — returning placeholder", { meta: {
1870
+ deviceId: input.deviceId,
1871
+ streamId: s.id,
1872
+ error: err instanceof Error ? err.message : String(err)
1873
+ } });
1874
+ results.push({ streamId: s.id });
1875
+ }
1876
+ }
1877
+ return results;
1878
+ },
1879
+ discoverDevices: async (input) => {
1880
+ const dp = await this.requireDeviceProvider(input.addonId);
1881
+ if (!await dp.supportsDiscovery({})) throw new Error(`Addon "${input.addonId}" does not support device discovery`);
1882
+ return (await dp.discoverDevices({})).map((d) => ({
1883
+ stableId: d.stableId,
1884
+ type: d.type,
1885
+ suggestedName: d.suggestedName,
1886
+ prefilledConfig: d.prefilledConfig
1887
+ }));
1888
+ },
1889
+ adoptDevice: async (input) => {
1890
+ const dp = await this.requireDeviceProvider(input.addonId);
1891
+ if (!await dp.supportsDiscovery({})) throw new Error(`Addon "${input.addonId}" does not support device adoption`);
1892
+ return dp.adoptDiscoveredDevice({ candidate: input.candidate });
1893
+ },
1894
+ getCreationSchema: async (input) => {
1895
+ const dp = await this.requireDeviceProvider(input.addonId);
1896
+ if (!await dp.supportsManualCreation({})) return null;
1897
+ return await dp.getChildCreationSchema({ type: input.type }) ?? null;
1898
+ },
1899
+ createDevice: async (input) => {
1900
+ const dp = await this.requireDeviceProvider(input.addonId);
1901
+ if (!await dp.supportsManualCreation({})) throw new Error(`Addon "${input.addonId}" does not support manual device creation`);
1902
+ return dp.createDevice({
1903
+ type: input.type,
1904
+ config: input.config
1905
+ });
1906
+ },
1907
+ testCreationField: async (input) => {
1908
+ return (await this.requireDeviceProvider(input.addonId)).testCreationField({
1909
+ type: input.type,
1910
+ key: input.key,
1911
+ value: input.value,
1912
+ ...input.formValues !== void 0 ? { formValues: input.formValues } : {}
1913
+ });
1914
+ },
1915
+ testField: async (input) => {
1916
+ const { deviceId } = input;
1917
+ let owningAddonId = null;
1918
+ if (registry) owningAddonId = registry.getAddonId(deviceId);
1919
+ if (!owningAddonId) owningAddonId = (await resolvePersistedById(deviceId))?.addonId ?? null;
1920
+ if (!owningAddonId) throw new Error(`Device with id ${deviceId} not found`);
1921
+ const dp = await this.waitDeviceProvider(owningAddonId);
1922
+ if (!dp) return {
1923
+ status: "ok",
1924
+ labels: [],
1925
+ error: void 0
1926
+ };
1927
+ if (typeof dp.testCreationField !== "function") return {
1928
+ status: "ok",
1929
+ labels: [],
1930
+ error: void 0
1931
+ };
1932
+ return dp.testCreationField({
1933
+ type: _camstack_types.DeviceType.Camera,
1934
+ key: input.key,
1935
+ value: input.value
1936
+ });
1937
+ },
1938
+ getBindings: async (input) => {
1939
+ const result = await this.getBindings({ deviceId: input.deviceId });
1940
+ return {
1941
+ deviceId: input.deviceId,
1942
+ entries: result.entries
1943
+ };
1944
+ },
1945
+ getAllBindings: async () => {
1946
+ return this.getAllBindings();
1947
+ },
1948
+ setWrapperActive: async (input) => {
1949
+ return this.setWrapperActive({
1950
+ deviceId: input.deviceId,
1951
+ capName: input.capName,
1952
+ wrapperAddonId: input.wrapperAddonId,
1953
+ active: input.active
1954
+ });
1955
+ },
1956
+ listWrappersForCap: async (input) => this.listWrappersForCap(input),
1957
+ listBindableCapsForDeviceType: async (input) => this.listBindableCapsForDeviceType(input),
1958
+ getDeviceSettingsAggregate: async (input) => {
1959
+ return this.getDeviceAggregate(input.deviceId, "settings");
1960
+ },
1961
+ getDeviceLiveInfoAggregate: async (input) => {
1962
+ return this.getDeviceAggregate(input.deviceId, "live");
1963
+ },
1964
+ getDeviceAggregate: async (input) => {
1965
+ const [settings, live] = await Promise.all([this.getDeviceAggregate(input.deviceId, "settings"), this.getDeviceAggregate(input.deviceId, "live")]);
1966
+ return {
1967
+ settings,
1968
+ live
1969
+ };
1970
+ },
1971
+ updateDeviceField: async (input) => {
1972
+ return this.updateDeviceField({
1973
+ deviceId: input.deviceId,
1974
+ writerCapName: input.writerCapName,
1975
+ writerAddonId: input.writerAddonId,
1976
+ key: input.key,
1977
+ value: input.value
1978
+ });
1979
+ },
1980
+ updateDeviceFieldsBatch: async (input) => {
1981
+ return this.updateDeviceFieldsBatch({
1982
+ deviceId: input.deviceId,
1983
+ changes: input.changes
1984
+ });
1985
+ },
1986
+ getDeviceStatusAggregate: async (input) => this.getDeviceStatusAggregate(input)
1987
+ };
1988
+ this.ctx.logger.info("registered device-manager capability", { meta: { liveRegistry: registry !== null } });
1989
+ if (registry) {
1990
+ this.propagator = new DeviceEventPropagator({
1991
+ eventBus: this.ctx.eventBus,
1992
+ getParentOf: (id) => registry.getById(id)?.parentDeviceId ?? null,
1993
+ logger: {
1994
+ warn: (msg, meta) => this.ctx.logger.warn(msg, meta ?? {}),
1995
+ debug: (msg, meta) => this.ctx.logger.debug(msg, meta ?? {})
1996
+ }
1997
+ });
1998
+ this.propagator.start();
1999
+ this.ctx.logger.info("device-event-propagator started");
2000
+ }
2001
+ return [{
2002
+ capability: _camstack_types.deviceManagerCapability,
2003
+ provider
2004
+ }, {
2005
+ capability: _camstack_types.deviceStateCapability,
2006
+ provider: {
2007
+ getSnapshot: async (input) => {
2008
+ return this.snapshotForDevice(input.deviceId);
2009
+ },
2010
+ getCapSlice: async (input) => {
2011
+ const slice = this.stateMirror.get(input.deviceId)?.get(input.capName);
2012
+ return slice ? { ...slice } : null;
2013
+ },
2014
+ getAllSnapshots: async () => {
2015
+ const out = {};
2016
+ for (const [deviceId, perCap] of this.stateMirror) {
2017
+ const dev = {};
2018
+ for (const [capName, slice] of perCap) dev[capName] = { ...slice };
2019
+ out[String(deviceId)] = dev;
2020
+ }
2021
+ return out;
2022
+ },
2023
+ setCapSlice: async (input) => {
2024
+ const { deviceId, capName, slice } = input;
2025
+ if (!await resolvePersistedById(deviceId)) throw new Error(`[device-manager] setCapSlice: unknown device id=${deviceId}`);
2026
+ this.applySingleCapUpdate(deviceId, capName, slice);
2027
+ this.scheduleRuntimeStateDiskWrite(deviceId, settings);
2028
+ }
2029
+ }
2030
+ }];
2031
+ }
2032
+ /**
2033
+ * Single-cap mirror update — diff against the current mirror,
2034
+ * persist the new slice in-memory, emit `DeviceStateChanged` for
2035
+ * this cap. No-op on identical writes (both same shape and same
2036
+ * values). Called by `setCapSlice` provider.
2037
+ */
2038
+ applySingleCapUpdate(deviceId, capName, slice) {
2039
+ let perCap = this.stateMirror.get(deviceId);
2040
+ if (!perCap) {
2041
+ perCap = /* @__PURE__ */ new Map();
2042
+ this.stateMirror.set(deviceId, perCap);
2043
+ }
2044
+ const prior = perCap.get(capName);
2045
+ if (prior && shallowEqual(prior, slice)) return;
2046
+ perCap.set(capName, { ...slice });
2047
+ this.emitStateChanged(deviceId, capName, slice);
2048
+ }
2049
+ /**
2050
+ * Debounced disk writer. Coalesces frequent writes (motion phase
2051
+ * transitions, battery pushes) into one `writeDeviceRuntimeState`
2052
+ * per `RUNTIME_STATE_DEBOUNCE_MS` window. Reads the per-device
2053
+ * blob from the live mirror at flush time so the disk picture is
2054
+ * always the latest state — no risk of writing a stale snapshot.
2055
+ */
2056
+ scheduleRuntimeStateDiskWrite(deviceId, settings) {
2057
+ let slot = this.runtimeStateDebounce.get(deviceId);
2058
+ if (!slot) {
2059
+ slot = {
2060
+ timer: null,
2061
+ inFlight: null
2062
+ };
2063
+ this.runtimeStateDebounce.set(deviceId, slot);
2064
+ }
2065
+ if (slot.timer) return;
2066
+ slot.timer = setTimeout(() => {
2067
+ slot.timer = null;
2068
+ const blob = this.snapshotForDevice(deviceId);
2069
+ const write = (async () => {
2070
+ try {
2071
+ await settings.writeDeviceRuntimeState(deviceId, blob);
2072
+ } catch (err) {
2073
+ this.ctx.logger.warn("writeDeviceRuntimeState failed", {
2074
+ tags: { deviceId },
2075
+ meta: { error: err instanceof Error ? err.message : String(err) }
2076
+ });
2077
+ } finally {
2078
+ slot.inFlight = null;
2079
+ }
2080
+ })();
2081
+ slot.inFlight = write;
2082
+ }, DeviceManagerAddon.RUNTIME_STATE_DEBOUNCE_MS);
2083
+ }
2084
+ /**
2085
+ * One-shot mirror seed used by `loadRuntimeState` at boot so the
2086
+ * hub knows about every persisted slice without waiting for the
2087
+ * first `setCapSlice` call. No events emitted — this is
2088
+ * initial-state population, not a transition.
2089
+ */
2090
+ seedMirror(deviceId, blob) {
2091
+ let perCap = this.stateMirror.get(deviceId);
2092
+ if (!perCap) {
2093
+ perCap = /* @__PURE__ */ new Map();
2094
+ this.stateMirror.set(deviceId, perCap);
2095
+ }
2096
+ for (const [capName, raw] of Object.entries(blob)) {
2097
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
2098
+ perCap.set(capName, { ...raw });
2099
+ }
2100
+ }
2101
+ snapshotForDevice(deviceId) {
2102
+ const perCap = this.stateMirror.get(deviceId);
2103
+ if (!perCap) return {};
2104
+ const out = {};
2105
+ for (const [k, v] of perCap) out[k] = { ...v };
2106
+ return out;
2107
+ }
2108
+ emitStateChanged(deviceId, capName, slice) {
2109
+ this.ctx.eventBus.emit({
2110
+ id: (0, node_crypto.randomUUID)(),
2111
+ timestamp: /* @__PURE__ */ new Date(),
2112
+ source: {
2113
+ type: "device",
2114
+ id: deviceId
2115
+ },
2116
+ category: _camstack_types.EventCategory.DeviceStateChanged,
2117
+ data: {
2118
+ deviceId,
2119
+ capName,
2120
+ slice
2121
+ }
2122
+ });
2123
+ }
2124
+ async onShutdown() {
2125
+ this.propagator?.stop();
2126
+ this.propagator = null;
2127
+ const settings = this.ctx.settings;
2128
+ const pending = [];
2129
+ for (const [deviceId, slot] of this.runtimeStateDebounce) {
2130
+ if (slot.timer) {
2131
+ clearTimeout(slot.timer);
2132
+ slot.timer = null;
2133
+ if (settings) {
2134
+ const blob = this.snapshotForDevice(deviceId);
2135
+ pending.push(settings.writeDeviceRuntimeState(deviceId, blob).catch((err) => {
2136
+ this.ctx.logger.warn("shutdown writeDeviceRuntimeState failed", {
2137
+ tags: { deviceId },
2138
+ meta: { error: err instanceof Error ? err.message : String(err) }
2139
+ });
2140
+ }));
2141
+ }
2142
+ }
2143
+ if (slot.inFlight) pending.push(slot.inFlight);
2144
+ }
2145
+ await Promise.all(pending);
2146
+ this.runtimeStateDebounce.clear();
2147
+ }
2149
2148
  };
2150
- var device_manager_addon_default = DeviceManagerAddon;
2151
- // Annotate the CommonJS export names for ESM import in node:
2152
- 0 && (module.exports = {
2153
- DeviceManagerAddon
2154
- });
2149
+ //#endregion
2150
+ exports.DeviceManagerAddon = DeviceManagerAddon;
2151
+ exports.default = DeviceManagerAddon;
2152
+
2155
2153
  //# sourceMappingURL=device-manager.addon.js.map