lifx-emulator 1.0.2__py3-none-any.whl → 2.0.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 (57) hide show
  1. lifx_emulator/__init__.py +1 -1
  2. lifx_emulator/__main__.py +26 -51
  3. lifx_emulator/api/__init__.py +18 -0
  4. lifx_emulator/api/app.py +154 -0
  5. lifx_emulator/api/mappers/__init__.py +5 -0
  6. lifx_emulator/api/mappers/device_mapper.py +114 -0
  7. lifx_emulator/api/models.py +133 -0
  8. lifx_emulator/api/routers/__init__.py +11 -0
  9. lifx_emulator/api/routers/devices.py +130 -0
  10. lifx_emulator/api/routers/monitoring.py +52 -0
  11. lifx_emulator/api/routers/scenarios.py +247 -0
  12. lifx_emulator/api/services/__init__.py +8 -0
  13. lifx_emulator/api/services/device_service.py +198 -0
  14. lifx_emulator/{api.py → api/templates/dashboard.html} +0 -942
  15. lifx_emulator/devices/__init__.py +37 -0
  16. lifx_emulator/devices/device.py +333 -0
  17. lifx_emulator/devices/manager.py +256 -0
  18. lifx_emulator/{async_storage.py → devices/persistence.py} +3 -3
  19. lifx_emulator/{state_restorer.py → devices/state_restorer.py} +2 -2
  20. lifx_emulator/devices/states.py +333 -0
  21. lifx_emulator/factories/__init__.py +37 -0
  22. lifx_emulator/factories/builder.py +371 -0
  23. lifx_emulator/factories/default_config.py +158 -0
  24. lifx_emulator/factories/factory.py +221 -0
  25. lifx_emulator/factories/firmware_config.py +59 -0
  26. lifx_emulator/factories/serial_generator.py +82 -0
  27. lifx_emulator/handlers/base.py +1 -1
  28. lifx_emulator/handlers/device_handlers.py +10 -28
  29. lifx_emulator/handlers/light_handlers.py +5 -9
  30. lifx_emulator/handlers/multizone_handlers.py +1 -1
  31. lifx_emulator/handlers/tile_handlers.py +1 -1
  32. lifx_emulator/products/generator.py +389 -170
  33. lifx_emulator/products/registry.py +52 -40
  34. lifx_emulator/products/specs.py +12 -13
  35. lifx_emulator/protocol/base.py +115 -61
  36. lifx_emulator/protocol/generator.py +18 -5
  37. lifx_emulator/protocol/packets.py +7 -7
  38. lifx_emulator/repositories/__init__.py +22 -0
  39. lifx_emulator/repositories/device_repository.py +155 -0
  40. lifx_emulator/repositories/storage_backend.py +107 -0
  41. lifx_emulator/scenarios/__init__.py +22 -0
  42. lifx_emulator/{scenario_manager.py → scenarios/manager.py} +11 -91
  43. lifx_emulator/scenarios/models.py +112 -0
  44. lifx_emulator/{scenario_persistence.py → scenarios/persistence.py} +82 -47
  45. lifx_emulator/server.py +38 -64
  46. {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.0.0.dist-info}/METADATA +1 -1
  47. lifx_emulator-2.0.0.dist-info/RECORD +62 -0
  48. lifx_emulator/device.py +0 -750
  49. lifx_emulator/device_states.py +0 -114
  50. lifx_emulator/factories.py +0 -380
  51. lifx_emulator/storage_protocol.py +0 -100
  52. lifx_emulator-1.0.2.dist-info/RECORD +0 -40
  53. /lifx_emulator/{observers.py → devices/observers.py} +0 -0
  54. /lifx_emulator/{state_serializer.py → devices/state_serializer.py} +0 -0
  55. {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.0.0.dist-info}/WHEEL +0 -0
  56. {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.0.0.dist-info}/entry_points.txt +0 -0
  57. {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,333 @@
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 MatrixState:
87
+ """Matrix (tile/candle) capability state."""
88
+
89
+ tile_count: int
90
+ tile_devices: list[dict[str, Any]]
91
+ tile_width: int
92
+ tile_height: int
93
+ effect_type: int = 0 # 0=OFF, 2=MORPH, 3=FLAME
94
+ effect_speed: int = 5 # Duration of one cycle in seconds
95
+ effect_palette_count: int = 0
96
+ effect_palette: list[LightHsbk] = field(default_factory=list)
97
+
98
+
99
+ @dataclass
100
+ class WaveformState:
101
+ """Waveform effect state."""
102
+
103
+ waveform_active: bool = False
104
+ waveform_type: int = 0
105
+ waveform_transient: bool = False
106
+ waveform_color: LightHsbk = field(
107
+ default_factory=lambda: LightHsbk(
108
+ hue=0, saturation=0, brightness=0, kelvin=3500
109
+ )
110
+ )
111
+ waveform_period_ms: int = 0
112
+ waveform_cycles: float = 0
113
+ waveform_duty_cycle: int = 0
114
+ waveform_skew_ratio: int = 0
115
+
116
+
117
+ @dataclass
118
+ class DeviceState:
119
+ """Composed device state following Single Responsibility Principle.
120
+
121
+ Each aspect of device state is managed by a focused sub-state object.
122
+ Properties are automatically delegated to the appropriate state object
123
+ using __getattr__ and __setattr__ magic methods.
124
+
125
+ Examples:
126
+ >>> state.label # Delegates to state.core.label
127
+ >>> state.location_label # Delegates to state.location.location_label
128
+ >>> state.zone_count # Delegates to state.multizone.zone_count (if present)
129
+ """
130
+
131
+ core: CoreDeviceState
132
+ network: NetworkState
133
+ location: LocationState
134
+ group: GroupState
135
+ waveform: WaveformState
136
+
137
+ # Optional capability-specific state
138
+ infrared: InfraredState | None = None
139
+ hev: HevState | None = None
140
+ multizone: MultiZoneState | None = None
141
+ matrix: MatrixState | None = None
142
+
143
+ # Capability flags (kept for convenience)
144
+ has_color: bool = True
145
+ has_infrared: bool = False
146
+ has_multizone: bool = False
147
+ has_extended_multizone: bool = False
148
+ has_matrix: bool = False
149
+ has_hev: bool = False
150
+
151
+ # Attribute routing map: maps attribute prefixes to state objects
152
+ # This eliminates ~360 lines of property boilerplate
153
+ _ATTRIBUTE_ROUTES = {
154
+ # Core properties (no prefix)
155
+ "serial": "core",
156
+ "label": "core",
157
+ "power_level": "core",
158
+ "color": "core",
159
+ "vendor": "core",
160
+ "product": "core",
161
+ "version_major": "core",
162
+ "version_minor": "core",
163
+ "build_timestamp": "core",
164
+ "uptime_ns": "core",
165
+ "mac_address": "core",
166
+ "port": "core",
167
+ # Network properties
168
+ "wifi_signal": "network",
169
+ # Location properties
170
+ "location_id": "location",
171
+ "location_label": "location",
172
+ "location_updated_at": "location",
173
+ # Group properties
174
+ "group_id": "group",
175
+ "group_label": "group",
176
+ "group_updated_at": "group",
177
+ # Waveform properties
178
+ "waveform_active": "waveform",
179
+ "waveform_type": "waveform",
180
+ "waveform_transient": "waveform",
181
+ "waveform_color": "waveform",
182
+ "waveform_period_ms": "waveform",
183
+ "waveform_cycles": "waveform",
184
+ "waveform_duty_cycle": "waveform",
185
+ "waveform_skew_ratio": "waveform",
186
+ # Infrared properties
187
+ "infrared_brightness": "infrared",
188
+ # HEV properties
189
+ "hev_cycle_duration_s": "hev",
190
+ "hev_cycle_remaining_s": "hev",
191
+ "hev_cycle_last_power": "hev",
192
+ "hev_indication": "hev",
193
+ "hev_last_result": "hev",
194
+ # Multizone properties
195
+ "zone_count": "multizone",
196
+ "zone_colors": "multizone",
197
+ "multizone_effect_type": ("multizone", "effect_type"),
198
+ "multizone_effect_speed": ("multizone", "effect_speed"),
199
+ # Matrix/Tile properties
200
+ "tile_count": "matrix",
201
+ "tile_devices": "matrix",
202
+ "tile_width": "matrix",
203
+ "tile_height": "matrix",
204
+ "tile_effect_type": ("matrix", "effect_type"),
205
+ "tile_effect_speed": ("matrix", "effect_speed"),
206
+ "tile_effect_palette_count": ("matrix", "effect_palette_count"),
207
+ "tile_effect_palette": ("matrix", "effect_palette"),
208
+ }
209
+
210
+ # Default values for optional state attributes when state object is None
211
+ _OPTIONAL_DEFAULTS = {
212
+ "infrared_brightness": 0,
213
+ "hev_cycle_duration_s": 0,
214
+ "hev_cycle_remaining_s": 0,
215
+ "hev_cycle_last_power": False,
216
+ "hev_indication": False,
217
+ "hev_last_result": 0,
218
+ "zone_count": 0,
219
+ "zone_colors": [],
220
+ "multizone_effect_type": 0,
221
+ "multizone_effect_speed": 0,
222
+ "tile_count": 0,
223
+ "tile_devices": [],
224
+ "tile_width": 8,
225
+ "tile_height": 8,
226
+ "tile_effect_type": 0,
227
+ "tile_effect_speed": 0,
228
+ "tile_effect_palette_count": 0,
229
+ "tile_effect_palette": [],
230
+ }
231
+
232
+ def get_target_bytes(self) -> bytes:
233
+ """Get target bytes for this device."""
234
+ return bytes.fromhex(self.core.serial) + b"\x00\x00"
235
+
236
+ def __getattr__(self, name: str) -> Any:
237
+ """Dynamically delegate attribute access to appropriate state object.
238
+
239
+ This eliminates ~180 lines of @property boilerplate.
240
+
241
+ Args:
242
+ name: Attribute name being accessed
243
+
244
+ Returns:
245
+ Attribute value from the appropriate state object
246
+
247
+ Raises:
248
+ AttributeError: If attribute is not found
249
+ """
250
+ # Check if this attribute has a routing rule
251
+ if name in self._ATTRIBUTE_ROUTES:
252
+ route = self._ATTRIBUTE_ROUTES[name]
253
+
254
+ # Route can be either 'state_name' or ('state_name', 'attr_name')
255
+ if isinstance(route, tuple):
256
+ state_name, attr_name = route
257
+ else:
258
+ state_name = route
259
+ attr_name = name
260
+
261
+ # Get the state object
262
+ state_obj = object.__getattribute__(self, state_name)
263
+
264
+ # Handle optional state objects (infrared, hev, multizone, matrix)
265
+ if state_obj is None:
266
+ # Return default value for optional attributes
267
+ return self._OPTIONAL_DEFAULTS.get(name)
268
+
269
+ # Delegate to the state object
270
+ return getattr(state_obj, attr_name)
271
+
272
+ # If not in routing map, raise AttributeError
273
+ raise AttributeError(
274
+ f"'{type(self).__name__}' object has no attribute '{name}'"
275
+ )
276
+
277
+ def __setattr__(self, name: str, value: Any) -> None:
278
+ """Dynamically delegate attribute writes to appropriate state object.
279
+
280
+ This eliminates ~180 lines of @property.setter boilerplate.
281
+
282
+ Args:
283
+ name: Attribute name being set
284
+ value: Value to set
285
+
286
+ Note:
287
+ Dataclass fields and private attributes bypass delegation.
288
+ """
289
+ # Dataclass fields and private attributes use normal assignment
290
+ if name in {
291
+ "core",
292
+ "network",
293
+ "location",
294
+ "group",
295
+ "waveform",
296
+ "infrared",
297
+ "hev",
298
+ "multizone",
299
+ "matrix",
300
+ "has_color",
301
+ "has_infrared",
302
+ "has_multizone",
303
+ "has_extended_multizone",
304
+ "has_matrix",
305
+ "has_hev",
306
+ } or name.startswith("_"):
307
+ object.__setattr__(self, name, value)
308
+ return
309
+
310
+ # Check if this attribute has a routing rule
311
+ if name in self._ATTRIBUTE_ROUTES:
312
+ route = self._ATTRIBUTE_ROUTES[name]
313
+
314
+ # Route can be either 'state_name' or ('state_name', 'attr_name')
315
+ if isinstance(route, tuple):
316
+ state_name, attr_name = route
317
+ else:
318
+ state_name = route
319
+ attr_name = name
320
+
321
+ # Get the state object
322
+ state_obj = object.__getattribute__(self, state_name)
323
+
324
+ # Handle optional state objects - silently ignore writes if None
325
+ if state_obj is None:
326
+ return
327
+
328
+ # Delegate to the state object
329
+ setattr(state_obj, attr_name, value)
330
+ return
331
+
332
+ # For unknown attributes, use normal assignment (allows adding new attributes)
333
+ object.__setattr__(self, name, value)
@@ -0,0 +1,37 @@
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_tile_device,
19
+ )
20
+ from lifx_emulator.factories.firmware_config import FirmwareConfig
21
+ from lifx_emulator.factories.serial_generator import SerialGenerator
22
+
23
+ __all__ = [
24
+ # Builder and helpers
25
+ "DeviceBuilder",
26
+ "SerialGenerator",
27
+ "DefaultColorConfig",
28
+ "FirmwareConfig",
29
+ # Factory functions
30
+ "create_device",
31
+ "create_color_light",
32
+ "create_infrared_light",
33
+ "create_hev_light",
34
+ "create_multizone_light",
35
+ "create_tile_device",
36
+ "create_color_temperature_light",
37
+ ]