lifx-emulator 2.4.0__py3-none-any.whl → 3.0.1__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 (68) hide show
  1. lifx_emulator-3.0.1.dist-info/METADATA +102 -0
  2. lifx_emulator-3.0.1.dist-info/RECORD +18 -0
  3. lifx_emulator-3.0.1.dist-info/entry_points.txt +2 -0
  4. lifx_emulator_app/__init__.py +10 -0
  5. {lifx_emulator → lifx_emulator_app}/__main__.py +2 -3
  6. {lifx_emulator → lifx_emulator_app}/api/__init__.py +1 -1
  7. {lifx_emulator → lifx_emulator_app}/api/app.py +3 -3
  8. {lifx_emulator → lifx_emulator_app}/api/mappers/__init__.py +1 -1
  9. {lifx_emulator → lifx_emulator_app}/api/mappers/device_mapper.py +1 -1
  10. {lifx_emulator → lifx_emulator_app}/api/models.py +1 -2
  11. lifx_emulator_app/api/routers/__init__.py +11 -0
  12. {lifx_emulator → lifx_emulator_app}/api/routers/devices.py +2 -2
  13. {lifx_emulator → lifx_emulator_app}/api/routers/monitoring.py +1 -1
  14. {lifx_emulator → lifx_emulator_app}/api/routers/scenarios.py +1 -1
  15. lifx_emulator_app/api/services/__init__.py +8 -0
  16. {lifx_emulator → lifx_emulator_app}/api/services/device_service.py +3 -2
  17. lifx_emulator/__init__.py +0 -31
  18. lifx_emulator/api/routers/__init__.py +0 -11
  19. lifx_emulator/api/services/__init__.py +0 -8
  20. lifx_emulator/constants.py +0 -33
  21. lifx_emulator/devices/__init__.py +0 -37
  22. lifx_emulator/devices/device.py +0 -395
  23. lifx_emulator/devices/manager.py +0 -256
  24. lifx_emulator/devices/observers.py +0 -139
  25. lifx_emulator/devices/persistence.py +0 -308
  26. lifx_emulator/devices/state_restorer.py +0 -259
  27. lifx_emulator/devices/state_serializer.py +0 -157
  28. lifx_emulator/devices/states.py +0 -381
  29. lifx_emulator/factories/__init__.py +0 -39
  30. lifx_emulator/factories/builder.py +0 -375
  31. lifx_emulator/factories/default_config.py +0 -158
  32. lifx_emulator/factories/factory.py +0 -252
  33. lifx_emulator/factories/firmware_config.py +0 -77
  34. lifx_emulator/factories/serial_generator.py +0 -82
  35. lifx_emulator/handlers/__init__.py +0 -39
  36. lifx_emulator/handlers/base.py +0 -49
  37. lifx_emulator/handlers/device_handlers.py +0 -322
  38. lifx_emulator/handlers/light_handlers.py +0 -503
  39. lifx_emulator/handlers/multizone_handlers.py +0 -249
  40. lifx_emulator/handlers/registry.py +0 -110
  41. lifx_emulator/handlers/tile_handlers.py +0 -488
  42. lifx_emulator/products/__init__.py +0 -28
  43. lifx_emulator/products/generator.py +0 -1079
  44. lifx_emulator/products/registry.py +0 -1530
  45. lifx_emulator/products/specs.py +0 -284
  46. lifx_emulator/products/specs.yml +0 -386
  47. lifx_emulator/protocol/__init__.py +0 -1
  48. lifx_emulator/protocol/base.py +0 -446
  49. lifx_emulator/protocol/const.py +0 -8
  50. lifx_emulator/protocol/generator.py +0 -1384
  51. lifx_emulator/protocol/header.py +0 -159
  52. lifx_emulator/protocol/packets.py +0 -1351
  53. lifx_emulator/protocol/protocol_types.py +0 -817
  54. lifx_emulator/protocol/serializer.py +0 -379
  55. lifx_emulator/repositories/__init__.py +0 -22
  56. lifx_emulator/repositories/device_repository.py +0 -155
  57. lifx_emulator/repositories/storage_backend.py +0 -107
  58. lifx_emulator/scenarios/__init__.py +0 -22
  59. lifx_emulator/scenarios/manager.py +0 -322
  60. lifx_emulator/scenarios/models.py +0 -112
  61. lifx_emulator/scenarios/persistence.py +0 -241
  62. lifx_emulator/server.py +0 -464
  63. lifx_emulator-2.4.0.dist-info/METADATA +0 -107
  64. lifx_emulator-2.4.0.dist-info/RECORD +0 -62
  65. lifx_emulator-2.4.0.dist-info/entry_points.txt +0 -2
  66. lifx_emulator-2.4.0.dist-info/licenses/LICENSE +0 -35
  67. {lifx_emulator-2.4.0.dist-info → lifx_emulator-3.0.1.dist-info}/WHEEL +0 -0
  68. {lifx_emulator → lifx_emulator_app}/api/templates/dashboard.html +0 -0
@@ -1,157 +0,0 @@
1
- """Shared serialization logic for device state."""
2
-
3
- from __future__ import annotations
4
-
5
- from typing import Any
6
-
7
- from lifx_emulator.protocol.protocol_types import LightHsbk
8
-
9
-
10
- def serialize_hsbk(hsbk: LightHsbk) -> dict[str, int]:
11
- """Serialize LightHsbk to dict."""
12
- return {
13
- "hue": hsbk.hue,
14
- "saturation": hsbk.saturation,
15
- "brightness": hsbk.brightness,
16
- "kelvin": hsbk.kelvin,
17
- }
18
-
19
-
20
- def deserialize_hsbk(data: dict[str, int]) -> LightHsbk:
21
- """Deserialize dict to LightHsbk."""
22
- return LightHsbk(
23
- hue=data["hue"],
24
- saturation=data["saturation"],
25
- brightness=data["brightness"],
26
- kelvin=data["kelvin"],
27
- )
28
-
29
-
30
- def serialize_device_state(device_state: Any) -> dict[str, Any]:
31
- """Serialize DeviceState to dict.
32
-
33
- Note: Accesses state via properties for backward compatibility with composed state.
34
- """
35
- state_dict = {
36
- "serial": device_state.serial,
37
- "label": device_state.label,
38
- "product": device_state.product,
39
- "power_level": device_state.power_level,
40
- "color": serialize_hsbk(device_state.color),
41
- "location_id": device_state.location_id.hex(),
42
- "location_label": device_state.location_label,
43
- "location_updated_at": device_state.location_updated_at,
44
- "group_id": device_state.group_id.hex(),
45
- "group_label": device_state.group_label,
46
- "group_updated_at": device_state.group_updated_at,
47
- "has_color": device_state.has_color,
48
- "has_infrared": device_state.has_infrared,
49
- "has_multizone": device_state.has_multizone,
50
- "has_matrix": device_state.has_matrix,
51
- "has_hev": device_state.has_hev,
52
- }
53
-
54
- if device_state.has_infrared:
55
- state_dict["infrared_brightness"] = device_state.infrared_brightness
56
-
57
- if device_state.has_hev:
58
- state_dict["hev_cycle_duration_s"] = device_state.hev_cycle_duration_s
59
- state_dict["hev_cycle_remaining_s"] = device_state.hev_cycle_remaining_s
60
- state_dict["hev_cycle_last_power"] = device_state.hev_cycle_last_power
61
- state_dict["hev_indication"] = device_state.hev_indication
62
- state_dict["hev_last_result"] = device_state.hev_last_result
63
-
64
- if device_state.has_multizone:
65
- state_dict["zone_count"] = device_state.zone_count
66
- state_dict["zone_colors"] = [
67
- serialize_hsbk(c) for c in device_state.zone_colors
68
- ]
69
- state_dict["multizone_effect_type"] = device_state.multizone_effect_type
70
- state_dict["multizone_effect_speed"] = device_state.multizone_effect_speed
71
-
72
- if device_state.has_matrix:
73
- state_dict["tile_count"] = device_state.tile_count
74
- state_dict["tile_width"] = device_state.tile_width
75
- state_dict["tile_height"] = device_state.tile_height
76
- state_dict["tile_effect_type"] = device_state.tile_effect_type
77
- state_dict["tile_effect_speed"] = device_state.tile_effect_speed
78
- state_dict["tile_effect_palette_count"] = device_state.tile_effect_palette_count
79
- state_dict["tile_effect_palette"] = [
80
- serialize_hsbk(c) for c in device_state.tile_effect_palette
81
- ]
82
- state_dict["tile_devices"] = [
83
- {
84
- "accel_meas_x": t["accel_meas_x"],
85
- "accel_meas_y": t["accel_meas_y"],
86
- "accel_meas_z": t["accel_meas_z"],
87
- "user_x": t["user_x"],
88
- "user_y": t["user_y"],
89
- "width": t["width"],
90
- "height": t["height"],
91
- "device_version_vendor": t["device_version_vendor"],
92
- "device_version_product": t["device_version_product"],
93
- "firmware_build": t["firmware_build"],
94
- "firmware_version_minor": t["firmware_version_minor"],
95
- "firmware_version_major": t["firmware_version_major"],
96
- "colors": [serialize_hsbk(c) for c in t["colors"]],
97
- }
98
- for t in device_state.tile_devices
99
- ]
100
- # Serialize tile framebuffers (non-visible framebuffers 1-7)
101
- state_dict["tile_framebuffers"] = [
102
- {
103
- "tile_index": fb.tile_index,
104
- "framebuffers": {
105
- str(fb_idx): [serialize_hsbk(c) for c in colors]
106
- for fb_idx, colors in fb.framebuffers.items()
107
- },
108
- }
109
- for fb in device_state.tile_framebuffers
110
- ]
111
-
112
- return state_dict
113
-
114
-
115
- def deserialize_device_state(state_dict: dict[str, Any]) -> dict[str, Any]:
116
- """Deserialize device state dict (convert hex strings and nested objects)."""
117
- # Deserialize bytes fields
118
- state_dict["location_id"] = bytes.fromhex(state_dict["location_id"])
119
- state_dict["group_id"] = bytes.fromhex(state_dict["group_id"])
120
-
121
- # Deserialize color
122
- state_dict["color"] = deserialize_hsbk(state_dict["color"])
123
-
124
- # Deserialize zone colors if present
125
- if "zone_colors" in state_dict:
126
- state_dict["zone_colors"] = [
127
- deserialize_hsbk(c) for c in state_dict["zone_colors"]
128
- ]
129
-
130
- # Deserialize tile effect palette if present
131
- if "tile_effect_palette" in state_dict:
132
- state_dict["tile_effect_palette"] = [
133
- deserialize_hsbk(c) for c in state_dict["tile_effect_palette"]
134
- ]
135
-
136
- # Deserialize tile devices if present
137
- if "tile_devices" in state_dict:
138
- for tile_dict in state_dict["tile_devices"]:
139
- tile_dict["colors"] = [deserialize_hsbk(c) for c in tile_dict["colors"]]
140
-
141
- # Deserialize tile framebuffers if present (for backwards compatibility)
142
- if "tile_framebuffers" in state_dict:
143
- from lifx_emulator.devices.states import TileFramebuffers
144
-
145
- deserialized_fbs = []
146
- for fb_dict in state_dict["tile_framebuffers"]:
147
- tile_fb = TileFramebuffers(tile_index=fb_dict["tile_index"])
148
- # Deserialize each framebuffer's colors
149
- for fb_idx_str, colors_list in fb_dict["framebuffers"].items():
150
- fb_idx = int(fb_idx_str)
151
- tile_fb.framebuffers[fb_idx] = [
152
- deserialize_hsbk(c) for c in colors_list
153
- ]
154
- deserialized_fbs.append(tile_fb)
155
- state_dict["tile_framebuffers"] = deserialized_fbs
156
-
157
- return state_dict
@@ -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
- ]