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,381 +0,0 @@
1
- """Focused state dataclasses following Single Responsibility Principle."""
2
-
3
- from __future__ import annotations
4
-
5
- import time
6
- import uuid
7
- from dataclasses import dataclass, field
8
- from typing import Any
9
-
10
- from lifx_emulator.constants import LIFX_UDP_PORT
11
- from lifx_emulator.protocol.protocol_types import LightHsbk
12
-
13
-
14
- @dataclass
15
- class CoreDeviceState:
16
- """Core device identification and basic state."""
17
-
18
- serial: str
19
- label: str
20
- power_level: int
21
- color: LightHsbk
22
- vendor: int
23
- product: int
24
- version_major: int
25
- version_minor: int
26
- build_timestamp: int
27
- uptime_ns: int = 0
28
- mac_address: bytes = field(default_factory=lambda: bytes.fromhex("d073d5123456"))
29
- port: int = LIFX_UDP_PORT
30
-
31
-
32
- @dataclass
33
- class NetworkState:
34
- """Network and connectivity state."""
35
-
36
- wifi_signal: float = -45.0
37
-
38
-
39
- @dataclass
40
- class LocationState:
41
- """Device location metadata."""
42
-
43
- location_id: bytes = field(default_factory=lambda: uuid.uuid4().bytes)
44
- location_label: str = "Test Location"
45
- location_updated_at: int = field(default_factory=lambda: int(time.time() * 1e9))
46
-
47
-
48
- @dataclass
49
- class GroupState:
50
- """Device group metadata."""
51
-
52
- group_id: bytes = field(default_factory=lambda: uuid.uuid4().bytes)
53
- group_label: str = "Test Group"
54
- group_updated_at: int = field(default_factory=lambda: int(time.time() * 1e9))
55
-
56
-
57
- @dataclass
58
- class InfraredState:
59
- """Infrared capability state."""
60
-
61
- infrared_brightness: int = 0 # 0-65535
62
-
63
-
64
- @dataclass
65
- class HevState:
66
- """HEV (germicidal UV) capability state."""
67
-
68
- hev_cycle_duration_s: int = 7200 # 2 hours default
69
- hev_cycle_remaining_s: int = 0
70
- hev_cycle_last_power: bool = False
71
- hev_indication: bool = True
72
- hev_last_result: int = 0 # 0=success
73
-
74
-
75
- @dataclass
76
- class MultiZoneState:
77
- """Multizone (strip/beam) capability state."""
78
-
79
- zone_count: int
80
- zone_colors: list[LightHsbk]
81
- effect_type: int = 0 # 0=OFF, 1=MOVE, 2=RESERVED
82
- effect_speed: int = 5 # Duration of one cycle in seconds
83
-
84
-
85
- @dataclass
86
- class TileFramebuffers:
87
- """Internal storage for non-visible tile framebuffers (1-7).
88
-
89
- Framebuffer 0 is stored in tile_devices[i]["colors"] (the visible buffer).
90
- Framebuffers 1-7 are stored here for Set64/CopyFrameBuffer operations.
91
- Each framebuffer is a list of LightHsbk colors with length = width * height.
92
- """
93
-
94
- tile_index: int # Which tile this belongs to
95
- framebuffers: dict[int, list[LightHsbk]] = field(default_factory=dict)
96
-
97
- def get_framebuffer(
98
- self, fb_index: int, width: int, height: int
99
- ) -> list[LightHsbk]:
100
- """Get framebuffer by index, creating it if needed."""
101
- if fb_index not in self.framebuffers:
102
- # Initialize with default black color
103
- zones = width * height
104
- self.framebuffers[fb_index] = [
105
- LightHsbk(hue=0, saturation=0, brightness=0, kelvin=3500)
106
- for _ in range(zones)
107
- ]
108
- return self.framebuffers[fb_index]
109
-
110
-
111
- @dataclass
112
- class MatrixState:
113
- """Matrix (tile/candle) capability state."""
114
-
115
- tile_count: int
116
- tile_devices: list[dict[str, Any]]
117
- tile_width: int
118
- tile_height: int
119
- effect_type: int = 0 # 0=OFF, 2=MORPH, 3=FLAME, 5=SKY
120
- effect_speed: int = 5 # Duration of one cycle in seconds
121
- effect_palette_count: int = 0
122
- effect_palette: list[LightHsbk] = field(default_factory=list)
123
- effect_sky_type: int = 0 # 0=SUNRISE, 1=SUNSET, 2=CLOUDS (only when effect_type=5)
124
- effect_cloud_sat_min: int = (
125
- 0 # Min cloud saturation 0-200 (only when effect_type=5)
126
- )
127
- effect_cloud_sat_max: int = (
128
- 0 # Max cloud saturation 0-200 (only when effect_type=5)
129
- )
130
- # Internal storage for non-visible framebuffers (1-7) per tile
131
- # Framebuffer 0 remains in tile_devices[i]["colors"]
132
- tile_framebuffers: list[TileFramebuffers] = field(default_factory=list)
133
-
134
-
135
- @dataclass
136
- class WaveformState:
137
- """Waveform effect state."""
138
-
139
- waveform_active: bool = False
140
- waveform_type: int = 0
141
- waveform_transient: bool = False
142
- waveform_color: LightHsbk = field(
143
- default_factory=lambda: LightHsbk(
144
- hue=0, saturation=0, brightness=0, kelvin=3500
145
- )
146
- )
147
- waveform_period_ms: int = 0
148
- waveform_cycles: float = 0
149
- waveform_duty_cycle: int = 0
150
- waveform_skew_ratio: int = 0
151
-
152
-
153
- @dataclass
154
- class DeviceState:
155
- """Composed device state following Single Responsibility Principle.
156
-
157
- Each aspect of device state is managed by a focused sub-state object.
158
- Properties are automatically delegated to the appropriate state object
159
- using __getattr__ and __setattr__ magic methods.
160
-
161
- Examples:
162
- >>> state.label # Delegates to state.core.label
163
- >>> state.location_label # Delegates to state.location.location_label
164
- >>> state.zone_count # Delegates to state.multizone.zone_count (if present)
165
- """
166
-
167
- core: CoreDeviceState
168
- network: NetworkState
169
- location: LocationState
170
- group: GroupState
171
- waveform: WaveformState
172
-
173
- # Optional capability-specific state
174
- infrared: InfraredState | None = None
175
- hev: HevState | None = None
176
- multizone: MultiZoneState | None = None
177
- matrix: MatrixState | None = None
178
-
179
- # Capability flags (kept for convenience)
180
- has_color: bool = True
181
- has_infrared: bool = False
182
- has_multizone: bool = False
183
- has_extended_multizone: bool = False
184
- has_matrix: bool = False
185
- has_hev: bool = False
186
- has_relays: bool = False
187
- has_buttons: bool = False
188
-
189
- # Attribute routing map: maps attribute prefixes to state objects
190
- # This eliminates ~360 lines of property boilerplate
191
- _ATTRIBUTE_ROUTES = {
192
- # Core properties (no prefix)
193
- "serial": "core",
194
- "label": "core",
195
- "power_level": "core",
196
- "color": "core",
197
- "vendor": "core",
198
- "product": "core",
199
- "version_major": "core",
200
- "version_minor": "core",
201
- "build_timestamp": "core",
202
- "uptime_ns": "core",
203
- "mac_address": "core",
204
- "port": "core",
205
- # Network properties
206
- "wifi_signal": "network",
207
- # Location properties
208
- "location_id": "location",
209
- "location_label": "location",
210
- "location_updated_at": "location",
211
- # Group properties
212
- "group_id": "group",
213
- "group_label": "group",
214
- "group_updated_at": "group",
215
- # Waveform properties
216
- "waveform_active": "waveform",
217
- "waveform_type": "waveform",
218
- "waveform_transient": "waveform",
219
- "waveform_color": "waveform",
220
- "waveform_period_ms": "waveform",
221
- "waveform_cycles": "waveform",
222
- "waveform_duty_cycle": "waveform",
223
- "waveform_skew_ratio": "waveform",
224
- # Infrared properties
225
- "infrared_brightness": "infrared",
226
- # HEV properties
227
- "hev_cycle_duration_s": "hev",
228
- "hev_cycle_remaining_s": "hev",
229
- "hev_cycle_last_power": "hev",
230
- "hev_indication": "hev",
231
- "hev_last_result": "hev",
232
- # Multizone properties
233
- "zone_count": "multizone",
234
- "zone_colors": "multizone",
235
- "multizone_effect_type": ("multizone", "effect_type"),
236
- "multizone_effect_speed": ("multizone", "effect_speed"),
237
- # Matrix/Tile properties
238
- "tile_count": "matrix",
239
- "tile_devices": "matrix",
240
- "tile_width": "matrix",
241
- "tile_height": "matrix",
242
- "tile_effect_type": ("matrix", "effect_type"),
243
- "tile_effect_speed": ("matrix", "effect_speed"),
244
- "tile_effect_palette_count": ("matrix", "effect_palette_count"),
245
- "tile_effect_palette": ("matrix", "effect_palette"),
246
- "tile_effect_sky_type": ("matrix", "effect_sky_type"),
247
- "tile_effect_cloud_sat_min": ("matrix", "effect_cloud_sat_min"),
248
- "tile_effect_cloud_sat_max": ("matrix", "effect_cloud_sat_max"),
249
- "tile_framebuffers": "matrix",
250
- }
251
-
252
- # Default values for optional state attributes when state object is None
253
- _OPTIONAL_DEFAULTS = {
254
- "infrared_brightness": 0,
255
- "hev_cycle_duration_s": 0,
256
- "hev_cycle_remaining_s": 0,
257
- "hev_cycle_last_power": False,
258
- "hev_indication": False,
259
- "hev_last_result": 0,
260
- "zone_count": 0,
261
- "zone_colors": [],
262
- "multizone_effect_type": 0,
263
- "multizone_effect_speed": 0,
264
- "tile_count": 0,
265
- "tile_devices": [],
266
- "tile_width": 8,
267
- "tile_height": 8,
268
- "tile_effect_type": 0,
269
- "tile_effect_speed": 0,
270
- "tile_effect_palette_count": 0,
271
- "tile_effect_palette": [],
272
- "tile_effect_sky_type": 0,
273
- "tile_effect_cloud_sat_min": 0,
274
- "tile_effect_cloud_sat_max": 0,
275
- "tile_framebuffers": [],
276
- }
277
-
278
- def get_target_bytes(self) -> bytes:
279
- """Get target bytes for this device."""
280
- return bytes.fromhex(self.core.serial) + b"\x00\x00"
281
-
282
- def __getattr__(self, name: str) -> Any:
283
- """Dynamically delegate attribute access to appropriate state object.
284
-
285
- This eliminates ~180 lines of @property boilerplate.
286
-
287
- Args:
288
- name: Attribute name being accessed
289
-
290
- Returns:
291
- Attribute value from the appropriate state object
292
-
293
- Raises:
294
- AttributeError: If attribute is not found
295
- """
296
- # Check if this attribute has a routing rule
297
- if name in self._ATTRIBUTE_ROUTES:
298
- route = self._ATTRIBUTE_ROUTES[name]
299
-
300
- # Route can be either 'state_name' or ('state_name', 'attr_name')
301
- if isinstance(route, tuple):
302
- state_name, attr_name = route
303
- else:
304
- state_name = route
305
- attr_name = name
306
-
307
- # Get the state object
308
- state_obj = object.__getattribute__(self, state_name)
309
-
310
- # Handle optional state objects (infrared, hev, multizone, matrix)
311
- if state_obj is None:
312
- # Return default value for optional attributes
313
- return self._OPTIONAL_DEFAULTS.get(name)
314
-
315
- # Delegate to the state object
316
- return getattr(state_obj, attr_name)
317
-
318
- # If not in routing map, raise AttributeError
319
- raise AttributeError(
320
- f"'{type(self).__name__}' object has no attribute '{name}'"
321
- )
322
-
323
- def __setattr__(self, name: str, value: Any) -> None:
324
- """Dynamically delegate attribute writes to appropriate state object.
325
-
326
- This eliminates ~180 lines of @property.setter boilerplate.
327
-
328
- Args:
329
- name: Attribute name being set
330
- value: Value to set
331
-
332
- Note:
333
- Dataclass fields and private attributes bypass delegation.
334
- """
335
- # Dataclass fields and private attributes use normal assignment
336
- if name in {
337
- "core",
338
- "network",
339
- "location",
340
- "group",
341
- "waveform",
342
- "infrared",
343
- "hev",
344
- "multizone",
345
- "matrix",
346
- "has_color",
347
- "has_infrared",
348
- "has_multizone",
349
- "has_extended_multizone",
350
- "has_matrix",
351
- "has_hev",
352
- "has_relays",
353
- "has_buttons",
354
- } or name.startswith("_"):
355
- object.__setattr__(self, name, value)
356
- return
357
-
358
- # Check if this attribute has a routing rule
359
- if name in self._ATTRIBUTE_ROUTES:
360
- route = self._ATTRIBUTE_ROUTES[name]
361
-
362
- # Route can be either 'state_name' or ('state_name', 'attr_name')
363
- if isinstance(route, tuple):
364
- state_name, attr_name = route
365
- else:
366
- state_name = route
367
- attr_name = name
368
-
369
- # Get the state object
370
- state_obj = object.__getattribute__(self, state_name)
371
-
372
- # Handle optional state objects - silently ignore writes if None
373
- if state_obj is None:
374
- return
375
-
376
- # Delegate to the state object
377
- setattr(state_obj, attr_name, value)
378
- return
379
-
380
- # For unknown attributes, use normal assignment (allows adding new attributes)
381
- object.__setattr__(self, name, value)
@@ -1,39 +0,0 @@
1
- """Device factory for creating emulated LIFX devices.
2
-
3
- This package provides a clean, testable API for creating LIFX devices using:
4
- - Builder pattern for flexible device construction
5
- - Separate services for serial generation, color config, firmware config
6
- - Product registry integration for accurate device specifications
7
- """
8
-
9
- from lifx_emulator.factories.builder import DeviceBuilder
10
- from lifx_emulator.factories.default_config import DefaultColorConfig
11
- from lifx_emulator.factories.factory import (
12
- create_color_light,
13
- create_color_temperature_light,
14
- create_device,
15
- create_hev_light,
16
- create_infrared_light,
17
- create_multizone_light,
18
- create_switch,
19
- create_tile_device,
20
- )
21
- from lifx_emulator.factories.firmware_config import FirmwareConfig
22
- from lifx_emulator.factories.serial_generator import SerialGenerator
23
-
24
- __all__ = [
25
- # Builder and helpers
26
- "DeviceBuilder",
27
- "SerialGenerator",
28
- "DefaultColorConfig",
29
- "FirmwareConfig",
30
- # Factory functions
31
- "create_device",
32
- "create_color_light",
33
- "create_infrared_light",
34
- "create_hev_light",
35
- "create_multizone_light",
36
- "create_switch",
37
- "create_tile_device",
38
- "create_color_temperature_light",
39
- ]