amaran-light-cli 1.0.2

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 (97) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +374 -0
  3. package/dist/__tests__/autoCct.test.d.ts +2 -0
  4. package/dist/__tests__/autoCct.test.d.ts.map +1 -0
  5. package/dist/__tests__/autoCct.test.js +65 -0
  6. package/dist/__tests__/autoCct.test.js.map +1 -0
  7. package/dist/__tests__/cctUtil.test.d.ts +2 -0
  8. package/dist/__tests__/cctUtil.test.d.ts.map +1 -0
  9. package/dist/__tests__/cctUtil.test.js +195 -0
  10. package/dist/__tests__/cctUtil.test.js.map +1 -0
  11. package/dist/__tests__/cli.smoke.test.d.ts +2 -0
  12. package/dist/__tests__/cli.smoke.test.d.ts.map +1 -0
  13. package/dist/__tests__/cli.smoke.test.js +18 -0
  14. package/dist/__tests__/cli.smoke.test.js.map +1 -0
  15. package/dist/__tests__/intensityApi.test.d.ts +2 -0
  16. package/dist/__tests__/intensityApi.test.d.ts.map +1 -0
  17. package/dist/__tests__/intensityApi.test.js +105 -0
  18. package/dist/__tests__/intensityApi.test.js.map +1 -0
  19. package/dist/__tests__/lightControl.test.d.ts +2 -0
  20. package/dist/__tests__/lightControl.test.d.ts.map +1 -0
  21. package/dist/__tests__/lightControl.test.js +185 -0
  22. package/dist/__tests__/lightControl.test.js.map +1 -0
  23. package/dist/cctUtil.d.ts +26 -0
  24. package/dist/cctUtil.d.ts.map +1 -0
  25. package/dist/cctUtil.js +93 -0
  26. package/dist/cctUtil.js.map +1 -0
  27. package/dist/cli.d.ts +10 -0
  28. package/dist/cli.d.ts.map +1 -0
  29. package/dist/cli.js +274 -0
  30. package/dist/cli.js.map +1 -0
  31. package/dist/commands/autoCct.d.ts +5 -0
  32. package/dist/commands/autoCct.d.ts.map +1 -0
  33. package/dist/commands/autoCct.js +270 -0
  34. package/dist/commands/autoCct.js.map +1 -0
  35. package/dist/commands/cct.d.ts +5 -0
  36. package/dist/commands/cct.d.ts.map +1 -0
  37. package/dist/commands/cct.js +68 -0
  38. package/dist/commands/cct.js.map +1 -0
  39. package/dist/commands/color.d.ts +5 -0
  40. package/dist/commands/color.d.ts.map +1 -0
  41. package/dist/commands/color.js +63 -0
  42. package/dist/commands/color.js.map +1 -0
  43. package/dist/commands/discover.d.ts +5 -0
  44. package/dist/commands/discover.d.ts.map +1 -0
  45. package/dist/commands/discover.js +30 -0
  46. package/dist/commands/discover.js.map +1 -0
  47. package/dist/commands/hsi.d.ts +5 -0
  48. package/dist/commands/hsi.d.ts.map +1 -0
  49. package/dist/commands/hsi.js +65 -0
  50. package/dist/commands/hsi.js.map +1 -0
  51. package/dist/commands/intensity.d.ts +5 -0
  52. package/dist/commands/intensity.d.ts.map +1 -0
  53. package/dist/commands/intensity.js +56 -0
  54. package/dist/commands/intensity.js.map +1 -0
  55. package/dist/commands/list.d.ts +5 -0
  56. package/dist/commands/list.d.ts.map +1 -0
  57. package/dist/commands/list.js +36 -0
  58. package/dist/commands/list.js.map +1 -0
  59. package/dist/commands/power.d.ts +5 -0
  60. package/dist/commands/power.d.ts.map +1 -0
  61. package/dist/commands/power.js +126 -0
  62. package/dist/commands/power.js.map +1 -0
  63. package/dist/commands/schedule.d.ts +5 -0
  64. package/dist/commands/schedule.d.ts.map +1 -0
  65. package/dist/commands/schedule.js +188 -0
  66. package/dist/commands/schedule.js.map +1 -0
  67. package/dist/commands/service.d.ts +5 -0
  68. package/dist/commands/service.d.ts.map +1 -0
  69. package/dist/commands/service.js +350 -0
  70. package/dist/commands/service.js.map +1 -0
  71. package/dist/commands/status.d.ts +5 -0
  72. package/dist/commands/status.d.ts.map +1 -0
  73. package/dist/commands/status.js +54 -0
  74. package/dist/commands/status.js.map +1 -0
  75. package/dist/commands.d.ts +5 -0
  76. package/dist/commands.d.ts.map +1 -0
  77. package/dist/commands.js +31 -0
  78. package/dist/commands.js.map +1 -0
  79. package/dist/discovery.d.ts +13 -0
  80. package/dist/discovery.d.ts.map +1 -0
  81. package/dist/discovery.js +130 -0
  82. package/dist/discovery.js.map +1 -0
  83. package/dist/geoipUtil.d.ts +8 -0
  84. package/dist/geoipUtil.d.ts.map +1 -0
  85. package/dist/geoipUtil.js +26 -0
  86. package/dist/geoipUtil.js.map +1 -0
  87. package/dist/lightControl.d.ts +114 -0
  88. package/dist/lightControl.d.ts.map +1 -0
  89. package/dist/lightControl.js +385 -0
  90. package/dist/lightControl.js.map +1 -0
  91. package/dist/types.d.ts +37 -0
  92. package/dist/types.d.ts.map +1 -0
  93. package/dist/types.js +3 -0
  94. package/dist/types.js.map +1 -0
  95. package/package.json +75 -0
  96. package/scripts/create-launchd-service.sh +114 -0
  97. package/scripts/run-auto-cct.sh +43 -0
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 S. Zachariah Sprackett <zac@sprackett.com>
4
+ Copyright (c) 2025 Mahyar McDonald <github@hmmfn.com>
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,374 @@
1
+ # Amaran Light CLI
2
+
3
+ A command line tool for controlling Aputure Amaran lights via WebSocket connection to the Amaran Desktop application. Not an offical Amaran command line tool.
4
+
5
+ Also has a circadian lighting command called `auto-cct` that will set the CCT & intensity according to the time of day it is currently at your location, and a service that will run the command every minute to automate it.
6
+
7
+ Written with AI mostly, including this documentation. Thanks to [Zac for his core websocket gist that enabled this](https://gist.github.com/zsprackett/29334b9be1e2bd90c1737bd0ba0eaf5c).
8
+
9
+ ## Installation
10
+
11
+ 1. Clone this repository or download the files
12
+ 2. Install dependencies:
13
+ ```bash
14
+ npm install
15
+ ```
16
+ 3. Build the project:
17
+ ```bash
18
+ npm run build
19
+ ```
20
+ 4. Optionally, install globally:
21
+ ```bash
22
+ npm install -g .
23
+ ```
24
+
25
+ ## Prerequisites
26
+
27
+ - Aputure Amaran Desktop application must be running
28
+ - WebSocket server should be accessible (default: ws://localhost:60124)
29
+
30
+ ## Configuration
31
+
32
+ Configure the WebSocket URL and other settings:
33
+
34
+ ```bash
35
+ # Set WebSocket URL
36
+ amaran-cli config -u ws://localhost:60124
37
+
38
+ # Set client ID
39
+ amaran-cli config -c my-cli-client
40
+
41
+ # Enable debug mode
42
+ amaran-cli config -d
43
+
44
+ # Set default location for auto-cct (overrides geoip)
45
+ amaran-cli config --lat 40.7128 --lon -74.0060
46
+
47
+ # Show current configuration
48
+ amaran-cli config --show
49
+ ```
50
+
51
+ Configuration is stored in `~/.amaran-cli.json`.
52
+
53
+ ## Discovery
54
+
55
+ The CLI can automatically discover the WebSocket endpoint from a running Amaran Desktop application:
56
+
57
+ ```bash
58
+ # Discover and save WebSocket URL
59
+ amaran-cli discover
60
+
61
+ # Discover with debug output
62
+ amaran-cli discover -d
63
+ ```
64
+
65
+ This command uses `lsof` to find the WebSocket port that the Amaran Desktop application is listening on, then saves it to your configuration file for future use.
66
+
67
+ ## Usage
68
+
69
+ ### List all available lights
70
+ ```bash
71
+ amaran-cli list
72
+ # or
73
+ amaran-cli ls
74
+ ```
75
+
76
+ ### Control lights
77
+ ```bash
78
+ # Turn light on
79
+ amaran-cli on "Light Name"
80
+ amaran-cli on node_id_123
81
+
82
+ # Turn on ALL lights
83
+ amaran-cli on
84
+
85
+ # Turn light off
86
+ amaran-cli off "Light Name"
87
+
88
+ # Turn off ALL lights
89
+ amaran-cli off
90
+
91
+ # Toggle light on/off
92
+ amaran-cli toggle "Light Name"
93
+
94
+ # Toggle ALL lights
95
+ amaran-cli toggle
96
+ ```
97
+
98
+ ### Set light properties
99
+ ```bash
100
+ # Set intensity (0-100%)
101
+ amaran-cli intensity 75 "Light Name"
102
+
103
+ # Set intensity for ALL lights
104
+ amaran-cli intensity 75
105
+
106
+ # Set color temperature (2000-6500K)
107
+ amaran-cli cct 5600 "Light Name"
108
+
109
+ # Set CCT for ALL lights
110
+ amaran-cli cct 5600
111
+
112
+ # Set color temperature with intensity
113
+ amaran-cli cct 3200 "Light Name" -i 80
114
+
115
+ # Set CCT with intensity for ALL lights
116
+ amaran-cli cct 3200 -i 80
117
+
118
+ # Set HSI color
119
+ amaran-cli hsi 240 100 75 "Light Name" # Blue at full saturation, 75% intensity
120
+
121
+ # Set HSI for ALL lights
122
+ amaran-cli hsi 240 100 75
123
+
124
+ # Set color by name or hex
125
+ amaran-cli color red "Light Name"
126
+ amaran-cli color "#ff0000" "Light Name"
127
+ amaran-cli color blue "Light Name" -i 50
128
+
129
+ # Set color for ALL lights
130
+ amaran-cli color red
131
+ amaran-cli color "#ff0000"
132
+ amaran-cli color blue -i 50
133
+ ```
134
+
135
+ **Note:** For all commands that support device names, you can omit the device name or use `all` to apply the command to all lights. Commands are throttled with 250ms delay between each device to prevent overwhelming the server.
136
+
137
+ ### Auto CCT (Circadian Lighting)
138
+ ```bash
139
+ # Set CCT for all lights based on current time and location (auto-detected via geoip)
140
+ amaran-cli auto-cct
141
+
142
+ # Override IP address for geolocation lookup
143
+ amaran-cli auto-cct --ip 8.8.8.8
144
+
145
+ # Manual latitude and longitude
146
+ amaran-cli auto-cct --lat 40.7128 --lon -74.0060
147
+
148
+ # Manual time (ISO 8601 format)
149
+ amaran-cli auto-cct --time 2025-10-26T14:30:00
150
+
151
+ # Combine manual location and time
152
+ amaran-cli auto-cct --lat 40.7128 --lon -74.0060 --time 2025-10-26T06:00:00
153
+
154
+ # Use with debug mode to see calculated values
155
+ amaran-cli auto-cct -d
156
+ ```
157
+
158
+ The `auto-cct` command automatically adjusts color temperature based on sunrise/sunset times for your location. By default, it maps the circadian curve to:
159
+ - **Before sunrise / After sunset**: 2000K at 5% intensity (warm, dim night lighting)
160
+ - **Solar noon**: 6500K at 100% intensity (cool, bright daylight)
161
+ - **Between sunrise and sunset**: Smooth bell curve transition from 2000K/5% → 6500K/100% → 2000K/5%
162
+
163
+ This mimics natural daylight changes throughout the day for more comfortable, circadian-friendly lighting. Both color temperature and brightness follow the same curve, providing natural dimming at dawn/dusk.
164
+
165
+ **Location Priority:**
166
+ 1. Command-line `--lat` and `--lon` arguments (highest priority)
167
+ 2. Config file defaults (set with `amaran-cli config --lat <lat> --lon <lon>`)
168
+ 3. GeoIP lookup based on public IP address (fallback)
169
+
170
+ **Bounds (optional):**
171
+ You can constrain the auto-cct curve to your preferred ranges via config:
172
+
173
+ ```bash
174
+ # CCT bounds in Kelvin (defaults 2000–6500)
175
+ amaran-cli config --cct-min 2500 --cct-max 6000
176
+
177
+ # Intensity bounds in percent (defaults 5–100)
178
+ amaran-cli config --intensity-min 10 --intensity-max 80
179
+
180
+ # Show the saved configuration
181
+ amaran-cli config --show
182
+ ```
183
+ If a bound is not set, the default is used. When both min and max are set, min must be <= max.
184
+
185
+ ### Running Auto-CCT as a Circadian Lighting Service
186
+
187
+ You can set up auto-cct to run automatically every minute as a circadian lighting background service. The service works with both global and local installations:
188
+
189
+ ```bash
190
+ # For global installation (recommended for production use)
191
+ npm install -g .
192
+
193
+ # Install and start the circadian lighting service (runs every 60 seconds by default)
194
+ amaran-cli circadian-service install
195
+
196
+ # Install with custom interval (minimum 10 seconds)
197
+ amaran-cli circadian-service install --interval 120
198
+
199
+ # Check service status
200
+ amaran-cli circadian-service status
201
+
202
+ # View logs
203
+ amaran-cli circadian-service logs
204
+
205
+ # Follow logs in real-time
206
+ amaran-cli circadian-service logs -f
207
+
208
+ # View error logs
209
+ amaran-cli circadian-service logs -e
210
+
211
+ # Stop the service temporarily
212
+ amaran-cli circadian-service stop
213
+
214
+ # Start the service
215
+ amaran-cli circadian-service start
216
+
217
+ # Uninstall the service completely
218
+ amaran-cli circadian-service uninstall
219
+ ```
220
+
221
+ **Backward Compatibility:**
222
+ The `service` command is still available as an alias to `circadian-service`:
223
+ ```bash
224
+ # These commands work the same way
225
+ amaran-cli service install
226
+ amaran-cli circadian-service install
227
+ ```
228
+
229
+ **Installation Detection:**
230
+ - The service automatically detects if amaran-cli is installed globally (via `npm install -g`) or running from a local development build
231
+ - Global installations run the executable directly for better performance
232
+ - Local development builds use Node.js to run the built JavaScript files
233
+
234
+ The service uses macOS launchd and will automatically start on login. Logs are stored in `~/Library/Logs/amaran-circadian-service.log`.
235
+
236
+ **Alternative: Using Cron**
237
+
238
+ If you prefer using cron instead of launchd, the included script handles both global and local installations:
239
+
240
+ ```bash
241
+ # Make the script executable
242
+ chmod +x scripts/run-auto-cct.sh
243
+
244
+ # Add to crontab (runs every minute)
245
+ crontab -e
246
+ # Add this line:
247
+ # * * * * * /absolute/path/to/amaran-cli/scripts/run-auto-cct.sh
248
+
249
+ # View logs
250
+ tail -f ~/.amaran-circadian-service.log
251
+ ```
252
+
253
+ **Manual launchd Setup:**
254
+
255
+ For advanced users who want to create the service manually:
256
+
257
+ ```bash
258
+ chmod +x scripts/create-launchd-service.sh
259
+ ./scripts/create-launchd-service.sh
260
+ ```
261
+
262
+ ### Preview Auto-CCT Schedule
263
+
264
+ View how color temperature and intensity will change throughout the day:
265
+
266
+ ```bash
267
+ # Preview schedule for today at your current location
268
+ amaran-cli schedule
269
+
270
+ # Preview with manual location
271
+ amaran-cli schedule --lat 40.7128 --lon -74.0060
272
+
273
+ # Preview for a specific date
274
+ amaran-cli schedule --date 2025-12-21
275
+
276
+ # Change time interval (default: 30 minutes)
277
+ amaran-cli schedule --interval 15
278
+
279
+ # Combine options
280
+ amaran-cli schedule --lat 51.5074 --lon -0.1278 --date 2025-06-21 --interval 60
281
+ ```
282
+
283
+ The schedule shows CCT and intensity values from 30 minutes before sunrise to 30 minutes after sunset, with special highlighting for:
284
+ - **Sunrise** (yellow)
285
+ - **Solar Noon** (green, bold)
286
+ - **Sunset** (magenta)
287
+
288
+ This helps you visualize and plan your automated lighting schedule before implementing it.
289
+
290
+ Note: The schedule respects any configured bounds set via `amaran-cli config --cct-min/--cct-max` and `--intensity-min/--intensity-max`. If no bounds are set, it uses the default curve from `calculateCCT`.
291
+
292
+ ### Get light status
293
+ ```bash
294
+ amaran-cli status "Light Name"
295
+ ```
296
+
297
+ ### Global options
298
+ All commands support these options:
299
+ - `-u, --url <url>`: Override WebSocket URL
300
+ - `-c, --client-id <id>`: Override client ID
301
+ - `-d, --debug`: Enable debug mode
302
+
303
+ Example:
304
+ ```bash
305
+ amaran-cli list -u ws://192.168.1.100:60124 -d
306
+ ```
307
+
308
+ ## Examples
309
+
310
+ ```bash
311
+ # Configure for remote Amaran Desktop
312
+ amaran-cli config -u ws://192.168.1.100:60124
313
+
314
+ # List all lights
315
+ amaran-cli list
316
+
317
+ # Turn on all lights
318
+ amaran-cli on
319
+
320
+ # Turn on the main key light
321
+ amaran-cli on "Key Light"
322
+
323
+ # Set all lights to 5600K at 80% intensity
324
+ amaran-cli cct 5600 -i 80
325
+
326
+ # Set key light to 5600K at 80% intensity
327
+ amaran-cli cct 5600 "Key Light" -i 80
328
+
329
+ # Set all lights to warm white at 60%
330
+ amaran-cli cct 3200 -i 60
331
+
332
+ # Set background light to blue at 40%
333
+ amaran-cli color blue "Background" -i 40
334
+
335
+ # Set all lights to 50% intensity
336
+ amaran-cli intensity 50
337
+
338
+ # Check status of key light
339
+ amaran-cli status "Key Light"
340
+
341
+ # Turn off all lights
342
+ amaran-cli off
343
+ ```
344
+
345
+ ## Development
346
+
347
+ ```bash
348
+ # Run in development mode
349
+ npm run dev
350
+
351
+ # Build the project
352
+ npm run build
353
+
354
+ # Clean build artifacts
355
+ npm run clean
356
+ ```
357
+
358
+ ## Device Identification
359
+
360
+ Devices can be identified by:
361
+ - **Device name**: Case-insensitive partial matching (e.g., "key" matches "Key Light")
362
+ - **Node ID**: Exact match of the device's node_id
363
+
364
+
365
+ ## License
366
+
367
+ MIT License - see the [LICENSE](LICENSE) file for details.
368
+
369
+ ## Credits
370
+
371
+ - Original WebSocket control implementation by S. Zachariah Sprackett
372
+ - CLI, auto-CCT, and additional features by Mahyar McDonald
373
+ - Built for use with Aputure Amaran lights and the Amaran Desktop application. This is not an official Amaran app and is not built by them. This was built as a quick hobby utility.
374
+
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=autoCct.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"autoCct.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/autoCct.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const globals_1 = require("@jest/globals");
7
+ const commander_1 = require("commander");
8
+ const commands_1 = __importDefault(require("../commands"));
9
+ globals_1.jest.mock('../geoipUtil', () => ({
10
+ getLocationFromIP: globals_1.jest.fn(() => ({ ll: [37.7749, -122.4194] })),
11
+ }));
12
+ globals_1.jest.mock('../cctUtil', () => ({
13
+ calculateCCT: globals_1.jest.fn(() => ({ cct: 5600, intensity: 500 })),
14
+ }));
15
+ describe('auto-cct command', () => {
16
+ const originalFetch = global.fetch;
17
+ const mockFetch = globals_1.jest.fn();
18
+ beforeAll(() => {
19
+ mockFetch.mockImplementation(async () => ({
20
+ json: async () => ({ ip: '1.2.3.4' }),
21
+ }));
22
+ global.fetch = mockFetch;
23
+ });
24
+ afterEach(() => {
25
+ mockFetch.mockClear();
26
+ });
27
+ afterAll(() => {
28
+ global.fetch = originalFetch;
29
+ });
30
+ test('skips sleeping lights when running auto-cct', async () => {
31
+ const setCCT = globals_1.jest.fn();
32
+ const disconnect = globals_1.jest.fn(async () => Promise.resolve());
33
+ const sleepState = {
34
+ '400J5-F2C008': false,
35
+ '400J5-F2C009': true,
36
+ };
37
+ const controllerStub = {
38
+ getDevices: globals_1.jest.fn(() => [
39
+ { node_id: '400J5-F2C008', device_name: 'On Light' },
40
+ { node_id: '400J5-F2C009', device_name: 'Off Light' },
41
+ ]),
42
+ getLightSleepStatus: globals_1.jest.fn((nodeId, cb) => {
43
+ setImmediate(() => cb(true, 'ok', { sleep: sleepState[nodeId] }));
44
+ }),
45
+ setCCT,
46
+ disconnect,
47
+ };
48
+ const deps = {
49
+ createController: async () => controllerStub,
50
+ findDevice: (_controller, _deviceQuery) => null,
51
+ asyncCommand: (fn) => (...args) => fn(...args),
52
+ loadConfig: () => ({}),
53
+ saveWsUrl: undefined,
54
+ };
55
+ const program = new commander_1.Command();
56
+ program.exitOverride();
57
+ (0, commands_1.default)(program, deps);
58
+ await program.parseAsync(['node', 'test', 'auto-cct']);
59
+ expect(setCCT).toHaveBeenCalledTimes(1);
60
+ // auto-cct should pass through calculateCCT result
61
+ expect(setCCT).toHaveBeenCalledWith('400J5-F2C008', 5600, 500);
62
+ expect(disconnect).toHaveBeenCalledTimes(1);
63
+ });
64
+ });
65
+ //# sourceMappingURL=autoCct.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"autoCct.test.js","sourceRoot":"","sources":["../../src/__tests__/autoCct.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cctUtil.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cctUtil.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/cctUtil.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,195 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const suncalc_1 = require("suncalc");
4
+ const cctUtil_1 = require("../cctUtil");
5
+ describe('calculateCCT', () => {
6
+ const NYC_LAT = 40.7128;
7
+ const NYC_LON = -74.006;
8
+ describe('NYC circadian lighting tests', () => {
9
+ let testDate;
10
+ let sunrise;
11
+ let sunset;
12
+ let solarNoon;
13
+ const MIN_INTENSITY_PCT = 5;
14
+ const MAX_INTENSITY_PCT = 100;
15
+ const MIN_CCT = 2000;
16
+ const MAX_CCT = 6500;
17
+ const MIN_INTENSITY_API = MIN_INTENSITY_PCT * 10; // API format (50)
18
+ const MAX_INTENSITY_API = MAX_INTENSITY_PCT * 10; // API format (1000)
19
+ beforeAll(() => {
20
+ // Use a fixed date for consistent test results
21
+ testDate = new Date('2024-06-21T12:00:00Z'); // Summer solstice
22
+ const times = (0, suncalc_1.getTimes)(testDate, NYC_LAT, NYC_LON);
23
+ sunrise = times.sunrise;
24
+ sunset = times.sunset;
25
+ solarNoon = times.solarNoon;
26
+ });
27
+ it('should return minimum CCT and intensity before sunrise', () => {
28
+ const beforeSunrise = new Date(sunrise.getTime() - 60000); // 1 minute before
29
+ const result = (0, cctUtil_1.calculateCCT)(NYC_LAT, NYC_LON, beforeSunrise, {
30
+ intensityMinPct: MIN_INTENSITY_PCT,
31
+ intensityMaxPct: MAX_INTENSITY_PCT,
32
+ cctMinK: MIN_CCT,
33
+ cctMaxK: MAX_CCT,
34
+ });
35
+ expect(result.cct).toBe(MIN_CCT);
36
+ expect(result.intensity).toBe(MIN_INTENSITY_API);
37
+ });
38
+ it('should return minimum CCT and intensity at sunrise edge', () => {
39
+ const atSunrise = new Date(sunrise.getTime());
40
+ const result = (0, cctUtil_1.calculateCCT)(NYC_LAT, NYC_LON, atSunrise, {
41
+ intensityMinPct: MIN_INTENSITY_PCT,
42
+ intensityMaxPct: MAX_INTENSITY_PCT,
43
+ cctMinK: MIN_CCT,
44
+ cctMaxK: MAX_CCT,
45
+ });
46
+ // At the exact sunrise moment, should be at minimum
47
+ expect(result.cct).toBe(MIN_CCT);
48
+ expect(result.intensity).toBe(MIN_INTENSITY_API);
49
+ });
50
+ it('should return maximum CCT and intensity at solar noon', () => {
51
+ const atNoon = new Date(solarNoon.getTime());
52
+ const result = (0, cctUtil_1.calculateCCT)(NYC_LAT, NYC_LON, atNoon, {
53
+ intensityMinPct: MIN_INTENSITY_PCT,
54
+ intensityMaxPct: MAX_INTENSITY_PCT,
55
+ cctMinK: MIN_CCT,
56
+ cctMaxK: MAX_CCT,
57
+ });
58
+ // At solar noon (middle of the day), should be at maximum
59
+ expect(result.cct).toBe(MAX_CCT);
60
+ expect(result.intensity).toBe(MAX_INTENSITY_API);
61
+ });
62
+ it('should return minimum CCT and intensity at sunset edge', () => {
63
+ const atSunset = new Date(sunset.getTime());
64
+ const result = (0, cctUtil_1.calculateCCT)(NYC_LAT, NYC_LON, atSunset, {
65
+ intensityMinPct: MIN_INTENSITY_PCT,
66
+ intensityMaxPct: MAX_INTENSITY_PCT,
67
+ cctMinK: MIN_CCT,
68
+ cctMaxK: MAX_CCT,
69
+ });
70
+ // At the exact sunset moment, should be at minimum
71
+ expect(result.cct).toBe(MIN_CCT);
72
+ expect(result.intensity).toBe(MIN_INTENSITY_API);
73
+ });
74
+ it('should return minimum CCT and intensity after sunset', () => {
75
+ const afterSunset = new Date(sunset.getTime() + 60000); // 1 minute after
76
+ const result = (0, cctUtil_1.calculateCCT)(NYC_LAT, NYC_LON, afterSunset, {
77
+ intensityMinPct: MIN_INTENSITY_PCT,
78
+ intensityMaxPct: MAX_INTENSITY_PCT,
79
+ cctMinK: MIN_CCT,
80
+ cctMaxK: MAX_CCT,
81
+ });
82
+ expect(result.cct).toBe(MIN_CCT);
83
+ expect(result.intensity).toBe(MIN_INTENSITY_API);
84
+ });
85
+ it('should have smooth progression from sunrise to noon', () => {
86
+ const quarterWay = new Date(sunrise.getTime() + (solarNoon.getTime() - sunrise.getTime()) / 4);
87
+ const result = (0, cctUtil_1.calculateCCT)(NYC_LAT, NYC_LON, quarterWay, {
88
+ intensityMinPct: MIN_INTENSITY_PCT,
89
+ intensityMaxPct: MAX_INTENSITY_PCT,
90
+ cctMinK: MIN_CCT,
91
+ cctMaxK: MAX_CCT,
92
+ });
93
+ // Should be between minimum and maximum values
94
+ expect(result.cct).toBeGreaterThan(MIN_CCT);
95
+ expect(result.cct).toBeLessThan(MAX_CCT);
96
+ expect(result.intensity).toBeGreaterThan(MIN_INTENSITY_API);
97
+ expect(result.intensity).toBeLessThan(MAX_INTENSITY_API);
98
+ });
99
+ it('should have smooth progression from noon to sunset', () => {
100
+ const threeQuartersWay = new Date(solarNoon.getTime() + (sunset.getTime() - solarNoon.getTime()) / 2);
101
+ const result = (0, cctUtil_1.calculateCCT)(NYC_LAT, NYC_LON, threeQuartersWay, {
102
+ intensityMinPct: MIN_INTENSITY_PCT,
103
+ intensityMaxPct: MAX_INTENSITY_PCT,
104
+ cctMinK: MIN_CCT,
105
+ cctMaxK: MAX_CCT,
106
+ });
107
+ // Should be between minimum and maximum values
108
+ expect(result.cct).toBeGreaterThan(MIN_CCT);
109
+ expect(result.cct).toBeLessThan(MAX_CCT);
110
+ expect(result.intensity).toBeGreaterThan(MIN_INTENSITY_API);
111
+ expect(result.intensity).toBeLessThan(MAX_INTENSITY_API);
112
+ });
113
+ });
114
+ describe('Edge cases', () => {
115
+ it('should handle midnight times', () => {
116
+ const midnight = new Date('2024-06-21T04:00:00Z'); // Midnight in NYC
117
+ const result = (0, cctUtil_1.calculateCCT)(NYC_LAT, NYC_LON, midnight);
118
+ expect(result.cct).toBe(2000);
119
+ expect(result.intensity).toBe(50); // 5% with default
120
+ });
121
+ it('should return valid results for different locations', () => {
122
+ const LONDON_LAT = 51.5074;
123
+ const LONDON_LON = -0.1278;
124
+ const testDate = new Date('2024-06-21T12:00:00Z');
125
+ const result = (0, cctUtil_1.calculateCCT)(LONDON_LAT, LONDON_LON, testDate);
126
+ expect(result.cct).toBeGreaterThanOrEqual(2000);
127
+ expect(result.cct).toBeLessThanOrEqual(6500);
128
+ expect(result.intensity).toBeGreaterThanOrEqual(50); // 5% minimum
129
+ expect(result.intensity).toBeLessThanOrEqual(1000);
130
+ });
131
+ it('should use default date if not provided', () => {
132
+ const result = (0, cctUtil_1.calculateCCT)(NYC_LAT, NYC_LON);
133
+ expect(result.cct).toBeGreaterThanOrEqual(2000);
134
+ expect(result.cct).toBeLessThanOrEqual(6500);
135
+ expect(result.intensity).toBeGreaterThanOrEqual(50); // 5% minimum
136
+ expect(result.intensity).toBeLessThanOrEqual(1000);
137
+ });
138
+ });
139
+ describe('Return value format', () => {
140
+ it('should return rounded integer values', () => {
141
+ const testDate = new Date('2024-06-21T12:00:00Z');
142
+ const result = (0, cctUtil_1.calculateCCT)(NYC_LAT, NYC_LON, testDate);
143
+ expect(Number.isInteger(result.cct)).toBe(true);
144
+ expect(Number.isInteger(result.intensity)).toBe(true);
145
+ });
146
+ it('should have correct properties', () => {
147
+ const testDate = new Date('2024-06-21T12:00:00Z');
148
+ const result = (0, cctUtil_1.calculateCCT)(NYC_LAT, NYC_LON, testDate);
149
+ expect(result).toHaveProperty('cct');
150
+ expect(result).toHaveProperty('intensity');
151
+ expect(typeof result.cct).toBe('number');
152
+ expect(typeof result.intensity).toBe('number');
153
+ });
154
+ });
155
+ describe('Intensity ranges', () => {
156
+ const NYC_LAT = 40.7128;
157
+ const NYC_LON = -74.006;
158
+ it('intensity stays within [50, 1000] across a whole day (hourly samples)', () => {
159
+ const base = new Date('2024-06-21T00:00:00Z'); // summer solstice
160
+ for (let hour = 0; hour < 24; hour++) {
161
+ const sample = new Date(base.getTime() + hour * 60 * 60 * 1000);
162
+ const { intensity } = (0, cctUtil_1.calculateCCT)(NYC_LAT, NYC_LON, sample);
163
+ expect(intensity).toBeGreaterThanOrEqual(50); // 5% minimum
164
+ expect(intensity).toBeLessThanOrEqual(1000);
165
+ }
166
+ });
167
+ it('intensity stays within range across seasons and locations (spot samples)', () => {
168
+ const cases = [
169
+ // Dates: solstices and equinoxes
170
+ '2024-03-20T00:00:00Z', // vernal equinox
171
+ '2024-06-21T00:00:00Z', // summer solstice
172
+ '2024-09-22T00:00:00Z', // autumnal equinox
173
+ '2024-12-21T00:00:00Z', // winter solstice
174
+ ];
175
+ const locations = [
176
+ { name: 'NYC', lat: 40.7128, lon: -74.006 },
177
+ { name: 'London', lat: 51.5074, lon: -0.1278 },
178
+ { name: 'Sydney', lat: -33.8688, lon: 151.2093 },
179
+ ];
180
+ const offsetsHours = [0, 6, 12, 18]; // midnight, morning, noon, evening (UTC)
181
+ for (const iso of cases) {
182
+ const base = new Date(iso);
183
+ for (const { lat, lon } of locations) {
184
+ for (const h of offsetsHours) {
185
+ const d = new Date(base.getTime() + h * 60 * 60 * 1000);
186
+ const { intensity } = (0, cctUtil_1.calculateCCT)(lat, lon, d);
187
+ expect(intensity).toBeGreaterThanOrEqual(50); // 5% minimum
188
+ expect(intensity).toBeLessThanOrEqual(1000);
189
+ }
190
+ }
191
+ }
192
+ });
193
+ });
194
+ });
195
+ //# sourceMappingURL=cctUtil.test.js.map