lifx-emulator 2.4.0__py3-none-any.whl → 3.1.0__py3-none-any.whl

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 (70) hide show
  1. lifx_emulator-3.1.0.dist-info/METADATA +103 -0
  2. lifx_emulator-3.1.0.dist-info/RECORD +19 -0
  3. {lifx_emulator-2.4.0.dist-info → lifx_emulator-3.1.0.dist-info}/WHEEL +1 -1
  4. lifx_emulator-3.1.0.dist-info/entry_points.txt +2 -0
  5. lifx_emulator_app/__init__.py +10 -0
  6. {lifx_emulator → lifx_emulator_app}/__main__.py +2 -3
  7. {lifx_emulator → lifx_emulator_app}/api/__init__.py +1 -1
  8. {lifx_emulator → lifx_emulator_app}/api/app.py +9 -4
  9. {lifx_emulator → lifx_emulator_app}/api/mappers/__init__.py +1 -1
  10. {lifx_emulator → lifx_emulator_app}/api/mappers/device_mapper.py +1 -1
  11. {lifx_emulator → lifx_emulator_app}/api/models.py +1 -2
  12. lifx_emulator_app/api/routers/__init__.py +11 -0
  13. {lifx_emulator → lifx_emulator_app}/api/routers/devices.py +2 -2
  14. {lifx_emulator → lifx_emulator_app}/api/routers/monitoring.py +1 -1
  15. {lifx_emulator → lifx_emulator_app}/api/routers/scenarios.py +1 -1
  16. lifx_emulator_app/api/services/__init__.py +8 -0
  17. {lifx_emulator → lifx_emulator_app}/api/services/device_service.py +3 -2
  18. lifx_emulator_app/api/static/dashboard.js +588 -0
  19. lifx_emulator_app/api/templates/dashboard.html +357 -0
  20. lifx_emulator/__init__.py +0 -31
  21. lifx_emulator/api/routers/__init__.py +0 -11
  22. lifx_emulator/api/services/__init__.py +0 -8
  23. lifx_emulator/api/templates/dashboard.html +0 -899
  24. lifx_emulator/constants.py +0 -33
  25. lifx_emulator/devices/__init__.py +0 -37
  26. lifx_emulator/devices/device.py +0 -395
  27. lifx_emulator/devices/manager.py +0 -256
  28. lifx_emulator/devices/observers.py +0 -139
  29. lifx_emulator/devices/persistence.py +0 -308
  30. lifx_emulator/devices/state_restorer.py +0 -259
  31. lifx_emulator/devices/state_serializer.py +0 -157
  32. lifx_emulator/devices/states.py +0 -381
  33. lifx_emulator/factories/__init__.py +0 -39
  34. lifx_emulator/factories/builder.py +0 -375
  35. lifx_emulator/factories/default_config.py +0 -158
  36. lifx_emulator/factories/factory.py +0 -252
  37. lifx_emulator/factories/firmware_config.py +0 -77
  38. lifx_emulator/factories/serial_generator.py +0 -82
  39. lifx_emulator/handlers/__init__.py +0 -39
  40. lifx_emulator/handlers/base.py +0 -49
  41. lifx_emulator/handlers/device_handlers.py +0 -322
  42. lifx_emulator/handlers/light_handlers.py +0 -503
  43. lifx_emulator/handlers/multizone_handlers.py +0 -249
  44. lifx_emulator/handlers/registry.py +0 -110
  45. lifx_emulator/handlers/tile_handlers.py +0 -488
  46. lifx_emulator/products/__init__.py +0 -28
  47. lifx_emulator/products/generator.py +0 -1079
  48. lifx_emulator/products/registry.py +0 -1530
  49. lifx_emulator/products/specs.py +0 -284
  50. lifx_emulator/products/specs.yml +0 -386
  51. lifx_emulator/protocol/__init__.py +0 -1
  52. lifx_emulator/protocol/base.py +0 -446
  53. lifx_emulator/protocol/const.py +0 -8
  54. lifx_emulator/protocol/generator.py +0 -1384
  55. lifx_emulator/protocol/header.py +0 -159
  56. lifx_emulator/protocol/packets.py +0 -1351
  57. lifx_emulator/protocol/protocol_types.py +0 -817
  58. lifx_emulator/protocol/serializer.py +0 -379
  59. lifx_emulator/repositories/__init__.py +0 -22
  60. lifx_emulator/repositories/device_repository.py +0 -155
  61. lifx_emulator/repositories/storage_backend.py +0 -107
  62. lifx_emulator/scenarios/__init__.py +0 -22
  63. lifx_emulator/scenarios/manager.py +0 -322
  64. lifx_emulator/scenarios/models.py +0 -112
  65. lifx_emulator/scenarios/persistence.py +0 -241
  66. lifx_emulator/server.py +0 -464
  67. lifx_emulator-2.4.0.dist-info/METADATA +0 -107
  68. lifx_emulator-2.4.0.dist-info/RECORD +0 -62
  69. lifx_emulator-2.4.0.dist-info/entry_points.txt +0 -2
  70. lifx_emulator-2.4.0.dist-info/licenses/LICENSE +0 -35
@@ -1,899 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>LIFX Emulator Monitor</title>
7
- <style>
8
- * {
9
- margin: 0;
10
- padding: 0;
11
- box-sizing: border-box;
12
- }
13
- body {
14
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI',
15
- Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
16
- background: #0a0a0a;
17
- color: #e0e0e0;
18
- line-height: 1.6;
19
- padding: 20px;
20
- }
21
- .container {
22
- max-width: 1400px;
23
- margin: 0 auto;
24
- }
25
- h1 {
26
- color: #fff;
27
- margin-bottom: 10px;
28
- font-size: 2em;
29
- }
30
- .subtitle {
31
- color: #888;
32
- margin-bottom: 30px;
33
- }
34
- .grid {
35
- display: grid;
36
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
37
- gap: 15px;
38
- margin-bottom: 25px;
39
- }
40
- .devices-grid {
41
- display: grid;
42
- grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
43
- gap: 10px;
44
- }
45
- .card {
46
- background: #1a1a1a;
47
- border: 1px solid #333;
48
- border-radius: 8px;
49
- padding: 20px;
50
- }
51
- .card h2 {
52
- color: #fff;
53
- font-size: 1.2em;
54
- margin-bottom: 15px;
55
- display: flex;
56
- align-items: center;
57
- gap: 10px;
58
- }
59
- .stat {
60
- display: flex;
61
- justify-content: space-between;
62
- padding: 8px 0;
63
- border-bottom: 1px solid #2a2a2a;
64
- }
65
- .stat:last-child {
66
- border-bottom: none;
67
- }
68
- .stat-label {
69
- color: #888;
70
- }
71
- .stat-value {
72
- color: #fff;
73
- font-weight: 600;
74
- }
75
- .device {
76
- background: #252525;
77
- border: 1px solid #333;
78
- border-radius: 6px;
79
- padding: 8px;
80
- margin-bottom: 8px;
81
- font-size: 0.85em;
82
- }
83
- .device-header {
84
- display: flex;
85
- justify-content: space-between;
86
- align-items: center;
87
- margin-bottom: 6px;
88
- }
89
- .device-serial {
90
- font-family: 'Monaco', 'Courier New', monospace;
91
- color: #4a9eff;
92
- font-weight: bold;
93
- font-size: 0.9em;
94
- }
95
- .device-label {
96
- color: #aaa;
97
- font-size: 0.85em;
98
- }
99
- .zones-container {
100
- margin-top: 8px;
101
- padding-top: 8px;
102
- border-top: 1px solid #333;
103
- }
104
- .zones-toggle, .metadata-toggle {
105
- cursor: pointer;
106
- color: #4a9eff;
107
- font-size: 0.8em;
108
- margin-top: 4px;
109
- user-select: none;
110
- }
111
- .zones-toggle:hover, .metadata-toggle:hover {
112
- color: #6bb0ff;
113
- }
114
- .zones-display, .metadata-display {
115
- display: none;
116
- margin-top: 6px;
117
- }
118
- .zones-display.show, .metadata-display.show {
119
- display: block;
120
- }
121
- .metadata-display {
122
- font-size: 0.75em;
123
- color: #888;
124
- padding: 6px;
125
- background: #1a1a1a;
126
- border-radius: 3px;
127
- border: 1px solid #333;
128
- }
129
- .metadata-row {
130
- display: flex;
131
- justify-content: space-between;
132
- padding: 2px 0;
133
- }
134
- .metadata-label {
135
- color: #666;
136
- }
137
- .metadata-value {
138
- color: #aaa;
139
- font-family: 'Monaco', 'Courier New', monospace;
140
- }
141
- .zone-strip {
142
- display: flex;
143
- height: 20px;
144
- border-radius: 3px;
145
- overflow: hidden;
146
- margin-bottom: 4px;
147
- }
148
- .zone-segment {
149
- flex: 1;
150
- min-width: 4px;
151
- }
152
- .color-swatch {
153
- display: inline-block;
154
- width: 16px;
155
- height: 16px;
156
- border-radius: 3px;
157
- border: 1px solid #333;
158
- vertical-align: middle;
159
- margin-right: 4px;
160
- }
161
- .tile-grid {
162
- display: grid;
163
- gap: 2px;
164
- margin-top: 4px;
165
- }
166
- .tile-zone {
167
- width: 8px;
168
- height: 8px;
169
- border-radius: 1px;
170
- }
171
- .tiles-container {
172
- display: flex;
173
- flex-wrap: wrap;
174
- gap: 8px;
175
- margin-top: 4px;
176
- }
177
- .tile-item {
178
- display: inline-block;
179
- }
180
- .badge {
181
- display: inline-block;
182
- padding: 2px 6px;
183
- border-radius: 3px;
184
- font-size: 0.7em;
185
- font-weight: 600;
186
- margin-right: 4px;
187
- margin-bottom: 2px;
188
- }
189
- .badge-power-on {
190
- background: #2d5;
191
- color: #000;
192
- }
193
- .badge-power-off {
194
- background: #555;
195
- color: #aaa;
196
- }
197
- .badge-capability {
198
- background: #333;
199
- color: #4a9eff;
200
- }
201
- .badge-extended-mz {
202
- background: #2d4a2d;
203
- color: #5dff5d;
204
- }
205
- .activity-log {
206
- background: #0d0d0d;
207
- border: 1px solid #333;
208
- border-radius: 6px;
209
- padding: 15px;
210
- max-height: 400px;
211
- overflow-y: auto;
212
- font-family: 'Monaco', 'Courier New', monospace;
213
- font-size: 0.85em;
214
- }
215
- .activity-item {
216
- padding: 6px 0;
217
- border-bottom: 1px solid #1a1a1a;
218
- display: flex;
219
- gap: 10px;
220
- }
221
- .activity-item:last-child {
222
- border-bottom: none;
223
- }
224
- .activity-time {
225
- color: #666;
226
- min-width: 80px;
227
- }
228
- .activity-rx {
229
- color: #4a9eff;
230
- }
231
- .activity-tx {
232
- color: #f9a825;
233
- }
234
- .activity-packet {
235
- color: #aaa;
236
- }
237
- .btn {
238
- background: #4a9eff;
239
- color: #000;
240
- border: none;
241
- padding: 4px 8px;
242
- border-radius: 3px;
243
- cursor: pointer;
244
- font-weight: 600;
245
- font-size: 0.75em;
246
- }
247
- .btn:hover {
248
- background: #6bb0ff;
249
- }
250
- .btn-delete {
251
- background: #d32f2f;
252
- color: #fff;
253
- }
254
- .btn-delete:hover {
255
- background: #e57373;
256
- }
257
- .form-group {
258
- margin-bottom: 15px;
259
- }
260
- .form-group label {
261
- display: block;
262
- color: #aaa;
263
- margin-bottom: 5px;
264
- font-size: 0.9em;
265
- }
266
- .form-group input, .form-group select {
267
- width: 100%;
268
- background: #0d0d0d;
269
- border: 1px solid #333;
270
- color: #fff;
271
- padding: 8px;
272
- border-radius: 4px;
273
- }
274
- .status-indicator {
275
- display: inline-block;
276
- width: 8px;
277
- height: 8px;
278
- border-radius: 50%;
279
- background: #2d5;
280
- animation: pulse 2s infinite;
281
- }
282
- @keyframes pulse {
283
- 0%, 100% { opacity: 1; }
284
- 50% { opacity: 0.5; }
285
- }
286
- .no-devices {
287
- text-align: center;
288
- color: #666;
289
- padding: 40px;
290
- }
291
- </style>
292
- </head>
293
- <body>
294
- <div class="container">
295
- <h1>LIFX Emulator Monitor</h1>
296
- <p class="subtitle">Real-time monitoring and device management</p>
297
-
298
- <div class="grid">
299
- <div class="card">
300
- <h2><span class="status-indicator"></span> Server Statistics</h2>
301
- <div id="stats">
302
- <div class="stat">
303
- <span class="stat-label">Loading...</span>
304
- <span class="stat-value"></span>
305
- </div>
306
- </div>
307
- </div>
308
-
309
- <div class="card">
310
- <h2>Add Device</h2>
311
- <form id="add-device-form">
312
- <div class="form-group">
313
- <label>Product ID</label>
314
- <select id="product-id" required>
315
- <option value="27">27 - LIFX A19</option>
316
- <option value="29">29 - LIFX A19 Night Vision</option>
317
- <option value="32">32 - LIFX Z (Strip)</option>
318
- <option value="38">38 - LIFX Beam</option>
319
- <option value="50">50 - LIFX Mini White to Warm</option>
320
- <option value="55">55 - LIFX Tile</option>
321
- <option value="90">90 - LIFX Clean (HEV)</option>
322
- </select>
323
- </div>
324
- <button type="submit" class="btn">Add Device</button>
325
- </form>
326
- </div>
327
- </div>
328
-
329
- <div class="card">
330
- <h2>
331
- Devices (<span id="device-count">0</span>)
332
- <span style="float: right; display: flex; gap: 8px;">
333
- <button
334
- class="btn btn-delete"
335
- onclick="removeAllDevices()"
336
- title="Remove all devices from server (runtime only)"
337
- >Remove All</button>
338
- <button
339
- class="btn btn-delete"
340
- onclick="clearStorage()"
341
- id="clear-storage-btn"
342
- title="Delete all persistent device state files"
343
- >Clear Storage</button>
344
- </span>
345
- </h2>
346
- <div id="devices" class="devices-grid"></div>
347
- </div>
348
-
349
- <div class="card" id="activity-card">
350
- <h2>Recent Activity</h2>
351
- <div class="activity-log" id="activity-log"></div>
352
- </div>
353
- </div>
354
-
355
- <script>
356
- let updateInterval;
357
-
358
- // Convert HSBK to RGB for display
359
- function hsbkToRgb(hsbk) {
360
- const h = hsbk.hue / 65535;
361
- const s = hsbk.saturation / 65535;
362
- const v = hsbk.brightness / 65535;
363
-
364
- let r, g, b;
365
- const i = Math.floor(h * 6);
366
- const f = h * 6 - i;
367
- const p = v * (1 - s);
368
- const q = v * (1 - f * s);
369
- const t = v * (1 - (1 - f) * s);
370
-
371
- switch (i % 6) {
372
- case 0: r = v; g = t; b = p; break;
373
- case 1: r = q; g = v; b = p; break;
374
- case 2: r = p; g = v; b = t; break;
375
- case 3: r = p; g = q; b = v; break;
376
- case 4: r = t; g = p; b = v; break;
377
- case 5: r = v; g = p; b = q; break;
378
- }
379
-
380
- const red = Math.round(r * 255);
381
- const green = Math.round(g * 255);
382
- const blue = Math.round(b * 255);
383
- return `rgb(${red}, ${green}, ${blue})`;
384
- }
385
-
386
- function toggleZones(serial) {
387
- const element = document.getElementById(`zones-${serial}`);
388
- const toggle = document.getElementById(`zones-toggle-${serial}`);
389
- if (element && toggle) {
390
- const isShown = element.classList.toggle('show');
391
- // Update toggle icon
392
- toggle.textContent = isShown
393
- ? toggle.textContent.replace('▸', '▾')
394
- : toggle.textContent.replace('▾', '▸');
395
- // Save state to localStorage
396
- localStorage.setItem(`zones-${serial}`, isShown ? 'show' : 'hide');
397
- }
398
- }
399
-
400
- function toggleMetadata(serial) {
401
- const element = document.getElementById(`metadata-${serial}`);
402
- const toggle = document.getElementById(`metadata-toggle-${serial}`);
403
- if (element && toggle) {
404
- const isShown = element.classList.toggle('show');
405
- // Update toggle icon
406
- toggle.textContent = isShown
407
- ? toggle.textContent.replace('▸', '▾')
408
- : toggle.textContent.replace('▾', '▸');
409
- // Save state to localStorage
410
- localStorage.setItem(`metadata-${serial}`, isShown ? 'show' : 'hide');
411
- }
412
- }
413
-
414
- function restoreToggleStates(serial) {
415
- // Restore zones toggle state
416
- const zonesState = localStorage.getItem(`zones-${serial}`);
417
- if (zonesState === 'show') {
418
- const element = document.getElementById(`zones-${serial}`);
419
- const toggle = document.getElementById(`zones-toggle-${serial}`);
420
- if (element && toggle) {
421
- element.classList.add('show');
422
- toggle.textContent = toggle.textContent.replace('▸', '▾');
423
- }
424
- }
425
-
426
- // Restore metadata toggle state
427
- const metadataState = localStorage.getItem(`metadata-${serial}`);
428
- if (metadataState === 'show') {
429
- const element = document.getElementById(`metadata-${serial}`);
430
- const toggle = document.getElementById(`metadata-toggle-${serial}`);
431
- if (element && toggle) {
432
- element.classList.add('show');
433
- toggle.textContent = toggle.textContent.replace('▸', '▾');
434
- }
435
- }
436
- }
437
-
438
- async function fetchStats() {
439
- try {
440
- const response = await fetch('/api/stats');
441
- if (!response.ok) {
442
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
443
- }
444
- const stats = await response.json();
445
-
446
- const uptimeValue = Math.floor(stats.uptime_seconds);
447
- const statsHtml = `
448
- <div class="stat">
449
- <span class="stat-label">Uptime</span>
450
- <span class="stat-value">${uptimeValue}s</span>
451
- </div>
452
- <div class="stat">
453
- <span class="stat-label">Devices</span>
454
- <span class="stat-value">${stats.device_count}</span>
455
- </div>
456
- <div class="stat">
457
- <span class="stat-label">Packets RX</span>
458
- <span class="stat-value">${stats.packets_received}</span>
459
- </div>
460
- <div class="stat">
461
- <span class="stat-label">Packets TX</span>
462
- <span class="stat-value">${stats.packets_sent}</span>
463
- </div>
464
- <div class="stat">
465
- <span class="stat-label">Errors</span>
466
- <span class="stat-value">${stats.error_count}</span>
467
- </div>
468
- `;
469
- document.getElementById('stats').innerHTML = statsHtml;
470
-
471
- // Show/hide activity log based on server configuration
472
- const activityCard = document.getElementById('activity-card');
473
- if (activityCard) {
474
- const displayValue = (
475
- stats.activity_enabled ? 'block' : 'none'
476
- );
477
- activityCard.style.display = displayValue;
478
- }
479
-
480
- return stats.activity_enabled;
481
- } catch (error) {
482
- console.error('Failed to fetch stats:', error);
483
- const errorLabelStyle = 'color: #d32f2f;';
484
- const errorHtml = `
485
- <div class="stat">
486
- <span class="stat-label" style="${errorLabelStyle}">
487
- Error loading stats
488
- </span>
489
- <span class="stat-value">${error.message}</span>
490
- </div>
491
- `;
492
- document.getElementById('stats').innerHTML = errorHtml;
493
- return false;
494
- }
495
- }
496
-
497
- async function fetchDevices() {
498
- try {
499
- const response = await fetch('/api/devices');
500
- if (!response.ok) {
501
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
502
- }
503
- const devices = await response.json();
504
-
505
- document.getElementById('device-count').textContent = devices.length;
506
-
507
- if (devices.length === 0) {
508
- const noDevicesHtml = (
509
- '<div class="no-devices">No devices emulated</div>'
510
- );
511
- document.getElementById('devices').innerHTML = noDevicesHtml;
512
- return;
513
- }
514
-
515
- const devicesHtml = devices.map(dev => {
516
- const capabilities = [];
517
- const capabilityBadges = [];
518
-
519
- if (dev.has_color) capabilities.push('color');
520
- if (dev.has_infrared) capabilities.push('IR');
521
-
522
- // Show extended-mz badge instead of multizone when both are present
523
- if (dev.has_extended_multizone) {
524
- const badgeHtml = (
525
- '<span class="badge badge-extended-mz">' +
526
- `extended-mz×${dev.zone_count}</span>`
527
- );
528
- capabilityBadges.push(badgeHtml);
529
- } else if (dev.has_multizone) {
530
- capabilities.push(`multizone×${dev.zone_count}`);
531
- }
532
-
533
- if (dev.has_matrix) capabilities.push(`matrix×${dev.tile_count}`);
534
- if (dev.has_hev) capabilities.push('HEV');
535
-
536
- const powerBadge = dev.power_level > 0
537
- ? '<span class="badge badge-power-on">ON</span>'
538
- : '<span class="badge badge-power-off">OFF</span>';
539
-
540
- // Generate capabilities list for metadata
541
- const capabilitiesMetadata = [];
542
- if (dev.has_color) capabilitiesMetadata.push('Color');
543
- if (dev.has_infrared) {
544
- capabilitiesMetadata.push('Infrared');
545
- }
546
- if (dev.has_multizone) {
547
- capabilitiesMetadata.push(
548
- `Multizone (${dev.zone_count} zones)`
549
- );
550
- }
551
- if (dev.has_extended_multizone) {
552
- capabilitiesMetadata.push('Extended Multizone');
553
- }
554
- if (dev.has_matrix) {
555
- capabilitiesMetadata.push(
556
- `Matrix (${dev.tile_count} tiles)`
557
- );
558
- }
559
- if (dev.has_hev) capabilitiesMetadata.push('HEV/Clean');
560
- const capabilitiesText = (
561
- capabilitiesMetadata.join(', ') || 'None'
562
- );
563
-
564
- // Generate metadata display
565
- const uptimeSeconds = Math.floor(dev.uptime_ns / 1e9);
566
- const metaToggleId = `metadata-toggle-${dev.serial}`;
567
- const metaToggleClick = `toggleMetadata('${dev.serial}')`;
568
- const metadataHtml = `
569
- <div
570
- class="metadata-toggle"
571
- id="${metaToggleId}"
572
- onclick="${metaToggleClick}"
573
- >
574
- ▸ Show metadata
575
- </div>
576
- <div id="metadata-${dev.serial}" class="metadata-display">
577
- <div class="metadata-row">
578
- <span class="metadata-label">Firmware:</span>
579
- <span class="metadata-value">
580
- ${dev.version_major}.${dev.version_minor}
581
- </span>
582
- </div>
583
- <div class="metadata-row">
584
- <span class="metadata-label">Vendor:</span>
585
- <span class="metadata-value">${dev.vendor}</span>
586
- </div>
587
- <div class="metadata-row">
588
- <span class="metadata-label">Product:</span>
589
- <span class="metadata-value">${dev.product}</span>
590
- </div>
591
- <div class="metadata-row">
592
- <span class="metadata-label">Capabilities:</span>
593
- <span
594
- class="metadata-value"
595
- style="color: #4a9eff;"
596
- >${capabilitiesText}</span>
597
- </div>
598
- <div class="metadata-row">
599
- <span class="metadata-label">Group:</span>
600
- <span class="metadata-value">${dev.group_label}</span>
601
- </div>
602
- <div class="metadata-row">
603
- <span class="metadata-label">Location:</span>
604
- <span class="metadata-value">${dev.location_label}</span>
605
- </div>
606
- <div class="metadata-row">
607
- <span class="metadata-label">Uptime:</span>
608
- <span class="metadata-value">${uptimeSeconds}s</span>
609
- </div>
610
- <div class="metadata-row">
611
- <span class="metadata-label">WiFi Signal:</span>
612
- <span class="metadata-value">
613
- ${dev.wifi_signal.toFixed(1)} dBm
614
- </span>
615
- </div>
616
- </div>
617
- `;
618
-
619
- // Generate zones display
620
- let zonesHtml = '';
621
- if (dev.has_multizone && dev.zone_colors &&
622
- dev.zone_colors.length > 0
623
- ) {
624
- const zoneSegments = dev.zone_colors.map(color => {
625
- const rgb = hsbkToRgb(color);
626
- const bgStyle = `background: ${rgb};`;
627
- return `<div class="zone-segment" style="${bgStyle}"></div>`;
628
- }).join('');
629
-
630
- const zoneCount = dev.zone_colors.length;
631
- const toggleId = `zones-toggle-${dev.serial}`;
632
- const toggleClick = `toggleZones('${dev.serial}')`;
633
- zonesHtml = `
634
- <div
635
- class="zones-toggle"
636
- id="${toggleId}"
637
- onclick="${toggleClick}"
638
- >
639
- ▸ Show zones (${zoneCount})
640
- </div>
641
- <div id="zones-${dev.serial}" class="zones-display">
642
- <div class="zone-strip">${zoneSegments}</div>
643
- </div>
644
- `;
645
- } else if (dev.has_matrix && dev.tile_devices &&
646
- dev.tile_devices.length > 0) {
647
- // Render actual tile zones
648
- const tilesHtml = dev.tile_devices.map((tile, tileIndex) => {
649
- if (!tile.colors || tile.colors.length === 0) {
650
- return '<div style="color: #666;">No color data</div>';
651
- }
652
-
653
- const width = tile.width || 8;
654
- const height = tile.height || 8;
655
- const totalzones = width * height;
656
-
657
- // Create grid of zones
658
- const slicedColors = tile.colors.slice(0, totalzones);
659
- const zonesHtml = slicedColors.map(color => {
660
- const rgb = hsbkToRgb(color);
661
- const bgStyle = `background: ${rgb};`;
662
- return `<div class="tile-zone" style="${bgStyle}"></div>`;
663
- }).join('');
664
-
665
- const labelStyle = (
666
- 'font-size: 0.7em; color: #666; ' +
667
- 'margin-bottom: 2px; text-align: center;'
668
- );
669
- const gridStyle = (
670
- `grid-template-columns: repeat(${width}, 8px);`
671
- );
672
- return `
673
- <div class="tile-item">
674
- <div style="${labelStyle}">
675
- T${tileIndex + 1}
676
- </div>
677
- <div class="tile-grid" style="${gridStyle}">
678
- ${zonesHtml}
679
- </div>
680
- </div>
681
- `;
682
- }).join('');
683
-
684
- const tileCount = dev.tile_devices.length;
685
- const toggleId = `zones-toggle-${dev.serial}`;
686
- const toggleClick = `toggleZones('${dev.serial}')`;
687
- zonesHtml = `
688
- <div
689
- class="zones-toggle"
690
- id="${toggleId}"
691
- onclick="${toggleClick}"
692
- >
693
- ▸ Show tiles (${tileCount})
694
- </div>
695
- <div id="zones-${dev.serial}" class="zones-display">
696
- <div class="tiles-container">
697
- ${tilesHtml}
698
- </div>
699
- </div>
700
- `;
701
- } else if (dev.has_color && dev.color) {
702
- const rgb = hsbkToRgb(dev.color);
703
- const swatchStyle = `background: ${rgb};`;
704
- const textStyle = 'color: #888; font-size: 0.75em;';
705
- zonesHtml = `
706
- <div style="margin-top: 4px;">
707
- <span class="color-swatch" style="${swatchStyle}"></span>
708
- <span style="${textStyle}">Current color</span>
709
- </div>
710
- `;
711
- }
712
-
713
- return `
714
- <div class="device">
715
- <div class="device-header">
716
- <div>
717
- <div class="device-serial">${dev.serial}</div>
718
- <div class="device-label">${dev.label}</div>
719
- </div>
720
- <button
721
- class="btn btn-delete"
722
- onclick="deleteDevice('${dev.serial}')"
723
- >Del</button>
724
- </div>
725
- <div>
726
- ${powerBadge}
727
- <span class="badge badge-capability">P${dev.product}</span>
728
- ${capabilities.map(c => (
729
- `<span class="badge badge-capability">${c}</span>`
730
- )).join('')}
731
- ${capabilityBadges.join('')}
732
- </div>
733
- ${metadataHtml}
734
- ${zonesHtml}
735
- </div>
736
- `;
737
- }).join('');
738
-
739
- document.getElementById('devices').innerHTML = devicesHtml;
740
-
741
- // Restore toggle states for all devices
742
- devices.forEach(dev => restoreToggleStates(dev.serial));
743
- } catch (error) {
744
- console.error('Failed to fetch devices:', error);
745
- const errorStyle = 'color: #d32f2f;';
746
- const errorHtml = (
747
- `<div class="no-devices" style="${errorStyle}">` +
748
- `Error loading devices: ${error.message}</div>`
749
- );
750
- document.getElementById('devices').innerHTML = errorHtml;
751
- }
752
- }
753
-
754
- async function fetchActivity() {
755
- try {
756
- const response = await fetch('/api/activity');
757
- if (!response.ok) {
758
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
759
- }
760
- const activities = await response.json();
761
-
762
- const activityHtml = activities.slice().reverse().map(act => {
763
- const timestamp = act.timestamp * 1000;
764
- const time = new Date(timestamp).toLocaleTimeString();
765
- const isRx = act.direction === 'rx';
766
- const dirClass = isRx ? 'activity-rx' : 'activity-tx';
767
- const dirLabel = isRx ? 'RX' : 'TX';
768
- const device = act.device || act.target || 'N/A';
769
-
770
- return `
771
- <div class="activity-item">
772
- <span class="activity-time">${time}</span>
773
- <span class="${dirClass}">${dirLabel}</span>
774
- <span class="activity-packet">${act.packet_name}</span>
775
- <span class="device-serial">${device}</span>
776
- <span style="color: #666">${act.addr}</span>
777
- </div>
778
- `;
779
- }).join('');
780
-
781
- const noActivity = '<div style="color: #666">No activity yet</div>';
782
- const logElement = document.getElementById('activity-log');
783
- logElement.innerHTML = activityHtml || noActivity;
784
- } catch (error) {
785
- console.error('Failed to fetch activity:', error);
786
- const errorStyle = 'color: #d32f2f;';
787
- const errorHtml = (
788
- `<div style="${errorStyle}">` +
789
- `Error loading activity: ${error.message}</div>`
790
- );
791
- document.getElementById('activity-log').innerHTML = errorHtml;
792
- }
793
- }
794
-
795
- async function deleteDevice(serial) {
796
- if (!confirm(`Delete device ${serial}?`)) return;
797
-
798
- const response = await fetch(`/api/devices/${serial}`, {
799
- method: 'DELETE'
800
- });
801
-
802
- if (response.ok) {
803
- await updateAll();
804
- } else {
805
- alert('Failed to delete device');
806
- }
807
- }
808
-
809
- async function removeAllDevices() {
810
- const deviceCount = document.getElementById('device-count').textContent;
811
- if (deviceCount === '0') {
812
- alert('No devices to remove');
813
- return;
814
- }
815
-
816
- const line1 = (
817
- `Remove all ${deviceCount} device(s) from the server?\\n\\n`
818
- );
819
- const line2 = (
820
- 'This will stop all devices from ' +
821
- 'responding to LIFX protocol packets, '
822
- );
823
- const line3 = 'but will not delete persistent storage.';
824
- const confirmMsg = line1 + line2 + line3;
825
- if (!confirm(confirmMsg)) return;
826
-
827
- const response = await fetch('/api/devices', {
828
- method: 'DELETE'
829
- });
830
-
831
- if (response.ok) {
832
- const result = await response.json();
833
- alert(result.message);
834
- await updateAll();
835
- } else {
836
- alert('Failed to remove all devices');
837
- }
838
- }
839
-
840
- async function clearStorage() {
841
- const confirmMsg = `Clear all persistent device state from storage?\\n\\n` +
842
- `This will permanently delete all saved device state files. ` +
843
- `Currently running devices will not be affected.\\n\\n` +
844
- `This action cannot be undone.`;
845
- if (!confirm(confirmMsg)) return;
846
-
847
- const response = await fetch('/api/storage', {
848
- method: 'DELETE'
849
- });
850
-
851
- if (response.ok) {
852
- const result = await response.json();
853
- alert(result.message);
854
- } else if (response.status === 503) {
855
- alert('Persistent storage is not enabled on this server');
856
- } else {
857
- alert('Failed to clear storage');
858
- }
859
- }
860
-
861
- const addDeviceForm = document.getElementById('add-device-form');
862
- addDeviceForm.addEventListener('submit', async (e) => {
863
- e.preventDefault();
864
-
865
- const productId = parseInt(document.getElementById('product-id').value);
866
-
867
- const response = await fetch('/api/devices', {
868
- method: 'POST',
869
- headers: {
870
- 'Content-Type': 'application/json'
871
- },
872
- body: JSON.stringify({ product_id: productId })
873
- });
874
-
875
- if (response.ok) {
876
- await updateAll();
877
- } else {
878
- const error = await response.json();
879
- alert(`Failed to create device: ${error.detail}`);
880
- }
881
- });
882
-
883
- async function updateAll() {
884
- const activityEnabled = await fetchStats();
885
- const tasks = [fetchDevices()];
886
- if (activityEnabled) {
887
- tasks.push(fetchActivity());
888
- }
889
- await Promise.all(tasks);
890
- }
891
-
892
- // Initial load
893
- updateAll();
894
-
895
- // Auto-refresh every 2 seconds
896
- updateInterval = setInterval(updateAll, 2000);
897
- </script>
898
- </body>
899
- </html>