amaran-light-cli 1.10.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.
- package/README.md +154 -17
- package/dist/__tests__/autoCct.test.js +35 -4
- package/dist/__tests__/autoCct.test.js.map +1 -1
- package/dist/__tests__/autoCctMaxLux.test.js +11 -9
- package/dist/__tests__/autoCctMaxLux.test.js.map +1 -1
- package/dist/__tests__/autostart.test.js +18 -0
- package/dist/__tests__/autostart.test.js.map +1 -1
- package/dist/__tests__/bleHttpControl.test.d.ts +2 -0
- package/dist/__tests__/bleHttpControl.test.d.ts.map +1 -0
- package/dist/__tests__/bleHttpControl.test.js +230 -0
- package/dist/__tests__/bleHttpControl.test.js.map +1 -0
- package/dist/__tests__/bleStack.test.d.ts +2 -0
- package/dist/__tests__/bleStack.test.d.ts.map +1 -0
- package/dist/__tests__/bleStack.test.js +1239 -0
- package/dist/__tests__/bleStack.test.js.map +1 -0
- package/dist/__tests__/cctUtil.test.js +11 -5
- package/dist/__tests__/cctUtil.test.js.map +1 -1
- package/dist/__tests__/circadianDashboardSettings.test.d.ts +2 -0
- package/dist/__tests__/circadianDashboardSettings.test.d.ts.map +1 -0
- package/dist/__tests__/circadianDashboardSettings.test.js +82 -0
- package/dist/__tests__/circadianDashboardSettings.test.js.map +1 -0
- package/dist/__tests__/circadianIsolation.test.d.ts +2 -0
- package/dist/__tests__/circadianIsolation.test.d.ts.map +1 -0
- package/dist/__tests__/circadianIsolation.test.js +30 -0
- package/dist/__tests__/circadianIsolation.test.js.map +1 -0
- package/dist/__tests__/cli.config.test.d.ts +2 -0
- package/dist/__tests__/cli.config.test.d.ts.map +1 -0
- package/dist/__tests__/cli.config.test.js +114 -0
- package/dist/__tests__/cli.config.test.js.map +1 -0
- package/dist/__tests__/cli.smoke.test.js +70 -17
- package/dist/__tests__/cli.smoke.test.js.map +1 -1
- package/dist/__tests__/currentCct.test.d.ts +2 -0
- package/dist/__tests__/currentCct.test.d.ts.map +1 -0
- package/dist/__tests__/currentCct.test.js +29 -0
- package/dist/__tests__/currentCct.test.js.map +1 -0
- package/dist/__tests__/dashboard.test.d.ts +2 -0
- package/dist/__tests__/dashboard.test.d.ts.map +1 -0
- package/dist/__tests__/dashboard.test.js +126 -0
- package/dist/__tests__/dashboard.test.js.map +1 -0
- package/dist/__tests__/daylightSimulationParseUtils.test.d.ts +2 -0
- package/dist/__tests__/daylightSimulationParseUtils.test.d.ts.map +1 -0
- package/dist/__tests__/daylightSimulationParseUtils.test.js +16 -0
- package/dist/__tests__/daylightSimulationParseUtils.test.js.map +1 -0
- package/dist/__tests__/desktopImport.test.d.ts +2 -0
- package/dist/__tests__/desktopImport.test.d.ts.map +1 -0
- package/dist/__tests__/desktopImport.test.js +234 -0
- package/dist/__tests__/desktopImport.test.js.map +1 -0
- package/dist/__tests__/lightControl.basic.test.d.ts +2 -0
- package/dist/__tests__/lightControl.basic.test.d.ts.map +1 -0
- package/dist/__tests__/lightControl.basic.test.js +119 -0
- package/dist/__tests__/lightControl.basic.test.js.map +1 -0
- package/dist/__tests__/lightControl.colorEffects.test.d.ts +2 -0
- package/dist/__tests__/lightControl.colorEffects.test.d.ts.map +1 -0
- package/dist/__tests__/lightControl.colorEffects.test.js +170 -0
- package/dist/__tests__/lightControl.colorEffects.test.js.map +1 -0
- package/dist/__tests__/lightControl.management.test.d.ts +2 -0
- package/dist/__tests__/lightControl.management.test.d.ts.map +1 -0
- package/dist/__tests__/lightControl.management.test.js +90 -0
- package/dist/__tests__/lightControl.management.test.js.map +1 -0
- package/dist/__tests__/lightControlTestUtils.d.ts +10 -0
- package/dist/__tests__/lightControlTestUtils.d.ts.map +1 -0
- package/dist/__tests__/lightControlTestUtils.js +57 -0
- package/dist/__tests__/lightControlTestUtils.js.map +1 -0
- package/dist/__tests__/maxLuxInterpolation.test.js +4 -2
- package/dist/__tests__/maxLuxInterpolation.test.js.map +1 -1
- package/dist/__tests__/meshConfiguration.test.d.ts +2 -0
- package/dist/__tests__/meshConfiguration.test.d.ts.map +1 -0
- package/dist/__tests__/meshConfiguration.test.js +78 -0
- package/dist/__tests__/meshConfiguration.test.js.map +1 -0
- package/dist/__tests__/nativeGroups.test.d.ts +2 -0
- package/dist/__tests__/nativeGroups.test.d.ts.map +1 -0
- package/dist/__tests__/nativeGroups.test.js +104 -0
- package/dist/__tests__/nativeGroups.test.js.map +1 -0
- package/dist/__tests__/packageExports.test.d.ts +2 -0
- package/dist/__tests__/packageExports.test.d.ts.map +1 -0
- package/dist/__tests__/packageExports.test.js +33 -0
- package/dist/__tests__/packageExports.test.js.map +1 -0
- package/dist/__tests__/programApi.test.d.ts +2 -0
- package/dist/__tests__/programApi.test.d.ts.map +1 -0
- package/dist/__tests__/programApi.test.js +73 -0
- package/dist/__tests__/programApi.test.js.map +1 -0
- package/dist/__tests__/programs.test.d.ts +2 -0
- package/dist/__tests__/programs.test.d.ts.map +1 -0
- package/dist/__tests__/programs.test.js +176 -0
- package/dist/__tests__/programs.test.js.map +1 -0
- package/dist/__tests__/scheduleMaker.test.d.ts +2 -0
- package/dist/__tests__/scheduleMaker.test.d.ts.map +1 -0
- package/dist/__tests__/scheduleMaker.test.js +25 -0
- package/dist/__tests__/scheduleMaker.test.js.map +1 -0
- package/dist/__tests__/simulateSchedule.test.js +7 -3
- package/dist/__tests__/simulateSchedule.test.js.map +1 -1
- package/dist/ble/async.d.ts +2 -0
- package/dist/ble/async.d.ts.map +1 -0
- package/dist/ble/async.js +15 -0
- package/dist/ble/async.js.map +1 -0
- package/dist/ble/colors.d.ts +5 -0
- package/dist/ble/colors.d.ts.map +1 -0
- package/dist/ble/colors.js +44 -0
- package/dist/ble/colors.js.map +1 -0
- package/dist/ble/configuration.d.ts +22 -0
- package/dist/ble/configuration.d.ts.map +1 -0
- package/dist/ble/configuration.js +122 -0
- package/dist/ble/configuration.js.map +1 -0
- package/dist/ble/controller.d.ts +118 -0
- package/dist/ble/controller.d.ts.map +1 -0
- package/dist/ble/controller.js +931 -0
- package/dist/ble/controller.js.map +1 -0
- package/dist/ble/crypto.d.ts +34 -0
- package/dist/ble/crypto.d.ts.map +1 -0
- package/dist/ble/crypto.js +178 -0
- package/dist/ble/crypto.js.map +1 -0
- package/dist/ble/daemon.d.ts +2 -0
- package/dist/ble/daemon.d.ts.map +1 -0
- package/dist/ble/daemon.js +76 -0
- package/dist/ble/daemon.js.map +1 -0
- package/dist/ble/dashboard.d.ts +331 -0
- package/dist/ble/dashboard.d.ts.map +1 -0
- package/dist/ble/dashboard.js +280 -0
- package/dist/ble/dashboard.js.map +1 -0
- package/dist/ble/desktop.d.ts +4 -0
- package/dist/ble/desktop.d.ts.map +1 -0
- package/dist/ble/desktop.js +33 -0
- package/dist/ble/desktop.js.map +1 -0
- package/dist/ble/desktopLibrary.d.ts +12 -0
- package/dist/ble/desktopLibrary.d.ts.map +1 -0
- package/dist/ble/desktopLibrary.js +346 -0
- package/dist/ble/desktopLibrary.js.map +1 -0
- package/dist/ble/effects.d.ts +34 -0
- package/dist/ble/effects.d.ts.map +1 -0
- package/dist/ble/effects.js +138 -0
- package/dist/ble/effects.js.map +1 -0
- package/dist/ble/fan.d.ts +244 -0
- package/dist/ble/fan.d.ts.map +1 -0
- package/dist/ble/fan.js +46 -0
- package/dist/ble/fan.js.map +1 -0
- package/dist/ble/library.d.ts +341 -0
- package/dist/ble/library.d.ts.map +1 -0
- package/dist/ble/library.js +201 -0
- package/dist/ble/library.js.map +1 -0
- package/dist/ble/media.d.ts +133 -0
- package/dist/ble/media.d.ts.map +1 -0
- package/dist/ble/media.js +186 -0
- package/dist/ble/media.js.map +1 -0
- package/dist/ble/nativeGroups.d.ts +22 -0
- package/dist/ble/nativeGroups.d.ts.map +1 -0
- package/dist/ble/nativeGroups.js +114 -0
- package/dist/ble/nativeGroups.js.map +1 -0
- package/dist/ble/packets.d.ts +3 -0
- package/dist/ble/packets.d.ts.map +1 -0
- package/dist/ble/packets.js +15 -0
- package/dist/ble/packets.js.map +1 -0
- package/dist/ble/programs.d.ts +318 -0
- package/dist/ble/programs.d.ts.map +1 -0
- package/dist/ble/programs.js +329 -0
- package/dist/ble/programs.js.map +1 -0
- package/dist/ble/provisioning.d.ts +13 -0
- package/dist/ble/provisioning.d.ts.map +1 -0
- package/dist/ble/provisioning.js +39 -0
- package/dist/ble/provisioning.js.map +1 -0
- package/dist/ble/server.d.ts +18 -0
- package/dist/ble/server.d.ts.map +1 -0
- package/dist/ble/server.js +548 -0
- package/dist/ble/server.js.map +1 -0
- package/dist/ble/service.d.ts +2 -0
- package/dist/ble/service.d.ts.map +1 -0
- package/dist/ble/service.js +77 -0
- package/dist/ble/service.js.map +1 -0
- package/dist/ble/settings.d.ts +18 -0
- package/dist/ble/settings.d.ts.map +1 -0
- package/dist/ble/settings.js +25 -0
- package/dist/ble/settings.js.map +1 -0
- package/dist/ble/setup.d.ts +2 -0
- package/dist/ble/setup.d.ts.map +1 -0
- package/dist/ble/setup.js +31 -0
- package/dist/ble/setup.js.map +1 -0
- package/dist/ble/storage.d.ts +94 -0
- package/dist/ble/storage.d.ts.map +1 -0
- package/dist/ble/storage.js +132 -0
- package/dist/ble/storage.js.map +1 -0
- package/dist/ble/telink.d.ts +63 -0
- package/dist/ble/telink.d.ts.map +1 -0
- package/dist/ble/telink.js +108 -0
- package/dist/ble/telink.js.map +1 -0
- package/dist/ble/transitions.d.ts +3 -0
- package/dist/ble/transitions.d.ts.map +1 -0
- package/dist/ble/transitions.js +28 -0
- package/dist/ble/transitions.js.map +1 -0
- package/dist/ble/transport.d.ts +48 -0
- package/dist/ble/transport.d.ts.map +1 -0
- package/dist/ble/transport.js +370 -0
- package/dist/ble/transport.js.map +1 -0
- package/dist/circadian.d.ts +19 -0
- package/dist/circadian.d.ts.map +1 -0
- package/dist/circadian.js +13 -0
- package/dist/circadian.js.map +1 -0
- package/dist/cli.d.ts +5 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +159 -33
- package/dist/cli.js.map +1 -1
- package/dist/commands/__tests__/integration.test.js +28 -0
- package/dist/commands/__tests__/integration.test.js.map +1 -1
- package/dist/commands/__tests__/newCommands.test.js +3 -4
- package/dist/commands/__tests__/newCommands.test.js.map +1 -1
- package/dist/commands/cmdUtils.d.ts +8 -2
- package/dist/commands/cmdUtils.d.ts.map +1 -1
- package/dist/commands/cmdUtils.js +40 -2
- package/dist/commands/cmdUtils.js.map +1 -1
- package/dist/commands/daylightSimulation/autoCct.d.ts.map +1 -1
- package/dist/commands/daylightSimulation/autoCct.js +74 -172
- package/dist/commands/daylightSimulation/autoCct.js.map +1 -1
- package/dist/commands/daylightSimulation/graphSchedule.d.ts +1 -1
- package/dist/commands/daylightSimulation/graphSchedule.d.ts.map +1 -1
- package/dist/commands/daylightSimulation/graphSchedule.js +2 -0
- package/dist/commands/daylightSimulation/graphSchedule.js.map +1 -1
- package/dist/commands/daylightSimulation/printSchedule.d.ts +1 -1
- package/dist/commands/daylightSimulation/printSchedule.d.ts.map +1 -1
- package/dist/commands/daylightSimulation/printSchedule.js +4 -2
- package/dist/commands/daylightSimulation/printSchedule.js.map +1 -1
- package/dist/commands/daylightSimulation/service.d.ts +1 -1
- package/dist/commands/daylightSimulation/service.d.ts.map +1 -1
- package/dist/commands/daylightSimulation/service.js +20 -10
- package/dist/commands/daylightSimulation/service.js.map +1 -1
- package/dist/commands/daylightSimulation/simulateSchedule.d.ts +1 -1
- package/dist/commands/daylightSimulation/simulateSchedule.d.ts.map +1 -1
- package/dist/commands/daylightSimulation/simulateSchedule.js +31 -13
- package/dist/commands/daylightSimulation/simulateSchedule.js.map +1 -1
- package/dist/commands/daylightSimulation/weather.d.ts.map +1 -1
- package/dist/commands/daylightSimulation/weather.js +19 -4
- package/dist/commands/daylightSimulation/weather.js.map +1 -1
- package/dist/commands/deviceControl/ble.d.ts +4 -0
- package/dist/commands/deviceControl/ble.d.ts.map +1 -0
- package/dist/commands/deviceControl/ble.js +214 -0
- package/dist/commands/deviceControl/ble.js.map +1 -0
- package/dist/commands/deviceControl/cct.d.ts.map +1 -1
- package/dist/commands/deviceControl/cct.js +47 -19
- package/dist/commands/deviceControl/cct.js.map +1 -1
- package/dist/commands/deviceControl/color.d.ts.map +1 -1
- package/dist/commands/deviceControl/color.js +16 -12
- package/dist/commands/deviceControl/color.js.map +1 -1
- package/dist/commands/deviceControl/config.d.ts.map +1 -1
- package/dist/commands/deviceControl/config.js +94 -58
- package/dist/commands/deviceControl/config.js.map +1 -1
- package/dist/commands/deviceControl/{discover.d.ts → desktop.d.ts} +3 -2
- package/dist/commands/deviceControl/desktop.d.ts.map +1 -0
- package/dist/commands/deviceControl/desktop.js +66 -0
- package/dist/commands/deviceControl/desktop.js.map +1 -0
- package/dist/commands/deviceControl/effect.d.ts.map +1 -1
- package/dist/commands/deviceControl/effect.js +90 -159
- package/dist/commands/deviceControl/effect.js.map +1 -1
- package/dist/commands/deviceControl/fan.d.ts.map +1 -1
- package/dist/commands/deviceControl/fan.js +77 -95
- package/dist/commands/deviceControl/fan.js.map +1 -1
- package/dist/commands/deviceControl/group.d.ts.map +1 -1
- package/dist/commands/deviceControl/group.js +43 -6
- package/dist/commands/deviceControl/group.js.map +1 -1
- package/dist/commands/deviceControl/hsi.d.ts.map +1 -1
- package/dist/commands/deviceControl/hsi.js +6 -10
- package/dist/commands/deviceControl/hsi.js.map +1 -1
- package/dist/commands/deviceControl/info.d.ts.map +1 -1
- package/dist/commands/deviceControl/info.js +0 -50
- package/dist/commands/deviceControl/info.js.map +1 -1
- package/dist/commands/deviceControl/intensity.d.ts.map +1 -1
- package/dist/commands/deviceControl/intensity.js +38 -16
- package/dist/commands/deviceControl/intensity.js.map +1 -1
- package/dist/commands/deviceControl/list.d.ts.map +1 -1
- package/dist/commands/deviceControl/list.js +21 -15
- package/dist/commands/deviceControl/list.js.map +1 -1
- package/dist/commands/deviceControl/power.d.ts.map +1 -1
- package/dist/commands/deviceControl/power.js +19 -28
- package/dist/commands/deviceControl/power.js.map +1 -1
- package/dist/commands/deviceControl/preset.d.ts.map +1 -1
- package/dist/commands/deviceControl/preset.js +56 -19
- package/dist/commands/deviceControl/preset.js.map +1 -1
- package/dist/commands/deviceControl/program.d.ts +4 -0
- package/dist/commands/deviceControl/program.d.ts.map +1 -0
- package/dist/commands/deviceControl/program.js +199 -0
- package/dist/commands/deviceControl/program.js.map +1 -0
- package/dist/commands/deviceControl/quickshot.d.ts.map +1 -1
- package/dist/commands/deviceControl/quickshot.js +75 -13
- package/dist/commands/deviceControl/quickshot.js.map +1 -1
- package/dist/commands/deviceControl/scene.d.ts.map +1 -1
- package/dist/commands/deviceControl/scene.js +72 -9
- package/dist/commands/deviceControl/scene.js.map +1 -1
- package/dist/commands/deviceControl/status.d.ts.map +1 -1
- package/dist/commands/deviceControl/status.js +29 -6
- package/dist/commands/deviceControl/status.js.map +1 -1
- package/dist/commands/parseUtils.d.ts +6 -0
- package/dist/commands/parseUtils.d.ts.map +1 -0
- package/dist/commands/parseUtils.js +32 -0
- package/dist/commands/parseUtils.js.map +1 -0
- package/dist/commands-api.d.ts +28 -0
- package/dist/commands-api.d.ts.map +1 -0
- package/dist/commands-api.js +27 -0
- package/dist/commands-api.js.map +1 -0
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +4 -2
- package/dist/commands.js.map +1 -1
- package/dist/config.d.ts +165 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +125 -0
- package/dist/config.js.map +1 -0
- package/dist/daylightSimulation/cctUtil.d.ts.map +1 -1
- package/dist/daylightSimulation/cctUtil.js +23 -22
- package/dist/daylightSimulation/cctUtil.js.map +1 -1
- package/dist/daylightSimulation/constants.d.ts +10 -0
- package/dist/daylightSimulation/constants.d.ts.map +1 -1
- package/dist/daylightSimulation/constants.js +4 -0
- package/dist/daylightSimulation/constants.js.map +1 -1
- package/dist/daylightSimulation/currentCct.d.ts +42 -0
- package/dist/daylightSimulation/currentCct.d.ts.map +1 -0
- package/dist/daylightSimulation/currentCct.js +217 -0
- package/dist/daylightSimulation/currentCct.js.map +1 -0
- package/dist/daylightSimulation/curves/realistic.d.ts +6 -6
- package/dist/daylightSimulation/curves/realistic.d.ts.map +1 -1
- package/dist/daylightSimulation/curves/realistic.js +36 -6
- package/dist/daylightSimulation/curves/realistic.js.map +1 -1
- package/dist/daylightSimulation/dashboardSettings.d.ts +47 -0
- package/dist/daylightSimulation/dashboardSettings.d.ts.map +1 -0
- package/dist/daylightSimulation/dashboardSettings.js +95 -0
- package/dist/daylightSimulation/dashboardSettings.js.map +1 -0
- package/dist/daylightSimulation/dashboardStatus.d.ts +72 -0
- package/dist/daylightSimulation/dashboardStatus.d.ts.map +1 -0
- package/dist/daylightSimulation/dashboardStatus.js +191 -0
- package/dist/daylightSimulation/dashboardStatus.js.map +1 -0
- package/dist/daylightSimulation/mathUtil.d.ts.map +1 -1
- package/dist/daylightSimulation/mathUtil.js +6 -2
- package/dist/daylightSimulation/mathUtil.js.map +1 -1
- package/dist/daylightSimulation/parseUtils.d.ts +3 -0
- package/dist/daylightSimulation/parseUtils.d.ts.map +1 -0
- package/dist/daylightSimulation/parseUtils.js +25 -0
- package/dist/daylightSimulation/parseUtils.js.map +1 -0
- package/dist/daylightSimulation/scheduleMaker.d.ts +6 -2
- package/dist/daylightSimulation/scheduleMaker.d.ts.map +1 -1
- package/dist/daylightSimulation/scheduleMaker.js +9 -11
- package/dist/daylightSimulation/scheduleMaker.js.map +1 -1
- package/dist/daylightSimulation/types.d.ts +15 -2
- package/dist/daylightSimulation/types.d.ts.map +1 -1
- package/dist/daylightSimulation/types.js +0 -2
- package/dist/daylightSimulation/types.js.map +1 -1
- package/dist/daylightSimulation/weatherUtil.d.ts.map +1 -1
- package/dist/daylightSimulation/weatherUtil.js +31 -0
- package/dist/daylightSimulation/weatherUtil.js.map +1 -1
- package/dist/device-control.d.ts +8 -0
- package/dist/device-control.d.ts.map +1 -0
- package/dist/device-control.js +8 -0
- package/dist/device-control.js.map +1 -0
- package/dist/deviceControl/autostart.d.ts.map +1 -1
- package/dist/deviceControl/autostart.js +15 -16
- package/dist/deviceControl/autostart.js.map +1 -1
- package/dist/deviceControl/bleHttpControl.d.ts +129 -0
- package/dist/deviceControl/bleHttpControl.d.ts.map +1 -0
- package/dist/deviceControl/bleHttpControl.js +753 -0
- package/dist/deviceControl/bleHttpControl.js.map +1 -0
- package/dist/deviceControl/lightControl.d.ts +9 -68
- package/dist/deviceControl/lightControl.d.ts.map +1 -1
- package/dist/deviceControl/lightControl.js +25 -255
- package/dist/deviceControl/lightControl.js.map +1 -1
- package/dist/deviceControl/lightControlCommands.d.ts +68 -0
- package/dist/deviceControl/lightControlCommands.d.ts.map +1 -0
- package/dist/deviceControl/lightControlCommands.js +244 -0
- package/dist/deviceControl/lightControlCommands.js.map +1 -0
- package/dist/deviceControl/types.d.ts +83 -4
- package/dist/deviceControl/types.d.ts.map +1 -1
- package/dist/deviceControl/types.js +1 -0
- package/dist/deviceControl/types.js.map +1 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/test/MockLightServer.d.ts +1 -0
- package/dist/test/MockLightServer.d.ts.map +1 -1
- package/dist/test/MockLightServer.js +5 -0
- package/dist/test/MockLightServer.js.map +1 -1
- package/docs/CHANGELOG.md +171 -0
- package/docs/DIRECT_BLE.md +359 -0
- package/package.json +39 -8
- package/scripts/ble-cli-check.ts +129 -0
- package/scripts/ble-config-probe.ts +109 -0
- package/scripts/ble-effects-check.ts +82 -0
- package/scripts/ble-extended-probe.ts +44 -0
- package/scripts/ble-fan-check.ts +129 -0
- package/scripts/ble-feature-probe.ts +37 -0
- package/scripts/ble-followup-check.ts +165 -0
- package/scripts/ble-hardware-check.ts +99 -0
- package/scripts/ble-live-extended-check.ts +205 -0
- package/scripts/ble-non-ota-check.ts +193 -0
- package/scripts/ble-probe.ts +45 -0
- package/scripts/calibrate-webcam.ts +152 -0
- package/scripts/desktop-reference.ts +85 -0
- package/scripts/webcam.ts +129 -0
- package/dist/__tests__/lightControl.test.d.ts +0 -2
- package/dist/__tests__/lightControl.test.d.ts.map +0 -1
- package/dist/__tests__/lightControl.test.js +0 -424
- package/dist/__tests__/lightControl.test.js.map +0 -1
- package/dist/commands/deviceControl/discover.d.ts.map +0 -1
- package/dist/commands/deviceControl/discover.js +0 -29
- package/dist/commands/deviceControl/discover.js.map +0 -1
|
@@ -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.`);
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { SavedStateSchema } from '../src/ble/library.js';
|
|
6
|
+
import { FAN_MODES, type FixtureState, parseFanMode } from '../src/ble/telink.js';
|
|
7
|
+
import { captureWebcam, imagePixels, RegionSchema, regionsFile } 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 results: unknown[] = [];
|
|
12
|
+
const errors: string[] = [];
|
|
13
|
+
const initial: Record<string, FixtureState> = {};
|
|
14
|
+
const fans: Record<string, number> = {};
|
|
15
|
+
const calibration = z.object({ regions: z.array(RegionSchema) }).parse(JSON.parse(readFileSync(regionsFile, 'utf8')));
|
|
16
|
+
const keys = ['desk', 'front', 'back'];
|
|
17
|
+
let changed = false;
|
|
18
|
+
|
|
19
|
+
async function request(route: string, body?: object, method = body ? 'POST' : 'GET'): Promise<unknown> {
|
|
20
|
+
const response = await fetch(`${base}${route}`, {
|
|
21
|
+
method,
|
|
22
|
+
...(body ? { body: JSON.stringify(body), headers: { 'content-type': 'application/json' } } : {}),
|
|
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
|
+
results.push({ route, body, result: data.result });
|
|
30
|
+
return data.result;
|
|
31
|
+
}
|
|
32
|
+
async function light(key: string, action: string, body?: object) {
|
|
33
|
+
return SavedStateSchema.parse(await request(`/lights/${key}/${action}`, body));
|
|
34
|
+
}
|
|
35
|
+
function image(label: string) {
|
|
36
|
+
const filename = captureWebcam(`extended-${run}-${label}`);
|
|
37
|
+
return z
|
|
38
|
+
.object({
|
|
39
|
+
regions: z.array(
|
|
40
|
+
z.object({ name: z.string(), rgb: z.array(z.number()), haloRgb: z.array(z.number()).optional() })
|
|
41
|
+
),
|
|
42
|
+
})
|
|
43
|
+
.parse(JSON.parse(readFileSync(filename.replace(/\.png$/, '.json'), 'utf8'))).regions;
|
|
44
|
+
}
|
|
45
|
+
function mean(rgb: number[]) {
|
|
46
|
+
return rgb.reduce((sum, value) => sum + value, 0) / rgb.length;
|
|
47
|
+
}
|
|
48
|
+
function pulsingVideo(key: string) {
|
|
49
|
+
const region = calibration.regions.find((entry) => entry.name === key);
|
|
50
|
+
if (!region) throw new Error(`No calibrated region for ${key}`);
|
|
51
|
+
const prefix = `extended-${run}-pulsing`;
|
|
52
|
+
const capture = spawnSync(
|
|
53
|
+
'ffmpeg',
|
|
54
|
+
[
|
|
55
|
+
'-hide_banner',
|
|
56
|
+
'-loglevel',
|
|
57
|
+
'error',
|
|
58
|
+
'-f',
|
|
59
|
+
'avfoundation',
|
|
60
|
+
'-pixel_format',
|
|
61
|
+
'nv12',
|
|
62
|
+
'-framerate',
|
|
63
|
+
'30',
|
|
64
|
+
'-video_size',
|
|
65
|
+
'1280x720',
|
|
66
|
+
'-i',
|
|
67
|
+
`${process.env.AMARAN_CAMERA_INDEX ?? '0'}:none`,
|
|
68
|
+
'-t',
|
|
69
|
+
'12',
|
|
70
|
+
'-vf',
|
|
71
|
+
'fps=2',
|
|
72
|
+
'-y',
|
|
73
|
+
`artifacts/webcam/${prefix}-%03d.png`,
|
|
74
|
+
],
|
|
75
|
+
{ encoding: 'utf8', timeout: 30_000 }
|
|
76
|
+
);
|
|
77
|
+
if (capture.status !== 0) throw new Error(capture.stderr || 'Pulsing video capture failed');
|
|
78
|
+
const levels = readdirSync('artifacts/webcam')
|
|
79
|
+
.filter((name) => name.startsWith(`${prefix}-`) && name.endsWith('.png'))
|
|
80
|
+
.sort()
|
|
81
|
+
.map((name) => {
|
|
82
|
+
const pixels = imagePixels(`artifacts/webcam/${name}`);
|
|
83
|
+
let sum = 0;
|
|
84
|
+
for (let y = region.y; y < region.y + region.height; y++)
|
|
85
|
+
for (let x = region.x; x < region.x + region.width; x++) {
|
|
86
|
+
const offset = (y * 1280 + x) * 3;
|
|
87
|
+
sum += mean([...pixels.subarray(offset, offset + 3)]);
|
|
88
|
+
}
|
|
89
|
+
return sum / (region.width * region.height);
|
|
90
|
+
});
|
|
91
|
+
assert.ok(levels.length >= 20, 'Need enough camera samples to observe pulsing');
|
|
92
|
+
const settled = levels.slice(6);
|
|
93
|
+
assert.ok(Math.max(...settled) - Math.min(...settled) > 40, `Camera did not confirm pulse modulation: ${levels}`);
|
|
94
|
+
const changes = settled.slice(1).map((value, index) => value - settled[index]);
|
|
95
|
+
assert.ok(
|
|
96
|
+
changes.some((value) => value > 20) && changes.some((value) => value < -20),
|
|
97
|
+
'Need both rising and falling output after camera exposure settles'
|
|
98
|
+
);
|
|
99
|
+
results.push({ opticalPulsing: { key, levels, span: Math.max(...settled) - Math.min(...settled) } });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
for (const key of keys) {
|
|
104
|
+
initial[key] = await light(key, 'state');
|
|
105
|
+
const fan = z.object({ mode: z.number() }).parse(await request(`/lights/${key}/fan`));
|
|
106
|
+
parseFanMode(fan.mode);
|
|
107
|
+
assert.notEqual(fan.mode, FAN_MODES.manual, 'Cannot infer an original manual RPM setpoint for restoration');
|
|
108
|
+
fans[key] = fan.mode;
|
|
109
|
+
}
|
|
110
|
+
writeFileSync(`artifacts/extended-${run}-initial.json`, JSON.stringify({ states: initial, fans }, null, 2), {
|
|
111
|
+
mode: 0o600,
|
|
112
|
+
});
|
|
113
|
+
assert.ok(
|
|
114
|
+
Object.values(initial).every((state) => state.mode === 'cct'),
|
|
115
|
+
'Begin hardware checks with all three fixtures in steady CCT mode'
|
|
116
|
+
);
|
|
117
|
+
changed = true;
|
|
118
|
+
await request('/batch', { targets: 'all', action: 'off', args: {} });
|
|
119
|
+
const dark = image('all-off');
|
|
120
|
+
await request('/batch', { targets: 'all', action: 'cct', args: { kelvin: 3200, brightness: 1 } });
|
|
121
|
+
const bright = image('all-1pct');
|
|
122
|
+
for (const key of keys) {
|
|
123
|
+
const before = dark.find((region) => region.name === key);
|
|
124
|
+
const after = bright.find((region) => region.name === key);
|
|
125
|
+
assert.ok(before && after);
|
|
126
|
+
assert.ok(mean(after.rgb) - mean(before.rgb) > 40, `Camera did not confirm ${key} illumination`);
|
|
127
|
+
const fan = z
|
|
128
|
+
.object({ mode: z.number(), speed: z.number(), highTemperature: z.boolean() })
|
|
129
|
+
.parse(await request(`/lights/${key}/fan`, { mode: 'medium' }));
|
|
130
|
+
assert.equal(fan.mode, 5);
|
|
131
|
+
assert.equal(fan.highTemperature, false);
|
|
132
|
+
await request(`/lights/${key}/fan`, { mode: fans[key] });
|
|
133
|
+
}
|
|
134
|
+
for (const gm of [-30, 30, 0]) {
|
|
135
|
+
assert.equal((await light('back', 'gm', { value: gm })).gm, gm);
|
|
136
|
+
image(`gm-${gm}`);
|
|
137
|
+
}
|
|
138
|
+
await request('/batch', { targets: 'all', action: 'brightness', args: { value: 2 }, broadcast: true });
|
|
139
|
+
for (const key of keys) assert.equal((await light(key, 'state')).intensity, 20);
|
|
140
|
+
image('broadcast-2pct');
|
|
141
|
+
await request('/fade', { targets: 'all', brightness: 1, seconds: 2 });
|
|
142
|
+
for (const key of keys) assert.equal((await light(key, 'state')).intensity, 10);
|
|
143
|
+
image('faded-1pct');
|
|
144
|
+
const scene = z.object({ id: z.string() }).parse(await request('/library/scenes', { name: `Hardware check ${run}` }));
|
|
145
|
+
try {
|
|
146
|
+
await light('back', 'color', { color: '#00ff00', brightness: 1 });
|
|
147
|
+
const colored = image('hsi-green').find((region) => region.name === 'back');
|
|
148
|
+
assert.ok(
|
|
149
|
+
colored?.haloRgb && colored.haloRgb[1] - Math.max(colored.haloRgb[0], colored.haloRgb[2]) > 20,
|
|
150
|
+
'Camera did not confirm green HSI'
|
|
151
|
+
);
|
|
152
|
+
await request(`/library/scenes/${scene.id}/recall`, {});
|
|
153
|
+
for (const key of keys) assert.equal((await light(key, 'state')).cct, 3200);
|
|
154
|
+
} finally {
|
|
155
|
+
await request(`/library/scenes/${scene.id}`, undefined, 'DELETE');
|
|
156
|
+
}
|
|
157
|
+
await light('front', 'off', {});
|
|
158
|
+
await light('back', 'off', {});
|
|
159
|
+
const pulse = await light('desk', 'effect', { name: 'pulsing', brightness: 1, frequency: 1, kelvin: 3200 });
|
|
160
|
+
assert.equal(pulse.effect, 'pulsing');
|
|
161
|
+
pulsingVideo('desk');
|
|
162
|
+
await light('desk', 'effect-stop', {});
|
|
163
|
+
await light('desk', 'off', {});
|
|
164
|
+
for (const settings of [
|
|
165
|
+
{ name: 'pulsing', brightness: 1, frequency: 1, hue: 120, saturation: 100 },
|
|
166
|
+
{ name: 'party-lights', brightness: 1, frequency: 1, saturation: 80 },
|
|
167
|
+
]) {
|
|
168
|
+
const state = await light('back', 'effect', settings);
|
|
169
|
+
assert.equal(state.effect, settings.name);
|
|
170
|
+
image(settings.name);
|
|
171
|
+
await light('back', 'effect-intensity', { value: 2 });
|
|
172
|
+
await light('back', 'effect-stop', {});
|
|
173
|
+
}
|
|
174
|
+
} catch (error) {
|
|
175
|
+
errors.push(String(error));
|
|
176
|
+
} finally {
|
|
177
|
+
for (const [key, state] of changed ? Object.entries(initial) : []) {
|
|
178
|
+
try {
|
|
179
|
+
assert.equal(state.mode, 'cct', 'This check restores an initial steady CCT setup');
|
|
180
|
+
await light(key, 'cct', {
|
|
181
|
+
kelvin: state.cct,
|
|
182
|
+
brightness: state.intensity / 10,
|
|
183
|
+
...(key === 'back' ? { gm: state.gm } : {}),
|
|
184
|
+
});
|
|
185
|
+
await light(key, state.sleep ? 'off' : 'on', {});
|
|
186
|
+
const restored = await light(key, 'state');
|
|
187
|
+
for (const field of ['cct', 'intensity', 'sleep'] as const) assert.equal(restored[field], state[field]);
|
|
188
|
+
if (key === 'back') assert.equal(restored.gm, state.gm);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
errors.push(`Restore ${key}: ${error}`);
|
|
191
|
+
}
|
|
192
|
+
if (fans[key] !== undefined) {
|
|
193
|
+
try {
|
|
194
|
+
await request(`/lights/${key}/fan`, { mode: fans[key] });
|
|
195
|
+
} catch (error) {
|
|
196
|
+
errors.push(`Restore ${key} fan: ${error}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
writeFileSync(`artifacts/extended-${run}-results.json`, JSON.stringify({ results, errors }, null, 2), {
|
|
201
|
+
mode: 0o600,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (errors.length) throw new Error(errors.join('\n'));
|
|
205
|
+
console.log(`Extended live checks passed; results saved to artifacts/extended-${run}-results.json`);
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { realpathSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { SavedStateSchema } from '../src/ble/library.js';
|
|
6
|
+
|
|
7
|
+
const base = process.env.AMARAN_BLE_URL ?? 'http://127.0.0.1:2708';
|
|
8
|
+
const database = process.argv[2];
|
|
9
|
+
if (!database) throw new Error('Supply the matching Desktop database path');
|
|
10
|
+
const run = Date.now();
|
|
11
|
+
const records: unknown[] = [],
|
|
12
|
+
failures: string[] = [];
|
|
13
|
+
const cleanup: string[] = [],
|
|
14
|
+
jobs: string[] = [];
|
|
15
|
+
let baseline: { id: string; states: Record<string, z.infer<typeof SavedStateSchema>> } | undefined;
|
|
16
|
+
let overrides: Record<string, number> = {};
|
|
17
|
+
async function request(route: string, body?: object, method = body ? 'POST' : 'GET') {
|
|
18
|
+
const response = await fetch(`${base}${route}`, {
|
|
19
|
+
method,
|
|
20
|
+
headers: { connection: 'close', ...(body ? { 'content-type': 'application/json' } : {}) },
|
|
21
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
22
|
+
signal: AbortSignal.timeout(60_000),
|
|
23
|
+
});
|
|
24
|
+
const data = z
|
|
25
|
+
.object({ ok: z.boolean(), result: z.unknown().optional(), error: z.string().optional() })
|
|
26
|
+
.parse(await response.json());
|
|
27
|
+
if (!response.ok || !data.ok) throw new Error(`${route}: ${data.error ?? response.status}`);
|
|
28
|
+
records.push({ route, body, result: data.result });
|
|
29
|
+
return data.result;
|
|
30
|
+
}
|
|
31
|
+
const Status = z.object({ id: z.string(), state: z.string(), frames: z.number(), error: z.string().optional() });
|
|
32
|
+
async function start(body: object) {
|
|
33
|
+
const status = Status.parse(await request('/programs', body));
|
|
34
|
+
jobs.push(status.id);
|
|
35
|
+
return status;
|
|
36
|
+
}
|
|
37
|
+
async function wait(id: string, condition: (state: z.infer<typeof Status>) => boolean) {
|
|
38
|
+
const end = Date.now() + 30_000;
|
|
39
|
+
while (Date.now() < end) {
|
|
40
|
+
const status = Status.parse(await request(`/programs/${id}`));
|
|
41
|
+
if (status.state === 'failed') throw new Error(status.error);
|
|
42
|
+
if (condition(status)) return status;
|
|
43
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
44
|
+
}
|
|
45
|
+
throw new Error('Timed out waiting for program');
|
|
46
|
+
}
|
|
47
|
+
async function state(key: string) {
|
|
48
|
+
return SavedStateSchema.parse(await request(`/lights/${key}/state`));
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
overrides = Object.fromEntries(
|
|
52
|
+
Object.entries(z.record(z.number()).parse(await request('/overrides', { targets: 'all' }))).map(([key, ms]) => [
|
|
53
|
+
key,
|
|
54
|
+
ms ? Date.now() + ms : 0,
|
|
55
|
+
])
|
|
56
|
+
);
|
|
57
|
+
for (const key of ['desk', 'front', 'back']) {
|
|
58
|
+
const light = await state(key);
|
|
59
|
+
assert.ok(!light.sleep && light.mode === 'cct' && light.intensity <= 50, 'Begin awake in CCT at 5% or less');
|
|
60
|
+
}
|
|
61
|
+
baseline = z
|
|
62
|
+
.object({ id: z.string(), states: z.record(SavedStateSchema) })
|
|
63
|
+
.parse(await request('/library/scenes', { name: `Non-OTA baseline ${run}` }));
|
|
64
|
+
const group = z.object({ id: z.string() }).parse(await request('/groups', { name: `Native verification ${run}` }));
|
|
65
|
+
cleanup.push(`/groups/${encodeURIComponent(group.id)}`);
|
|
66
|
+
for (const member of ['desk', 'back']) await request(`/groups/${encodeURIComponent(group.id)}/members`, { member });
|
|
67
|
+
await request(`/groups/${encodeURIComponent(group.id)}/native`, { action: 'enable' });
|
|
68
|
+
const changed = z
|
|
69
|
+
.object({ delivery: z.literal('native-group') })
|
|
70
|
+
.parse(await request(`/lights/${encodeURIComponent(group.id)}/brightness`, { value: 1 }));
|
|
71
|
+
assert.equal(changed.delivery, 'native-group');
|
|
72
|
+
assert.equal((await state('desk')).intensity, 10);
|
|
73
|
+
assert.equal((await state('back')).intensity, 10);
|
|
74
|
+
assert.equal((await state('front')).intensity, baseline.states.front.intensity);
|
|
75
|
+
await request(`/groups/${encodeURIComponent(group.id)}/members`, { member: 'front' });
|
|
76
|
+
await request(`/groups/${encodeURIComponent(group.id)}/members`, { member: 'front', remove: true });
|
|
77
|
+
await request(`/groups/${encodeURIComponent(group.id)}/native`, { action: 'disable' });
|
|
78
|
+
await request(`/library/scenes/${baseline.id}/recall`, {});
|
|
79
|
+
const preview = z
|
|
80
|
+
.object({
|
|
81
|
+
valid: z.boolean(),
|
|
82
|
+
plan: z.object({
|
|
83
|
+
groups: z.array(z.object({ id: z.string() })),
|
|
84
|
+
entries: z.array(z.object({ collection: z.string(), entry: z.object({ id: z.string() }) })),
|
|
85
|
+
}),
|
|
86
|
+
})
|
|
87
|
+
.parse(await request('/desktop/import', { database: realpathSync(database), prefix: `Import check ${run}` }));
|
|
88
|
+
assert.equal(preview.valid, true);
|
|
89
|
+
for (const item of preview.plan.groups) cleanup.push(`/groups/${encodeURIComponent(item.id)}`);
|
|
90
|
+
for (const item of preview.plan.entries)
|
|
91
|
+
cleanup.push(`/library/${item.collection}/${encodeURIComponent(item.entry.id)}`);
|
|
92
|
+
await request('/desktop/import', { database: realpathSync(database), prefix: `Import check ${run}`, apply: true });
|
|
93
|
+
const again = z
|
|
94
|
+
.object({ changes: z.object({ created: z.number(), unchanged: z.number() }) })
|
|
95
|
+
.parse(
|
|
96
|
+
await request('/desktop/import', { database: realpathSync(database), prefix: `Import check ${run}`, apply: true })
|
|
97
|
+
);
|
|
98
|
+
assert.equal(again.changes.created, 0);
|
|
99
|
+
assert.ok(again.changes.unchanged > 0);
|
|
100
|
+
const timeline = await start({
|
|
101
|
+
kind: 'timeline',
|
|
102
|
+
targets: 'all',
|
|
103
|
+
duration: 2,
|
|
104
|
+
steps: [
|
|
105
|
+
{ at: 0, action: 'brightness', args: { value: 1 } },
|
|
106
|
+
{ at: 1, action: 'brightness', args: { value: 2 } },
|
|
107
|
+
],
|
|
108
|
+
});
|
|
109
|
+
await wait(timeline.id, (status) => status.state === 'completed');
|
|
110
|
+
const takeover = await start({
|
|
111
|
+
kind: 'timeline',
|
|
112
|
+
targets: ['back'],
|
|
113
|
+
duration: 5,
|
|
114
|
+
steps: [
|
|
115
|
+
{ at: 0, action: 'brightness', args: { value: 1 } },
|
|
116
|
+
{ at: 4, action: 'brightness', args: { value: 2 } },
|
|
117
|
+
],
|
|
118
|
+
});
|
|
119
|
+
await wait(takeover.id, (status) => status.frames > 0);
|
|
120
|
+
await request('/lights/back/brightness', { value: 3 });
|
|
121
|
+
assert.equal(Status.parse(await request(`/programs/${takeover.id}`)).state, 'cancelled');
|
|
122
|
+
assert.equal((await state('back')).intensity, 30);
|
|
123
|
+
await request(`/library/scenes/${baseline.id}/recall`, {});
|
|
124
|
+
const picker = await start({
|
|
125
|
+
kind: 'picker',
|
|
126
|
+
targets: ['back'],
|
|
127
|
+
duration: 4,
|
|
128
|
+
maxBrightness: 1,
|
|
129
|
+
source: { kind: 'image-file', file: realpathSync('artifacts/program-test-green.png') },
|
|
130
|
+
});
|
|
131
|
+
await wait(picker.id, (status) => status.frames > 0);
|
|
132
|
+
assert.equal((await state('back')).mode, 'hsi');
|
|
133
|
+
assert.equal((await state('back')).hue, 120);
|
|
134
|
+
await wait(picker.id, (status) => status.state === 'completed');
|
|
135
|
+
const audio = await start({
|
|
136
|
+
kind: 'audio',
|
|
137
|
+
targets: 'all',
|
|
138
|
+
duration: 3,
|
|
139
|
+
maxBrightness: 2,
|
|
140
|
+
source: { kind: 'audio-file', file: realpathSync('artifacts/program-test-tone.wav') },
|
|
141
|
+
});
|
|
142
|
+
const audioDone = await wait(audio.id, (status) => status.state === 'completed');
|
|
143
|
+
assert.ok(audioDone.frames > 0);
|
|
144
|
+
const file = `artifacts/timeline-check-${run}.json`;
|
|
145
|
+
writeFileSync(file, JSON.stringify({ duration: 1, steps: [{ at: 0, action: 'brightness', args: { value: 1 } }] }), {
|
|
146
|
+
mode: 0o600,
|
|
147
|
+
});
|
|
148
|
+
execFileSync(process.execPath, ['dist/cli.js', 'ble', 'program', file, '--targets', 'back'], {
|
|
149
|
+
stdio: 'pipe',
|
|
150
|
+
timeout: 20_000,
|
|
151
|
+
});
|
|
152
|
+
} catch (error) {
|
|
153
|
+
failures.push(String(error));
|
|
154
|
+
} finally {
|
|
155
|
+
for (const id of jobs) {
|
|
156
|
+
try {
|
|
157
|
+
await request(`/programs/${id}`, undefined, 'DELETE');
|
|
158
|
+
} catch (error) {
|
|
159
|
+
failures.push(`Stop program ${id}: ${error}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (baseline) {
|
|
163
|
+
try {
|
|
164
|
+
await request(`/library/scenes/${baseline.id}/recall`, {});
|
|
165
|
+
for (const [key, expected] of Object.entries(baseline.states)) {
|
|
166
|
+
const actual = await state(key);
|
|
167
|
+
for (const field of ['mode', 'intensity', 'sleep', 'cct'] as const)
|
|
168
|
+
assert.equal(actual[field], expected[field]);
|
|
169
|
+
}
|
|
170
|
+
} catch (error) {
|
|
171
|
+
failures.push(`Restore: ${error}`);
|
|
172
|
+
}
|
|
173
|
+
cleanup.push(`/library/scenes/${baseline.id}`);
|
|
174
|
+
}
|
|
175
|
+
for (const route of cleanup.reverse()) {
|
|
176
|
+
try {
|
|
177
|
+
await request(route, undefined, 'DELETE');
|
|
178
|
+
} catch (error) {
|
|
179
|
+
failures.push(`Cleanup ${route}: ${error}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (!failures.length)
|
|
183
|
+
for (const [key, until] of Object.entries(overrides)) {
|
|
184
|
+
try {
|
|
185
|
+
await request('/overrides', { targets: [key], minutes: Math.max(0, (until - Date.now()) / 60_000) });
|
|
186
|
+
} catch (error) {
|
|
187
|
+
failures.push(`Restore override ${key}: ${error}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
writeFileSync(`artifacts/non-ota-check-${run}.json`, JSON.stringify({ records, failures }, null, 2), { mode: 0o600 });
|
|
191
|
+
}
|
|
192
|
+
if (failures.length) throw new Error(failures.join('\n'));
|
|
193
|
+
console.log(`Non-OTA checks passed; original state restored: artifacts/non-ota-check-${run}.json`);
|