amaran-light-cli 1.11.0 → 1.12.0

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 (363) hide show
  1. package/README.md +108 -14
  2. package/dist/__tests__/autoCct.test.js +27 -0
  3. package/dist/__tests__/autoCct.test.js.map +1 -1
  4. package/dist/__tests__/bleHttpControl.test.d.ts +2 -0
  5. package/dist/__tests__/bleHttpControl.test.d.ts.map +1 -0
  6. package/dist/__tests__/bleHttpControl.test.js +230 -0
  7. package/dist/__tests__/bleHttpControl.test.js.map +1 -0
  8. package/dist/__tests__/bleStack.test.d.ts +2 -0
  9. package/dist/__tests__/bleStack.test.d.ts.map +1 -0
  10. package/dist/__tests__/bleStack.test.js +1239 -0
  11. package/dist/__tests__/bleStack.test.js.map +1 -0
  12. package/dist/__tests__/cctUtil.test.js +9 -4
  13. package/dist/__tests__/cctUtil.test.js.map +1 -1
  14. package/dist/__tests__/circadianDashboardSettings.test.d.ts +2 -0
  15. package/dist/__tests__/circadianDashboardSettings.test.d.ts.map +1 -0
  16. package/dist/__tests__/circadianDashboardSettings.test.js +82 -0
  17. package/dist/__tests__/circadianDashboardSettings.test.js.map +1 -0
  18. package/dist/__tests__/circadianIsolation.test.d.ts +2 -0
  19. package/dist/__tests__/circadianIsolation.test.d.ts.map +1 -0
  20. package/dist/__tests__/circadianIsolation.test.js +30 -0
  21. package/dist/__tests__/circadianIsolation.test.js.map +1 -0
  22. package/dist/__tests__/cli.config.test.js +49 -0
  23. package/dist/__tests__/cli.config.test.js.map +1 -1
  24. package/dist/__tests__/cli.smoke.test.js +34 -0
  25. package/dist/__tests__/cli.smoke.test.js.map +1 -1
  26. package/dist/__tests__/currentCct.test.d.ts +2 -0
  27. package/dist/__tests__/currentCct.test.d.ts.map +1 -0
  28. package/dist/__tests__/currentCct.test.js +29 -0
  29. package/dist/__tests__/currentCct.test.js.map +1 -0
  30. package/dist/__tests__/dashboard.test.d.ts +2 -0
  31. package/dist/__tests__/dashboard.test.d.ts.map +1 -0
  32. package/dist/__tests__/dashboard.test.js +126 -0
  33. package/dist/__tests__/dashboard.test.js.map +1 -0
  34. package/dist/__tests__/daylightSimulationParseUtils.test.d.ts +2 -0
  35. package/dist/__tests__/daylightSimulationParseUtils.test.d.ts.map +1 -0
  36. package/dist/__tests__/daylightSimulationParseUtils.test.js +16 -0
  37. package/dist/__tests__/daylightSimulationParseUtils.test.js.map +1 -0
  38. package/dist/__tests__/desktopImport.test.d.ts +2 -0
  39. package/dist/__tests__/desktopImport.test.d.ts.map +1 -0
  40. package/dist/__tests__/desktopImport.test.js +234 -0
  41. package/dist/__tests__/desktopImport.test.js.map +1 -0
  42. package/dist/__tests__/lightControl.basic.test.d.ts +2 -0
  43. package/dist/__tests__/lightControl.basic.test.d.ts.map +1 -0
  44. package/dist/__tests__/lightControl.basic.test.js +119 -0
  45. package/dist/__tests__/lightControl.basic.test.js.map +1 -0
  46. package/dist/__tests__/lightControl.colorEffects.test.d.ts +2 -0
  47. package/dist/__tests__/lightControl.colorEffects.test.d.ts.map +1 -0
  48. package/dist/__tests__/lightControl.colorEffects.test.js +170 -0
  49. package/dist/__tests__/lightControl.colorEffects.test.js.map +1 -0
  50. package/dist/__tests__/lightControl.management.test.d.ts +2 -0
  51. package/dist/__tests__/lightControl.management.test.d.ts.map +1 -0
  52. package/dist/__tests__/lightControl.management.test.js +90 -0
  53. package/dist/__tests__/lightControl.management.test.js.map +1 -0
  54. package/dist/__tests__/lightControlTestUtils.d.ts +10 -0
  55. package/dist/__tests__/lightControlTestUtils.d.ts.map +1 -0
  56. package/dist/__tests__/lightControlTestUtils.js +57 -0
  57. package/dist/__tests__/lightControlTestUtils.js.map +1 -0
  58. package/dist/__tests__/meshConfiguration.test.d.ts +2 -0
  59. package/dist/__tests__/meshConfiguration.test.d.ts.map +1 -0
  60. package/dist/__tests__/meshConfiguration.test.js +78 -0
  61. package/dist/__tests__/meshConfiguration.test.js.map +1 -0
  62. package/dist/__tests__/nativeGroups.test.d.ts +2 -0
  63. package/dist/__tests__/nativeGroups.test.d.ts.map +1 -0
  64. package/dist/__tests__/nativeGroups.test.js +104 -0
  65. package/dist/__tests__/nativeGroups.test.js.map +1 -0
  66. package/dist/__tests__/programApi.test.d.ts +2 -0
  67. package/dist/__tests__/programApi.test.d.ts.map +1 -0
  68. package/dist/__tests__/programApi.test.js +73 -0
  69. package/dist/__tests__/programApi.test.js.map +1 -0
  70. package/dist/__tests__/programs.test.d.ts +2 -0
  71. package/dist/__tests__/programs.test.d.ts.map +1 -0
  72. package/dist/__tests__/programs.test.js +176 -0
  73. package/dist/__tests__/programs.test.js.map +1 -0
  74. package/dist/__tests__/scheduleMaker.test.d.ts +2 -0
  75. package/dist/__tests__/scheduleMaker.test.d.ts.map +1 -0
  76. package/dist/__tests__/scheduleMaker.test.js +25 -0
  77. package/dist/__tests__/scheduleMaker.test.js.map +1 -0
  78. package/dist/ble/async.d.ts +2 -0
  79. package/dist/ble/async.d.ts.map +1 -0
  80. package/dist/ble/async.js +15 -0
  81. package/dist/ble/async.js.map +1 -0
  82. package/dist/ble/colors.d.ts +5 -0
  83. package/dist/ble/colors.d.ts.map +1 -0
  84. package/dist/ble/colors.js +44 -0
  85. package/dist/ble/colors.js.map +1 -0
  86. package/dist/ble/configuration.d.ts +22 -0
  87. package/dist/ble/configuration.d.ts.map +1 -0
  88. package/dist/ble/configuration.js +122 -0
  89. package/dist/ble/configuration.js.map +1 -0
  90. package/dist/ble/controller.d.ts +118 -0
  91. package/dist/ble/controller.d.ts.map +1 -0
  92. package/dist/ble/controller.js +931 -0
  93. package/dist/ble/controller.js.map +1 -0
  94. package/dist/ble/crypto.d.ts +34 -0
  95. package/dist/ble/crypto.d.ts.map +1 -0
  96. package/dist/ble/crypto.js +178 -0
  97. package/dist/ble/crypto.js.map +1 -0
  98. package/dist/ble/daemon.d.ts +2 -0
  99. package/dist/ble/daemon.d.ts.map +1 -0
  100. package/dist/ble/daemon.js +76 -0
  101. package/dist/ble/daemon.js.map +1 -0
  102. package/dist/ble/dashboard.d.ts +331 -0
  103. package/dist/ble/dashboard.d.ts.map +1 -0
  104. package/dist/ble/dashboard.js +280 -0
  105. package/dist/ble/dashboard.js.map +1 -0
  106. package/dist/ble/desktop.d.ts +4 -0
  107. package/dist/ble/desktop.d.ts.map +1 -0
  108. package/dist/ble/desktop.js +33 -0
  109. package/dist/ble/desktop.js.map +1 -0
  110. package/dist/ble/desktopLibrary.d.ts +12 -0
  111. package/dist/ble/desktopLibrary.d.ts.map +1 -0
  112. package/dist/ble/desktopLibrary.js +346 -0
  113. package/dist/ble/desktopLibrary.js.map +1 -0
  114. package/dist/ble/effects.d.ts +34 -0
  115. package/dist/ble/effects.d.ts.map +1 -0
  116. package/dist/ble/effects.js +138 -0
  117. package/dist/ble/effects.js.map +1 -0
  118. package/dist/ble/fan.d.ts +244 -0
  119. package/dist/ble/fan.d.ts.map +1 -0
  120. package/dist/ble/fan.js +46 -0
  121. package/dist/ble/fan.js.map +1 -0
  122. package/dist/ble/library.d.ts +341 -0
  123. package/dist/ble/library.d.ts.map +1 -0
  124. package/dist/ble/library.js +201 -0
  125. package/dist/ble/library.js.map +1 -0
  126. package/dist/ble/media.d.ts +133 -0
  127. package/dist/ble/media.d.ts.map +1 -0
  128. package/dist/ble/media.js +186 -0
  129. package/dist/ble/media.js.map +1 -0
  130. package/dist/ble/nativeGroups.d.ts +22 -0
  131. package/dist/ble/nativeGroups.d.ts.map +1 -0
  132. package/dist/ble/nativeGroups.js +114 -0
  133. package/dist/ble/nativeGroups.js.map +1 -0
  134. package/dist/ble/packets.d.ts +3 -0
  135. package/dist/ble/packets.d.ts.map +1 -0
  136. package/dist/ble/packets.js +15 -0
  137. package/dist/ble/packets.js.map +1 -0
  138. package/dist/ble/programs.d.ts +318 -0
  139. package/dist/ble/programs.d.ts.map +1 -0
  140. package/dist/ble/programs.js +329 -0
  141. package/dist/ble/programs.js.map +1 -0
  142. package/dist/ble/provisioning.d.ts +13 -0
  143. package/dist/ble/provisioning.d.ts.map +1 -0
  144. package/dist/ble/provisioning.js +39 -0
  145. package/dist/ble/provisioning.js.map +1 -0
  146. package/dist/ble/server.d.ts +18 -0
  147. package/dist/ble/server.d.ts.map +1 -0
  148. package/dist/ble/server.js +548 -0
  149. package/dist/ble/server.js.map +1 -0
  150. package/dist/ble/service.d.ts +2 -0
  151. package/dist/ble/service.d.ts.map +1 -0
  152. package/dist/ble/service.js +77 -0
  153. package/dist/ble/service.js.map +1 -0
  154. package/dist/ble/settings.d.ts +18 -0
  155. package/dist/ble/settings.d.ts.map +1 -0
  156. package/dist/ble/settings.js +25 -0
  157. package/dist/ble/settings.js.map +1 -0
  158. package/dist/ble/setup.d.ts +2 -0
  159. package/dist/ble/setup.d.ts.map +1 -0
  160. package/dist/ble/setup.js +31 -0
  161. package/dist/ble/setup.js.map +1 -0
  162. package/dist/ble/storage.d.ts +94 -0
  163. package/dist/ble/storage.d.ts.map +1 -0
  164. package/dist/ble/storage.js +132 -0
  165. package/dist/ble/storage.js.map +1 -0
  166. package/dist/ble/telink.d.ts +63 -0
  167. package/dist/ble/telink.d.ts.map +1 -0
  168. package/dist/ble/telink.js +108 -0
  169. package/dist/ble/telink.js.map +1 -0
  170. package/dist/ble/transitions.d.ts +3 -0
  171. package/dist/ble/transitions.d.ts.map +1 -0
  172. package/dist/ble/transitions.js +28 -0
  173. package/dist/ble/transitions.js.map +1 -0
  174. package/dist/ble/transport.d.ts +48 -0
  175. package/dist/ble/transport.d.ts.map +1 -0
  176. package/dist/ble/transport.js +370 -0
  177. package/dist/ble/transport.js.map +1 -0
  178. package/dist/circadian.d.ts +3 -0
  179. package/dist/circadian.d.ts.map +1 -1
  180. package/dist/circadian.js +2 -0
  181. package/dist/circadian.js.map +1 -1
  182. package/dist/cli.d.ts +3 -2
  183. package/dist/cli.d.ts.map +1 -1
  184. package/dist/cli.js +125 -6
  185. package/dist/cli.js.map +1 -1
  186. package/dist/commands/__tests__/newCommands.test.js +3 -4
  187. package/dist/commands/__tests__/newCommands.test.js.map +1 -1
  188. package/dist/commands/cmdUtils.d.ts +5 -2
  189. package/dist/commands/cmdUtils.d.ts.map +1 -1
  190. package/dist/commands/cmdUtils.js +29 -10
  191. package/dist/commands/cmdUtils.js.map +1 -1
  192. package/dist/commands/daylightSimulation/autoCct.d.ts.map +1 -1
  193. package/dist/commands/daylightSimulation/autoCct.js +67 -197
  194. package/dist/commands/daylightSimulation/autoCct.js.map +1 -1
  195. package/dist/commands/daylightSimulation/graphSchedule.d.ts +1 -1
  196. package/dist/commands/daylightSimulation/graphSchedule.d.ts.map +1 -1
  197. package/dist/commands/daylightSimulation/printSchedule.d.ts +1 -1
  198. package/dist/commands/daylightSimulation/printSchedule.d.ts.map +1 -1
  199. package/dist/commands/daylightSimulation/printSchedule.js +2 -2
  200. package/dist/commands/daylightSimulation/printSchedule.js.map +1 -1
  201. package/dist/commands/daylightSimulation/service.d.ts +1 -1
  202. package/dist/commands/daylightSimulation/service.d.ts.map +1 -1
  203. package/dist/commands/daylightSimulation/service.js +5 -3
  204. package/dist/commands/daylightSimulation/service.js.map +1 -1
  205. package/dist/commands/daylightSimulation/simulateSchedule.d.ts +1 -1
  206. package/dist/commands/daylightSimulation/simulateSchedule.d.ts.map +1 -1
  207. package/dist/commands/daylightSimulation/simulateSchedule.js +5 -4
  208. package/dist/commands/daylightSimulation/simulateSchedule.js.map +1 -1
  209. package/dist/commands/deviceControl/ble.d.ts +4 -0
  210. package/dist/commands/deviceControl/ble.d.ts.map +1 -0
  211. package/dist/commands/deviceControl/ble.js +214 -0
  212. package/dist/commands/deviceControl/ble.js.map +1 -0
  213. package/dist/commands/deviceControl/cct.d.ts.map +1 -1
  214. package/dist/commands/deviceControl/cct.js +41 -11
  215. package/dist/commands/deviceControl/cct.js.map +1 -1
  216. package/dist/commands/deviceControl/color.d.ts.map +1 -1
  217. package/dist/commands/deviceControl/color.js +13 -3
  218. package/dist/commands/deviceControl/color.js.map +1 -1
  219. package/dist/commands/deviceControl/config.d.ts.map +1 -1
  220. package/dist/commands/deviceControl/config.js +26 -3
  221. package/dist/commands/deviceControl/config.js.map +1 -1
  222. package/dist/commands/deviceControl/{discover.d.ts → desktop.d.ts} +3 -2
  223. package/dist/commands/deviceControl/desktop.d.ts.map +1 -0
  224. package/dist/commands/deviceControl/desktop.js +66 -0
  225. package/dist/commands/deviceControl/desktop.js.map +1 -0
  226. package/dist/commands/deviceControl/effect.d.ts.map +1 -1
  227. package/dist/commands/deviceControl/effect.js +90 -159
  228. package/dist/commands/deviceControl/effect.js.map +1 -1
  229. package/dist/commands/deviceControl/fan.d.ts.map +1 -1
  230. package/dist/commands/deviceControl/fan.js +77 -95
  231. package/dist/commands/deviceControl/fan.js.map +1 -1
  232. package/dist/commands/deviceControl/group.d.ts.map +1 -1
  233. package/dist/commands/deviceControl/group.js +43 -6
  234. package/dist/commands/deviceControl/group.js.map +1 -1
  235. package/dist/commands/deviceControl/hsi.d.ts.map +1 -1
  236. package/dist/commands/deviceControl/hsi.js +3 -1
  237. package/dist/commands/deviceControl/hsi.js.map +1 -1
  238. package/dist/commands/deviceControl/info.d.ts.map +1 -1
  239. package/dist/commands/deviceControl/info.js +0 -50
  240. package/dist/commands/deviceControl/info.js.map +1 -1
  241. package/dist/commands/deviceControl/intensity.d.ts.map +1 -1
  242. package/dist/commands/deviceControl/intensity.js +32 -8
  243. package/dist/commands/deviceControl/intensity.js.map +1 -1
  244. package/dist/commands/deviceControl/list.d.ts.map +1 -1
  245. package/dist/commands/deviceControl/list.js +3 -2
  246. package/dist/commands/deviceControl/list.js.map +1 -1
  247. package/dist/commands/deviceControl/power.d.ts.map +1 -1
  248. package/dist/commands/deviceControl/power.js +9 -3
  249. package/dist/commands/deviceControl/power.js.map +1 -1
  250. package/dist/commands/deviceControl/preset.d.ts.map +1 -1
  251. package/dist/commands/deviceControl/preset.js +56 -19
  252. package/dist/commands/deviceControl/preset.js.map +1 -1
  253. package/dist/commands/deviceControl/program.d.ts +4 -0
  254. package/dist/commands/deviceControl/program.d.ts.map +1 -0
  255. package/dist/commands/deviceControl/program.js +199 -0
  256. package/dist/commands/deviceControl/program.js.map +1 -0
  257. package/dist/commands/deviceControl/quickshot.d.ts.map +1 -1
  258. package/dist/commands/deviceControl/quickshot.js +75 -13
  259. package/dist/commands/deviceControl/quickshot.js.map +1 -1
  260. package/dist/commands/deviceControl/scene.d.ts.map +1 -1
  261. package/dist/commands/deviceControl/scene.js +72 -9
  262. package/dist/commands/deviceControl/scene.js.map +1 -1
  263. package/dist/commands/deviceControl/status.d.ts.map +1 -1
  264. package/dist/commands/deviceControl/status.js +26 -3
  265. package/dist/commands/deviceControl/status.js.map +1 -1
  266. package/dist/commands/parseUtils.d.ts +1 -1
  267. package/dist/commands/parseUtils.d.ts.map +1 -1
  268. package/dist/commands/parseUtils.js +1 -11
  269. package/dist/commands/parseUtils.js.map +1 -1
  270. package/dist/commands-api.d.ts +1 -1
  271. package/dist/commands-api.d.ts.map +1 -1
  272. package/dist/commands-api.js +1 -1
  273. package/dist/commands-api.js.map +1 -1
  274. package/dist/commands.d.ts.map +1 -1
  275. package/dist/commands.js +4 -2
  276. package/dist/commands.js.map +1 -1
  277. package/dist/config.d.ts +82 -0
  278. package/dist/config.d.ts.map +1 -1
  279. package/dist/config.js +13 -2
  280. package/dist/config.js.map +1 -1
  281. package/dist/daylightSimulation/cctUtil.d.ts.map +1 -1
  282. package/dist/daylightSimulation/cctUtil.js +2 -2
  283. package/dist/daylightSimulation/cctUtil.js.map +1 -1
  284. package/dist/daylightSimulation/constants.d.ts +10 -0
  285. package/dist/daylightSimulation/constants.d.ts.map +1 -1
  286. package/dist/daylightSimulation/constants.js +4 -0
  287. package/dist/daylightSimulation/constants.js.map +1 -1
  288. package/dist/daylightSimulation/currentCct.d.ts +42 -0
  289. package/dist/daylightSimulation/currentCct.d.ts.map +1 -0
  290. package/dist/daylightSimulation/currentCct.js +217 -0
  291. package/dist/daylightSimulation/currentCct.js.map +1 -0
  292. package/dist/daylightSimulation/dashboardSettings.d.ts +47 -0
  293. package/dist/daylightSimulation/dashboardSettings.d.ts.map +1 -0
  294. package/dist/daylightSimulation/dashboardSettings.js +95 -0
  295. package/dist/daylightSimulation/dashboardSettings.js.map +1 -0
  296. package/dist/daylightSimulation/dashboardStatus.d.ts +72 -0
  297. package/dist/daylightSimulation/dashboardStatus.d.ts.map +1 -0
  298. package/dist/daylightSimulation/dashboardStatus.js +191 -0
  299. package/dist/daylightSimulation/dashboardStatus.js.map +1 -0
  300. package/dist/daylightSimulation/parseUtils.d.ts +3 -0
  301. package/dist/daylightSimulation/parseUtils.d.ts.map +1 -0
  302. package/dist/daylightSimulation/parseUtils.js +25 -0
  303. package/dist/daylightSimulation/parseUtils.js.map +1 -0
  304. package/dist/daylightSimulation/scheduleMaker.d.ts +2 -2
  305. package/dist/daylightSimulation/scheduleMaker.d.ts.map +1 -1
  306. package/dist/daylightSimulation/scheduleMaker.js +6 -6
  307. package/dist/daylightSimulation/scheduleMaker.js.map +1 -1
  308. package/dist/daylightSimulation/types.d.ts +13 -2
  309. package/dist/daylightSimulation/types.d.ts.map +1 -1
  310. package/dist/daylightSimulation/types.js +0 -2
  311. package/dist/daylightSimulation/types.js.map +1 -1
  312. package/dist/device-control.d.ts +1 -0
  313. package/dist/device-control.d.ts.map +1 -1
  314. package/dist/device-control.js +1 -0
  315. package/dist/device-control.js.map +1 -1
  316. package/dist/deviceControl/bleHttpControl.d.ts +129 -0
  317. package/dist/deviceControl/bleHttpControl.d.ts.map +1 -0
  318. package/dist/deviceControl/bleHttpControl.js +753 -0
  319. package/dist/deviceControl/bleHttpControl.js.map +1 -0
  320. package/dist/deviceControl/lightControl.d.ts +8 -68
  321. package/dist/deviceControl/lightControl.d.ts.map +1 -1
  322. package/dist/deviceControl/lightControl.js +6 -246
  323. package/dist/deviceControl/lightControl.js.map +1 -1
  324. package/dist/deviceControl/lightControlCommands.d.ts +68 -0
  325. package/dist/deviceControl/lightControlCommands.d.ts.map +1 -0
  326. package/dist/deviceControl/lightControlCommands.js +244 -0
  327. package/dist/deviceControl/lightControlCommands.js.map +1 -0
  328. package/dist/deviceControl/types.d.ts +75 -4
  329. package/dist/deviceControl/types.d.ts.map +1 -1
  330. package/dist/deviceControl/types.js +1 -0
  331. package/dist/deviceControl/types.js.map +1 -1
  332. package/dist/index.d.ts +2 -1
  333. package/dist/index.d.ts.map +1 -1
  334. package/dist/index.js +1 -0
  335. package/dist/index.js.map +1 -1
  336. package/dist/test/MockLightServer.d.ts +1 -0
  337. package/dist/test/MockLightServer.d.ts.map +1 -1
  338. package/dist/test/MockLightServer.js +5 -0
  339. package/dist/test/MockLightServer.js.map +1 -1
  340. package/docs/CHANGELOG.md +37 -0
  341. package/docs/DIRECT_BLE.md +359 -0
  342. package/package.json +8 -4
  343. package/scripts/ble-cli-check.ts +129 -0
  344. package/scripts/ble-config-probe.ts +109 -0
  345. package/scripts/ble-effects-check.ts +82 -0
  346. package/scripts/ble-extended-probe.ts +44 -0
  347. package/scripts/ble-fan-check.ts +129 -0
  348. package/scripts/ble-feature-probe.ts +37 -0
  349. package/scripts/ble-followup-check.ts +165 -0
  350. package/scripts/ble-hardware-check.ts +99 -0
  351. package/scripts/ble-live-extended-check.ts +205 -0
  352. package/scripts/ble-non-ota-check.ts +193 -0
  353. package/scripts/ble-probe.ts +45 -0
  354. package/scripts/calibrate-webcam.ts +152 -0
  355. package/scripts/desktop-reference.ts +85 -0
  356. package/scripts/webcam.ts +129 -0
  357. package/dist/__tests__/lightControl.test.d.ts +0 -2
  358. package/dist/__tests__/lightControl.test.d.ts.map +0 -1
  359. package/dist/__tests__/lightControl.test.js +0 -424
  360. package/dist/__tests__/lightControl.test.js.map +0 -1
  361. package/dist/commands/deviceControl/discover.d.ts.map +0 -1
  362. package/dist/commands/deviceControl/discover.js +0 -29
  363. package/dist/commands/deviceControl/discover.js.map +0 -1
@@ -0,0 +1,109 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import {
3
+ changeSubscription,
4
+ type MeshModel,
5
+ modelBindings,
6
+ readComposition,
7
+ subscriptions,
8
+ } from '../src/ble/configuration.js';
9
+ import { MeshCrypto } from '../src/ble/crypto.js';
10
+ import { desktopDeviceKeys } from '../src/ble/desktop.js';
11
+ import { loadMeshConfig, meshDirectory, SequenceStore } from '../src/ble/storage.js';
12
+ import { brightnessPacket, type FixtureState } from '../src/ble/telink.js';
13
+ import { MeshTransport } from '../src/ble/transport.js';
14
+
15
+ if (!process.argv[2]) throw new Error('Supply the matching Desktop database path; this probe is read-only');
16
+ const config = desktopDeviceKeys(process.argv[2], loadMeshConfig());
17
+ const crypto = new MeshCrypto(config.netKey, config.appKey);
18
+ const sequence = new SequenceStore(meshDirectory(), crypto.networkId.toString('hex'), config.source);
19
+ const link = new MeshTransport(config, sequence, process.argv.includes('--debug'));
20
+ const results: unknown[] = [];
21
+ const capable: { key: string; address: number; model: MeshModel; groups: number[] }[] = [];
22
+ const added: typeof capable = [];
23
+ const original = new Map<number, FixtureState>();
24
+ let discoveryFailed = false;
25
+ try {
26
+ await link.connect();
27
+ for (const light of config.lights) {
28
+ try {
29
+ const data = await readComposition(link, light.address);
30
+ console.log(light.key, data);
31
+ const models = [];
32
+ for (const model of data.models.filter((item) => item.company !== undefined || item.model >= 0x1000)) {
33
+ const bindings = await modelBindings(link, light.address, model);
34
+ if (!bindings.includes(0)) continue;
35
+ const groups = await subscriptions(link, light.address, model);
36
+ models.push({ model, groups, bindings });
37
+ capable.push({ key: light.key, address: light.address, model, groups });
38
+ }
39
+ const phase = await link.configuration(
40
+ light.address,
41
+ Buffer.from([0x80, 0x15, 0, 0]),
42
+ (data) => data.length === 6 && data[0] === 0x80 && data[1] === 0x17
43
+ );
44
+ console.log(light.key, 'subscriptions', models, 'key refresh status', phase.toString('hex'));
45
+ results.push({
46
+ key: light.key,
47
+ composition: data,
48
+ subscriptions: models,
49
+ keyRefreshStatus: phase.toString('hex'),
50
+ });
51
+ } catch (error) {
52
+ discoveryFailed = true;
53
+ console.error(light.key, (error as Error).message);
54
+ results.push({ key: light.key, error: (error as Error).message });
55
+ }
56
+ }
57
+ if (process.argv.includes('--exercise')) {
58
+ const group = 0xce01;
59
+ if (discoveryFailed || !capable.length || capable.some((item) => item.groups.includes(group)))
60
+ throw new Error('Incomplete discovery or test group already in use');
61
+ for (const light of config.lights) {
62
+ const state = await link.readState(light.address);
63
+ if (state.intensity > 50) throw new Error('Use at most 5% output for this check');
64
+ original.set(light.address, state);
65
+ }
66
+ for (const item of capable.filter(
67
+ (item) => item.key !== 'front' && item.model.company === undefined && item.model.model === 0x1000
68
+ )) {
69
+ console.log('Adding temporary subscription', item.key, item.model);
70
+ added.push(item);
71
+ await changeSubscription(link, item.address, item.model, group, false);
72
+ }
73
+ await link.send(group, brightnessPacket(10));
74
+ await new Promise((resolve) => setTimeout(resolve, 500));
75
+ for (const light of config.lights) {
76
+ const state = await link.readState(light.address);
77
+ const expected = light.key === 'front' ? original.get(light.address)?.intensity : 10;
78
+ if (state.intensity !== expected)
79
+ throw new Error(`Native group did not match ${light.key}: ${state.intensity}, expected ${expected}`);
80
+ }
81
+ console.log('Native subset group controlled desk/back without changing front');
82
+ }
83
+ } finally {
84
+ const failures: string[] = [];
85
+ for (const [address, state] of original) {
86
+ try {
87
+ await link.send(address, brightnessPacket(state.intensity));
88
+ const restored = await link.readState(address);
89
+ if (restored.intensity !== state.intensity) failures.push(`Could not restore brightness at ${address}`);
90
+ } catch (error) {
91
+ failures.push(`${address}: ${error}`);
92
+ }
93
+ }
94
+ for (const item of added.reverse()) {
95
+ try {
96
+ await changeSubscription(link, item.address, item.model, 0xce01, true);
97
+ } catch (error) {
98
+ failures.push(`Remove test subscription ${item.address}: ${error}`);
99
+ }
100
+ }
101
+ await link.disconnect();
102
+ sequence.close();
103
+ if (failures.length) {
104
+ console.error(failures.join('\n'));
105
+ process.exitCode = 1;
106
+ }
107
+ writeFileSync(`artifacts/config-probe-${Date.now()}.json`, JSON.stringify(results, null, 2), { mode: 0o600 });
108
+ }
109
+ process.exit(process.exitCode ?? 0);
@@ -0,0 +1,82 @@
1
+ import assert from 'node:assert/strict';
2
+ import { writeFileSync } from 'node:fs';
3
+ import { z } from 'zod';
4
+ import { COLOR_EFFECTS, WHITE_EFFECTS } from '../src/ble/effects.js';
5
+ import { SavedStateSchema } from '../src/ble/library.js';
6
+ import type { FixtureState } from '../src/ble/telink.js';
7
+ import { captureWebcam } from './webcam.js';
8
+
9
+ const base = process.env.AMARAN_BLE_URL ?? 'http://127.0.0.1:2708';
10
+ const run = Date.now();
11
+ const original: Record<string, FixtureState> = {};
12
+ const results: unknown[] = [];
13
+ const errors: string[] = [];
14
+ let changed = false;
15
+ async function request(key: string, action: string, body?: object) {
16
+ const response = await fetch(`${base}/lights/${key}/${action}`, {
17
+ method: body ? 'POST' : 'GET',
18
+ ...(body ? { body: JSON.stringify(body), headers: { 'content-type': 'application/json' } } : {}),
19
+ signal: AbortSignal.timeout(60_000),
20
+ });
21
+ const data = z
22
+ .object({ ok: z.boolean(), result: z.unknown().optional(), error: z.string().optional() })
23
+ .parse(await response.json());
24
+ if (!response.ok || !data.ok) throw new Error(`${key}/${action}: ${data.error ?? response.status}`);
25
+ const state = SavedStateSchema.parse(data.result);
26
+ results.push({ key, action, body, state });
27
+ return state;
28
+ }
29
+ try {
30
+ for (const key of ['desk', 'front', 'back']) original[key] = await request(key, 'state');
31
+ assert.ok(
32
+ Object.values(original).every((state) => state.mode === 'cct'),
33
+ 'Begin with a steady CCT setup'
34
+ );
35
+ changed = true;
36
+ for (const key of Object.keys(original)) await request(key, 'cct', { kelvin: 3200, brightness: 0 });
37
+ captureWebcam(`effects-${run}-dark-before`);
38
+ for (const key of Object.keys(original)) {
39
+ for (const name of key === 'back' ? COLOR_EFFECTS : WHITE_EFFECTS) {
40
+ const state = await request(key, 'effect', { name, brightness: 0, frequency: 1 });
41
+ assert.equal(state.effect, name);
42
+ assert.equal(state.intensity, 0);
43
+ assert.equal((await request(key, 'effect-speed', { value: 2 })).frequency, 2);
44
+ assert.equal((await request(key, 'effect-intensity', { value: 0 })).intensity, 0);
45
+ console.log(`${key}: ${name}, zero output, verified`);
46
+ }
47
+ if (key === 'back') {
48
+ for (const name of ['faulty-bulb', 'pulsing']) {
49
+ const state = await request(key, 'effect', { name, brightness: 0, frequency: 1, hue: 240, saturation: 80 });
50
+ assert.equal(state.hue, 240);
51
+ assert.equal(state.sat, 80);
52
+ assert.equal(state.intensity, 0);
53
+ }
54
+ }
55
+ await request(key, 'effect-stop', {});
56
+ }
57
+ captureWebcam(`effects-${run}-dark-after`);
58
+ } catch (error) {
59
+ errors.push(String(error));
60
+ } finally {
61
+ if (changed)
62
+ for (const [key, state] of Object.entries(original)) {
63
+ try {
64
+ await request(key, 'cct', {
65
+ kelvin: state.cct,
66
+ brightness: state.intensity / 10,
67
+ ...(key === 'back' ? { gm: state.gm } : {}),
68
+ });
69
+ await request(key, state.sleep ? 'off' : 'on', {});
70
+ const restored = await request(key, 'state');
71
+ for (const field of ['cct', 'sleep', 'intensity'] as const) assert.equal(restored[field], state[field]);
72
+ if (key === 'back') assert.equal(restored.gm, state.gm);
73
+ } catch (error) {
74
+ errors.push(`Restore ${key}: ${error}`);
75
+ }
76
+ }
77
+ writeFileSync(`artifacts/effects-${run}-results.json`, JSON.stringify({ original, results, errors }, null, 2), {
78
+ mode: 0o600,
79
+ });
80
+ }
81
+ if (errors.length) throw new Error(errors.join('\n'));
82
+ console.log(`All native effect settings verified at zero output; artifacts/effects-${run}-results.json`);
@@ -0,0 +1,44 @@
1
+ import assert from 'node:assert/strict';
2
+ import { setTimeout as delay } from 'node:timers/promises';
3
+ import { MeshCrypto } from '../src/ble/crypto.js';
4
+ import { loadMeshConfig, meshDirectory, SequenceStore } from '../src/ble/storage.js';
5
+ import { cctPacket, powerPacket } from '../src/ble/telink.js';
6
+ import { MeshTransport } from '../src/ble/transport.js';
7
+ import { captureWebcam } from './webcam.js';
8
+
9
+ const config = loadMeshConfig();
10
+ const crypto = new MeshCrypto(config.netKey, config.appKey);
11
+ const sequence = new SequenceStore(meshDirectory(), crypto.networkId.toString('hex'), config.source);
12
+ const transport = new MeshTransport(config, sequence, true);
13
+ try {
14
+ await transport.connect();
15
+ const back = config.lights.find((light) => light.model === '150c');
16
+ if (!back) throw new Error('150c fixture not found');
17
+ const original = await transport.readState(back.address);
18
+ if (process.argv[2] === 'gm') {
19
+ try {
20
+ for (const gm of [-30, 0, 30]) {
21
+ await transport.send(back.address, cctPacket(3200, 10, gm));
22
+ await delay(300);
23
+ const state = await transport.readState(back.address);
24
+ console.log('G/M applied', state);
25
+ assert.equal(state.gm, gm);
26
+ assert.equal(state.cct, 3200);
27
+ assert.equal(state.intensity, 10);
28
+ console.log(captureWebcam(`gm-${gm < 0 ? 'minus' : 'plus'}-${Math.abs(gm)}-${Date.now()}`));
29
+ }
30
+ } finally {
31
+ await transport.send(back.address, cctPacket(original.cct ?? 3200, original.intensity, 0));
32
+ await delay(300);
33
+ await transport.send(back.address, powerPacket(!original.sleep));
34
+ console.log('Restored with corrected neutral tint', await transport.readState(back.address));
35
+ }
36
+ } else {
37
+ console.log(original);
38
+ for (const light of config.lights) console.log(light.key, await transport.readFan(light.address));
39
+ }
40
+ } finally {
41
+ await transport.disconnect();
42
+ sequence.close();
43
+ }
44
+ process.exit(0);
@@ -0,0 +1,129 @@
1
+ import assert from 'node:assert/strict';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { mkdirSync, writeFileSync } from 'node:fs';
4
+ import { z } from 'zod';
5
+ import { FanStateSchema, FanStatesSchema } from '../src/ble/fan.js';
6
+ import { SavedStateSchema } from '../src/ble/library.js';
7
+ import { FAN_MODES, parseFanMode } from '../src/ble/telink.js';
8
+
9
+ const base = process.env.AMARAN_BLE_URL ?? 'http://127.0.0.1:2708';
10
+ const run = Date.now();
11
+ const records: unknown[] = [];
12
+ const failures: string[] = [];
13
+ let original: z.infer<typeof FanStatesSchema> | undefined;
14
+ let group: string | undefined;
15
+ let changed = false;
16
+ const lights: Record<string, z.infer<typeof SavedStateSchema>> = {};
17
+
18
+ async function request(route: string, body?: object, method = body ? 'POST' : 'GET') {
19
+ const response = await fetch(`${base}${route}`, {
20
+ method,
21
+ headers: { connection: 'close', ...(body ? { 'content-type': 'application/json' } : {}) },
22
+ ...(body ? { body: JSON.stringify(body) } : {}),
23
+ signal: AbortSignal.timeout(60_000),
24
+ });
25
+ const data = z
26
+ .object({ ok: z.boolean(), result: z.unknown().optional(), error: z.string().optional() })
27
+ .parse(await response.json());
28
+ if (!response.ok || !data.ok) throw new Error(`${route}: ${data.error ?? response.status}`);
29
+ records.push({ route, body, result: data.result });
30
+ return data.result;
31
+ }
32
+ function cli(...args: string[]) {
33
+ const output = execFileSync(
34
+ process.execPath,
35
+ ['dist/cli.js', 'fan', ...args, '--backend', 'ble', '--url', base, '--json'],
36
+ {
37
+ encoding: 'utf8',
38
+ timeout: 60_000,
39
+ }
40
+ );
41
+ const data = FanStatesSchema.parse(JSON.parse(output));
42
+ records.push({ cli: args, result: data });
43
+ return data;
44
+ }
45
+ async function checkLighting() {
46
+ for (const [key, before] of Object.entries(lights)) {
47
+ const after = SavedStateSchema.parse(await request(`/lights/${key}/state`));
48
+ const { observedAt: _beforeTime, ...beforeSettings } = before;
49
+ const { observedAt: _afterTime, ...afterSettings } = after;
50
+ assert.deepEqual(afterSettings, beforeSettings, `${key}: fan control changed lighting`);
51
+ }
52
+ }
53
+
54
+ try {
55
+ original = FanStatesSchema.parse(await request('/fans'));
56
+ const keys = Object.keys(original.states);
57
+ for (const [key, state] of Object.entries(original.states)) {
58
+ parseFanMode(state.mode);
59
+ assert.notEqual(state.mode, FAN_MODES.manual, 'Cannot infer an original manual RPM setpoint for restoration');
60
+ assert.equal(state.highTemperature, false);
61
+ assert.ok(state.supported.smart && state.supported.medium);
62
+ lights[key] = SavedStateSchema.parse(await request(`/lights/${key}/state`));
63
+ assert.ok(
64
+ lights[key].intensity <= 50,
65
+ 'Run fan checks at 5% brightness or less; do not stress-test thermal protection'
66
+ );
67
+ }
68
+ changed = true;
69
+ for (const state of Object.values(cli('mode', 'all', 'smart').states)) assert.equal(state.mode, 1);
70
+ for (const key of keys) {
71
+ const selected = cli('mode', key, 'medium').states[key];
72
+ assert.equal(selected.mode, 5);
73
+ assert.equal(selected.highTemperature, false);
74
+ assert.equal(selected.rpmStatus, selected.speed === 0 ? 'zero-reported' : 'rotation-reported');
75
+ assert.equal(FanStateSchema.parse(await request(`/lights/${key}/fan`)).mode, 5);
76
+ }
77
+ const savedGroup = z.object({ id: z.string() }).parse(await request('/groups', { name: `Fan verification ${run}` }));
78
+ group = savedGroup.id;
79
+ const members = [...new Set([keys[0], keys[keys.length - 1]])];
80
+ for (const member of members) await request(`/groups/${encodeURIComponent(group)}/members`, { member });
81
+ const grouped = cli('mode', group, 'smart');
82
+ assert.deepEqual(Object.keys(grouped.states).sort(), [...members].sort());
83
+ for (const state of Object.values(grouped.states)) assert.equal(state.mode, 1);
84
+ const deduplicated = FanStatesSchema.parse(await request('/fans', { targets: [group, members[0]], mode: 'medium' }));
85
+ assert.equal(Object.keys(deduplicated.states).length, members.length);
86
+ assert.equal(Object.keys(cli('info', group).states).length, members.length);
87
+ for (const mode of ['off', 'silent', 'low', 'manual']) {
88
+ await assert.rejects(
89
+ request('/fans', { targets: 'all', mode, ...(mode === 'manual' ? { rpm: 2200 } : {}) }),
90
+ /does not advertise/
91
+ );
92
+ }
93
+ await checkLighting();
94
+ if (process.argv.includes('--restart-service')) {
95
+ for (const action of ['stop', 'start'])
96
+ execFileSync(process.execPath, ['dist/cli.js', 'ble', 'service', action], { stdio: 'inherit', timeout: 100_000 });
97
+ for (const state of Object.values(cli('info').states))
98
+ assert.equal(state.mode, 5, 'Fan profile was reset on daemon restart');
99
+ await checkLighting();
100
+ }
101
+ } catch (error) {
102
+ failures.push(String(error));
103
+ } finally {
104
+ if (changed && original)
105
+ for (const [key, state] of Object.entries(original.states)) {
106
+ try {
107
+ const restored = FanStateSchema.parse(await request(`/lights/${key}/fan`, { mode: state.mode }));
108
+ assert.equal(restored.mode, state.mode);
109
+ assert.equal(restored.highTemperature, false);
110
+ } catch (error) {
111
+ failures.push(`Restore ${key}: ${error}`);
112
+ }
113
+ }
114
+ if (group) {
115
+ try {
116
+ await request(`/groups/${encodeURIComponent(group)}`, undefined, 'DELETE');
117
+ } catch (error) {
118
+ failures.push(`Group cleanup: ${error}`);
119
+ }
120
+ }
121
+ mkdirSync('artifacts/fan-safety', { recursive: true });
122
+ writeFileSync(
123
+ `artifacts/fan-safety/live-${run}.json`,
124
+ JSON.stringify({ original, lights, records, failures }, null, 2),
125
+ { mode: 0o600 }
126
+ );
127
+ }
128
+ if (failures.length) throw new Error(failures.join('\n'));
129
+ console.log(`Fan profiles, CLI groups/all, telemetry and restoration passed: artifacts/fan-safety/live-${run}.json`);
@@ -0,0 +1,37 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import { MeshCrypto } from '../src/ble/crypto.js';
3
+ import { packet } from '../src/ble/packets.js';
4
+ import { decodeProductInfo } from '../src/ble/settings.js';
5
+ import { loadMeshConfig, meshDirectory, SequenceStore } from '../src/ble/storage.js';
6
+ import { MeshTransport } from '../src/ble/transport.js';
7
+
8
+ const config = loadMeshConfig();
9
+ const crypto = new MeshCrypto(config.netKey, config.appKey);
10
+ const sequence = new SequenceStore(meshDirectory(), crypto.networkId.toString('hex'), config.source);
11
+ const link = new MeshTransport(config, sequence);
12
+ const results: unknown[] = [];
13
+ try {
14
+ await link.connect();
15
+ for (const light of config.lights) {
16
+ const info = await link.readPacket(light.address, packet(0), decodeProductInfo);
17
+ console.log(light.key, info);
18
+ let dimming: unknown;
19
+ try {
20
+ dimming = await link.readPacket(light.address, packet(8), (data) => {
21
+ if (data.length !== 10 || (data[9] & 127) !== 8) return undefined;
22
+ if ((data.subarray(1).reduce((sum, byte) => sum + byte, 0) & 255) !== data[0])
23
+ throw new Error('Invalid curve checksum');
24
+ return { mode: data[8], raw: data.toString('hex') };
25
+ });
26
+ } catch (error) {
27
+ dimming = { unavailable: (error as Error).message };
28
+ }
29
+ console.log(light.key, 'dimming query', dimming);
30
+ results.push({ key: light.key, info, dimming });
31
+ }
32
+ } finally {
33
+ await link.disconnect();
34
+ sequence.close();
35
+ writeFileSync(`artifacts/feature-probe-${Date.now()}.json`, JSON.stringify(results, null, 2), { mode: 0o600 });
36
+ }
37
+ process.exit(0);
@@ -0,0 +1,165 @@
1
+ import assert from 'node:assert/strict';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { readFileSync, writeFileSync } from 'node:fs';
4
+ import { z } from 'zod';
5
+ import { SavedStateSchema } from '../src/ble/library.js';
6
+ import { FAN_MODES } from '../src/ble/telink.js';
7
+ import { captureWebcam } from './webcam.js';
8
+
9
+ const base = process.env.AMARAN_BLE_URL ?? 'http://127.0.0.1:2708';
10
+ const run = Date.now();
11
+ const records: unknown[] = [];
12
+ const failures: string[] = [];
13
+ const cleanup: string[] = [];
14
+ let baseline: { id: string; states: Record<string, z.infer<typeof SavedStateSchema>> } | undefined;
15
+ let group: string | undefined;
16
+ let priorOverrides: Record<string, number> = {};
17
+
18
+ async function request(route: string, body?: object, method = body ? 'POST' : 'GET') {
19
+ const response = await fetch(`${base}${route}`, {
20
+ method,
21
+ headers: { connection: 'close', ...(body ? { 'content-type': 'application/json' } : {}) },
22
+ ...(body ? { body: JSON.stringify(body) } : {}),
23
+ signal: AbortSignal.timeout(60_000),
24
+ });
25
+ const data = z
26
+ .object({ ok: z.boolean(), result: z.unknown().optional(), error: z.string().optional() })
27
+ .parse(await response.json());
28
+ if (!response.ok || !data.ok) throw new Error(`${route}: ${data.error ?? response.status}`);
29
+ records.push({ route, body, result: data.result });
30
+ return data.result;
31
+ }
32
+ function cli(...args: string[]) {
33
+ const output = execFileSync(process.execPath, ['dist/cli.js', ...args, '--backend', 'ble', '--url', base], {
34
+ encoding: 'utf8',
35
+ timeout: 60_000,
36
+ });
37
+ records.push({ cli: args, output });
38
+ return output;
39
+ }
40
+ async function state(key: string) {
41
+ return SavedStateSchema.parse(await request(`/lights/${key}/state`));
42
+ }
43
+ const entry = z.object({ id: z.string() });
44
+ function image(label: string) {
45
+ const filename = captureWebcam(`followup-${run}-${label}`);
46
+ const report = z
47
+ .object({
48
+ regions: z.array(
49
+ z.object({ name: z.string(), clippedFraction: z.number(), haloRgb: z.array(z.number()).optional() })
50
+ ),
51
+ })
52
+ .parse(JSON.parse(readFileSync(filename.replace(/\.png$/, '.json'), 'utf8')));
53
+ records.push({ camera: filename, regions: report.regions });
54
+ return report.regions;
55
+ }
56
+
57
+ try {
58
+ const remaining = z.record(z.number()).parse(await request('/overrides', { targets: 'all' }));
59
+ priorOverrides = Object.fromEntries(Object.entries(remaining).map(([key, ms]) => [key, ms ? Date.now() + ms : 0]));
60
+ for (const key of ['desk', 'front', 'back']) {
61
+ const before = await state(key);
62
+ assert.equal(before.mode, 'cct', 'Start these checks in steady CCT mode');
63
+ assert.ok(!before.sleep && before.intensity >= 10 && before.intensity <= 50, 'Begin with all fixtures on at 1-5%');
64
+ }
65
+ const regions = image('baseline');
66
+ for (const key of ['desk', 'front', 'back']) {
67
+ const region = regions.find((item) => item.name === key);
68
+ assert.ok(
69
+ region && region.clippedFraction > 0.25,
70
+ `${key}: camera region is not on the emitter; recalibrate before testing`
71
+ );
72
+ }
73
+ baseline = z
74
+ .object({ id: z.string(), states: z.record(SavedStateSchema) })
75
+ .parse(await request('/library/scenes', { name: `Followup baseline ${run}` }));
76
+ for (const key of Object.keys(baseline.states)) {
77
+ const info = z
78
+ .object({ cctMin: z.number(), cctMax: z.number(), protocolVersion: z.number() })
79
+ .parse(await request(`/lights/${key}/info`));
80
+ assert.equal(info.cctMin, key === 'back' ? 2500 : 2700);
81
+ }
82
+ group = entry.parse(await request('/groups', { name: `Followup ${run}` })).id;
83
+ for (const member of ['desk', 'back']) await request(`/groups/${encodeURIComponent(group)}/members`, { member });
84
+ cli('group', 'rename', group, `Followup renamed ${run}`);
85
+ assert.equal(
86
+ z.object({ name: z.string() }).parse(await request(`/groups/${encodeURIComponent(group)}`)).name,
87
+ `Followup renamed ${run}`
88
+ );
89
+ cli('ble', 'transition', 'cct', '2', '--targets', 'all', '--args', '{"kelvin":3200,"brightness":1}');
90
+ for (const key of ['desk', 'front', 'back']) assert.equal((await state(key)).cct, 3200);
91
+ cli('ble', 'transition', 'hsi', '2', '--targets', 'back', '--args', '{"hue":120,"saturation":100,"brightness":1}');
92
+ const green = image('green').find((region) => region.name === 'back')?.haloRgb;
93
+ assert.ok(green && green[1] - Math.max(green[0], green[2]) > 20);
94
+ assert.deepEqual(await request('/lights/back/auto-cct', { kelvin: 5600, brightness: 1 }), {
95
+ skipped: true,
96
+ reason: 'manual-override',
97
+ });
98
+ assert.equal((await state('back')).mode, 'hsi');
99
+ for (const key of ['desk', 'back'])
100
+ await request(`/lights/${key}/effect`, { name: 'pulsing', brightness: 0, frequency: 1 });
101
+ const triggered = z
102
+ .object({ triggerRequest: z.object({ sent: z.literal(true), eventConfirmed: z.literal(false) }) })
103
+ .parse(await request(`/lights/${encodeURIComponent(group)}/effect-trigger`, {}));
104
+ assert.equal(triggered.triggerRequest.eventConfirmed, false);
105
+ cli('effect', 'stop', group);
106
+ assert.equal((await state('desk')).mode, 'cct');
107
+ assert.equal((await state('back')).mode, 'hsi');
108
+ const preset = entry.parse(await request('/library/presets', { name: `Preset ${run}`, keys: ['back'] }));
109
+ cleanup.push(`/library/presets/${preset.id}`);
110
+ cli('preset', 'update', preset.id, '--name', `Preset updated ${run}`);
111
+ assert.equal(
112
+ z.object({ name: z.string() }).parse(await request(`/library/presets/${preset.id}`)).name,
113
+ `Preset updated ${run}`
114
+ );
115
+ const quickshot = entry.parse(await request('/library/quickshots', { name: `Quickshot ${run}` }));
116
+ cleanup.push(`/library/quickshots/${quickshot.id}`);
117
+ cli('quickshot', 'update', quickshot.id, '--name', `Quickshot updated ${run}`);
118
+ await request('/lights/back/fan', { mode: 'medium' });
119
+ cli('scene', 'recall', baseline.id, '--fade', '2');
120
+ for (const [key, expected] of Object.entries(baseline.states)) {
121
+ const actual = await state(key);
122
+ for (const field of ['mode', 'cct', 'intensity', 'sleep'] as const) assert.equal(actual[field], expected[field]);
123
+ if (expected.fan)
124
+ assert.equal(
125
+ z.object({ mode: z.number() }).parse(await request(`/lights/${key}/fan`)).mode,
126
+ FAN_MODES[expected.fan.mode]
127
+ );
128
+ }
129
+ } catch (error) {
130
+ failures.push(String(error));
131
+ } finally {
132
+ if (baseline) {
133
+ try {
134
+ await request(`/library/scenes/${baseline.id}/recall`, {});
135
+ } catch (error) {
136
+ failures.push(`Restore baseline: ${error}`);
137
+ }
138
+ cleanup.push(`/library/scenes/${baseline.id}`);
139
+ }
140
+ if (group) cleanup.push(`/groups/${encodeURIComponent(group)}`);
141
+ for (const route of cleanup) {
142
+ try {
143
+ await request(route, undefined, 'DELETE');
144
+ } catch (error) {
145
+ failures.push(`Cleanup ${route}: ${error}`);
146
+ }
147
+ }
148
+ if (!failures.length)
149
+ for (const [key, expires] of Object.entries(priorOverrides)) {
150
+ try {
151
+ await request('/overrides', { targets: [key], minutes: Math.max(0, (expires - Date.now()) / 60_000) });
152
+ } catch (error) {
153
+ failures.push(`Restore override ${key}: ${error}`);
154
+ }
155
+ }
156
+ writeFileSync(
157
+ `artifacts/followup-${run}-results.json`,
158
+ JSON.stringify({ baseline, priorOverrides, records, failures }, null, 2),
159
+ { mode: 0o600 }
160
+ );
161
+ }
162
+ if (failures.length) throw new Error(failures.join('\n'));
163
+ console.log(
164
+ `Supported-control checks passed; lighting, fan profiles and overrides restored. artifacts/followup-${run}-results.json`
165
+ );
@@ -0,0 +1,99 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
+ import { setTimeout as delay } from 'node:timers/promises';
3
+ import { captureWebcam } from './webcam.js';
4
+
5
+ const base = process.env.AMARAN_BLE_URL ?? 'http://127.0.0.1:2708';
6
+ const run = Date.now();
7
+ const results: object[] = [];
8
+ const positions: Record<string, string> = { desk: 'desk', front: 'front', back: 'back' };
9
+
10
+ async function request(key: string, action: string, body?: object) {
11
+ const response = await fetch(`${base}/lights/${encodeURIComponent(key)}/${action}`, {
12
+ method: body ? 'POST' : 'GET',
13
+ headers: body ? { 'content-type': 'application/json' } : undefined,
14
+ body: body ? JSON.stringify(body) : undefined,
15
+ signal: AbortSignal.timeout(60_000),
16
+ });
17
+ const data = await response.json();
18
+ if (!response.ok || !data.ok) throw new Error(`${key} ${action}: ${data.error ?? response.status}`);
19
+ return data.result;
20
+ }
21
+
22
+ function image(label: string, key: string) {
23
+ const filename = captureWebcam(`ble-check-${run}-${label}`);
24
+ const metrics = JSON.parse(readFileSync(filename.replace(/\.png$/, '.json'), 'utf8'));
25
+ const region = metrics.regions.find((entry: { name: string }) => entry.name === positions[key]);
26
+ if (!region) throw new Error(`No camera ROI for ${key}`);
27
+ return {
28
+ filename,
29
+ rgb: region.rgb as number[],
30
+ haloRgb: region.haloRgb as number[] | undefined,
31
+ clippedFraction: region.clippedFraction as number,
32
+ };
33
+ }
34
+
35
+ const initial: Record<string, unknown> = {};
36
+ const errors: unknown[] = [];
37
+ try {
38
+ for (const key of Object.keys(positions)) initial[key] = await request(key, 'state');
39
+ writeFileSync(`artifacts/ble-check-${run}-initial.json`, JSON.stringify(initial, null, 2), { mode: 0o600 });
40
+ for (const key of Object.keys(positions)) await request(key, 'off', {});
41
+ for (const key of Object.keys(positions)) {
42
+ const dark = image(`${key}-off`, key);
43
+ await request(key, 'cct', { kelvin: 3200, brightness: 1 });
44
+ await request(key, 'on', {});
45
+ for (const kelvin of [3200, 6500]) {
46
+ const state = await request(key, 'cct', { kelvin, brightness: 1 });
47
+ await delay(500);
48
+ const optical = image(`${key}-${kelvin}-1pct`, key);
49
+ const difference = optical.rgb.reduce((sum, value, i) => sum + value - dark.rgb[i], 0) / 3;
50
+ if (difference < 40) throw new Error(`${key}: camera did not confirm illumination (difference ${difference})`);
51
+ results.push({ key, action: 'cct', requested: kelvin, state, optical, opticalOnDifference: difference });
52
+ }
53
+ for (const value of [0, 1, 2]) {
54
+ const state = await request(key, 'brightness', { value });
55
+ results.push({
56
+ key,
57
+ action: 'brightness',
58
+ requested: value,
59
+ state,
60
+ optical: image(`${key}-${value}pct`, key),
61
+ });
62
+ }
63
+ const state = await request(key, 'off', {});
64
+ results.push({ key, action: 'off', state, optical: image(`${key}-off-after`, key) });
65
+ }
66
+ await request('back', 'on', {});
67
+ for (const [hue, channel] of [
68
+ [0, 0],
69
+ [120, 1],
70
+ [240, 2],
71
+ ]) {
72
+ const state = await request('back', 'hsi', { hue, saturation: 100, brightness: 1 });
73
+ await delay(500);
74
+ const optical = image(`back-hue-${hue}`, 'back');
75
+ const color = optical.haloRgb;
76
+ if (!color || color[channel] - Math.max(...color.filter((_, i) => i !== channel)) < 20)
77
+ throw new Error(`Camera did not confirm hue ${hue}: halo RGB ${color}`);
78
+ results.push({ key: 'back', action: 'hsi', requested: hue, state, optical });
79
+ }
80
+ } catch (error) {
81
+ errors.push(error);
82
+ } finally {
83
+ const failures: string[] = [];
84
+ for (const key of Object.keys(positions)) {
85
+ try {
86
+ await request(key, 'off', {});
87
+ } catch (error) {
88
+ failures.push(`${key}: ${(error as Error).message}`);
89
+ }
90
+ }
91
+ writeFileSync(
92
+ `artifacts/ble-check-${run}-results.json`,
93
+ JSON.stringify({ results, errors: errors.map(String), shutdownFailures: failures }, null, 2),
94
+ { mode: 0o600 }
95
+ );
96
+ if (failures.length) errors.push(new Error(`Could not confirm lights off: ${failures.join('; ')}`));
97
+ }
98
+ if (errors.length) throw new AggregateError(errors, 'Hardware verification failed');
99
+ console.log(`Hardware feedback checks completed: ${results.length} verified states with local images.`);