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,259 +0,0 @@
1
- """State restoration for devices with persistent storage.
2
-
3
- This module provides centralized state restoration logic, eliminating
4
- duplication between factories and device initialization.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- import logging
10
- from typing import Any
11
-
12
- from lifx_emulator.devices.device import DeviceState
13
-
14
- logger = logging.getLogger(__name__)
15
-
16
-
17
- class StateRestorer:
18
- """Handles restoration of device state from persistent storage.
19
-
20
- Consolidates state restoration logic that was previously duplicated
21
- between factories and device initialization.
22
- """
23
-
24
- def __init__(self, storage: Any):
25
- """Initialize state restorer.
26
-
27
- Args:
28
- storage: Storage instance (DeviceStorage or DevicePersistenceAsyncFile)
29
- """
30
- self.storage = storage
31
-
32
- def restore_if_available(self, state: DeviceState) -> DeviceState:
33
- """Restore saved state if available and compatible.
34
-
35
- Args:
36
- state: DeviceState to restore into
37
-
38
- Returns:
39
- The same DeviceState instance with restored values
40
- """
41
- if not self.storage:
42
- return state
43
-
44
- saved_state = self.storage.load_device_state(state.serial)
45
- if not saved_state:
46
- logger.debug("No saved state found for device %s", state.serial)
47
- return state
48
-
49
- # Only restore if product matches
50
- if saved_state.get("product") != state.product:
51
- logger.warning(
52
- "Saved state for %s has different product (%s vs %s), skipping restore",
53
- state.serial,
54
- saved_state.get("product"),
55
- state.product,
56
- )
57
- return state
58
-
59
- logger.info("Restoring saved state for device %s", state.serial)
60
-
61
- # Restore core state
62
- self._restore_core_state(state, saved_state)
63
-
64
- # Restore location and group
65
- self._restore_location_and_group(state, saved_state)
66
-
67
- # Restore capability-specific state
68
- self._restore_capability_state(state, saved_state)
69
-
70
- return state
71
-
72
- def _restore_core_state(
73
- self, state: DeviceState, saved_state: dict[str, Any]
74
- ) -> None:
75
- """Restore core device state fields.
76
-
77
- Args:
78
- state: DeviceState to restore into
79
- saved_state: Dictionary with saved state values
80
- """
81
- if "label" in saved_state:
82
- state.core.label = saved_state["label"]
83
- if "power_level" in saved_state:
84
- state.core.power_level = saved_state["power_level"]
85
- if "color" in saved_state:
86
- state.core.color = saved_state["color"]
87
-
88
- def _restore_location_and_group(
89
- self, state: DeviceState, saved_state: dict[str, Any]
90
- ) -> None:
91
- """Restore location and group metadata.
92
-
93
- Args:
94
- state: DeviceState to restore into
95
- saved_state: Dictionary with saved state values
96
- """
97
- # Location
98
- if "location_id" in saved_state:
99
- state.location.location_id = saved_state["location_id"]
100
- if "location_label" in saved_state:
101
- state.location.location_label = saved_state["location_label"]
102
- if "location_updated_at" in saved_state:
103
- state.location.location_updated_at = saved_state["location_updated_at"]
104
-
105
- # Group
106
- if "group_id" in saved_state:
107
- state.group.group_id = saved_state["group_id"]
108
- if "group_label" in saved_state:
109
- state.group.group_label = saved_state["group_label"]
110
- if "group_updated_at" in saved_state:
111
- state.group.group_updated_at = saved_state["group_updated_at"]
112
-
113
- def _restore_capability_state(
114
- self, state: DeviceState, saved_state: dict[str, Any]
115
- ) -> None:
116
- """Restore capability-specific state.
117
-
118
- Args:
119
- state: DeviceState to restore into
120
- saved_state: Dictionary with saved state values
121
- """
122
- # Infrared
123
- if (
124
- state.has_infrared
125
- and state.infrared
126
- and "infrared_brightness" in saved_state
127
- ):
128
- state.infrared.infrared_brightness = saved_state["infrared_brightness"]
129
-
130
- # HEV
131
- if state.has_hev and state.hev:
132
- if "hev_cycle_duration_s" in saved_state:
133
- state.hev.hev_cycle_duration_s = saved_state["hev_cycle_duration_s"]
134
- if "hev_cycle_remaining_s" in saved_state:
135
- state.hev.hev_cycle_remaining_s = saved_state["hev_cycle_remaining_s"]
136
- if "hev_cycle_last_power" in saved_state:
137
- state.hev.hev_cycle_last_power = saved_state["hev_cycle_last_power"]
138
- if "hev_indication" in saved_state:
139
- state.hev.hev_indication = saved_state["hev_indication"]
140
- if "hev_last_result" in saved_state:
141
- state.hev.hev_last_result = saved_state["hev_last_result"]
142
-
143
- # Multizone
144
- if state.has_multizone and state.multizone:
145
- self._restore_multizone_state(state, saved_state)
146
-
147
- # Matrix (Tile)
148
- if state.has_matrix and state.matrix:
149
- self._restore_matrix_state(state, saved_state)
150
-
151
- def _restore_multizone_state(
152
- self, state: DeviceState, saved_state: dict[str, Any]
153
- ) -> None:
154
- """Restore multizone-specific state.
155
-
156
- Args:
157
- state: DeviceState to restore into
158
- saved_state: Dictionary with saved state values
159
- """
160
- if state.multizone is None:
161
- return
162
-
163
- # First restore zone_count from saved state
164
- # This ensures the device matches what was previously saved
165
- if "zone_count" in saved_state:
166
- state.multizone.zone_count = saved_state["zone_count"]
167
- logger.debug("Restored zone_count: %s", state.multizone.zone_count)
168
-
169
- # Now restore zone colors if available
170
- if "zone_colors" in saved_state:
171
- # Verify zone count matches (should match now that we restored it)
172
- if len(saved_state["zone_colors"]) == state.multizone.zone_count:
173
- state.multizone.zone_colors = saved_state["zone_colors"]
174
- logger.debug("Restored %s zone colors", len(saved_state["zone_colors"]))
175
- else:
176
- logger.warning(
177
- "Zone count mismatch: saved has %s zones, current has %s zones",
178
- len(saved_state["zone_colors"]),
179
- state.multizone.zone_count,
180
- )
181
-
182
- if "multizone_effect_type" in saved_state:
183
- state.multizone.effect_type = saved_state["multizone_effect_type"]
184
- if "multizone_effect_speed" in saved_state:
185
- state.multizone.effect_speed = saved_state["multizone_effect_speed"]
186
-
187
- def _restore_matrix_state(
188
- self, state: DeviceState, saved_state: dict[str, Any]
189
- ) -> None:
190
- """Restore matrix (tile) specific state.
191
-
192
- Args:
193
- state: DeviceState to restore into
194
- saved_state: Dictionary with saved state values
195
- """
196
- if state.matrix is None:
197
- return
198
-
199
- # First restore tile configuration (count, width, height) from saved state
200
- # This ensures the device matches what was previously saved
201
- if "tile_count" in saved_state:
202
- state.matrix.tile_count = saved_state["tile_count"]
203
- logger.debug("Restored tile_count: %s", state.matrix.tile_count)
204
- if "tile_width" in saved_state:
205
- state.matrix.tile_width = saved_state["tile_width"]
206
- logger.debug("Restored tile_width: %s", state.matrix.tile_width)
207
- if "tile_height" in saved_state:
208
- state.matrix.tile_height = saved_state["tile_height"]
209
- logger.debug("Restored tile_height: %s", state.matrix.tile_height)
210
-
211
- # Now restore tile devices if available
212
- if "tile_devices" in saved_state:
213
- saved_tiles = saved_state["tile_devices"]
214
- # Verify tile count matches (should match now that we restored it)
215
- if len(saved_tiles) == state.matrix.tile_count:
216
- # Verify all tiles have matching dimensions
217
- if all(
218
- t["width"] == state.matrix.tile_width
219
- and t["height"] == state.matrix.tile_height
220
- for t in saved_tiles
221
- ):
222
- state.matrix.tile_devices = saved_tiles
223
- logger.debug("Restored %s tile devices", len(saved_tiles))
224
- else:
225
- logger.warning(
226
- "Tile dimensions mismatch, skipping tile restoration"
227
- )
228
- else:
229
- logger.warning(
230
- f"Tile count mismatch: saved has {len(saved_tiles)} tiles, "
231
- f"current has {state.matrix.tile_count} tiles"
232
- )
233
-
234
- if "tile_effect_type" in saved_state:
235
- state.matrix.effect_type = saved_state["tile_effect_type"]
236
- if "tile_effect_speed" in saved_state:
237
- state.matrix.effect_speed = saved_state["tile_effect_speed"]
238
- if "tile_effect_palette_count" in saved_state:
239
- state.matrix.effect_palette_count = saved_state["tile_effect_palette_count"]
240
- if "tile_effect_palette" in saved_state:
241
- state.matrix.effect_palette = saved_state["tile_effect_palette"]
242
-
243
-
244
- class NullStateRestorer:
245
- """No-op state restorer for devices without persistence.
246
-
247
- Allows code to unconditionally call restore without checking for None.
248
- """
249
-
250
- def restore_if_available(self, state: DeviceState) -> DeviceState:
251
- """No-op restoration.
252
-
253
- Args:
254
- state: DeviceState (returned unchanged)
255
-
256
- Returns:
257
- The same DeviceState instance
258
- """
259
- return state
@@ -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