lifx-emulator 1.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.
- lifx_emulator/__init__.py +31 -0
- lifx_emulator/__main__.py +607 -0
- lifx_emulator/api.py +1825 -0
- lifx_emulator/async_storage.py +308 -0
- lifx_emulator/constants.py +33 -0
- lifx_emulator/device.py +750 -0
- lifx_emulator/device_states.py +114 -0
- lifx_emulator/factories.py +380 -0
- lifx_emulator/handlers/__init__.py +39 -0
- lifx_emulator/handlers/base.py +49 -0
- lifx_emulator/handlers/device_handlers.py +340 -0
- lifx_emulator/handlers/light_handlers.py +372 -0
- lifx_emulator/handlers/multizone_handlers.py +249 -0
- lifx_emulator/handlers/registry.py +110 -0
- lifx_emulator/handlers/tile_handlers.py +309 -0
- lifx_emulator/observers.py +139 -0
- lifx_emulator/products/__init__.py +28 -0
- lifx_emulator/products/generator.py +771 -0
- lifx_emulator/products/registry.py +1446 -0
- lifx_emulator/products/specs.py +242 -0
- lifx_emulator/products/specs.yml +327 -0
- lifx_emulator/protocol/__init__.py +1 -0
- lifx_emulator/protocol/base.py +334 -0
- lifx_emulator/protocol/const.py +8 -0
- lifx_emulator/protocol/generator.py +1371 -0
- lifx_emulator/protocol/header.py +159 -0
- lifx_emulator/protocol/packets.py +1351 -0
- lifx_emulator/protocol/protocol_types.py +844 -0
- lifx_emulator/protocol/serializer.py +379 -0
- lifx_emulator/scenario_manager.py +402 -0
- lifx_emulator/scenario_persistence.py +206 -0
- lifx_emulator/server.py +482 -0
- lifx_emulator/state_restorer.py +259 -0
- lifx_emulator/state_serializer.py +130 -0
- lifx_emulator/storage_protocol.py +100 -0
- lifx_emulator-1.0.0.dist-info/METADATA +445 -0
- lifx_emulator-1.0.0.dist-info/RECORD +40 -0
- lifx_emulator-1.0.0.dist-info/WHEEL +4 -0
- lifx_emulator-1.0.0.dist-info/entry_points.txt +2 -0
- lifx_emulator-1.0.0.dist-info/licenses/LICENSE +35 -0
|
@@ -0,0 +1,259 @@
|
|
|
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.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 AsyncDeviceStorage)
|
|
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
|
|
@@ -0,0 +1,130 @@
|
|
|
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
|
+
|
|
101
|
+
return state_dict
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def deserialize_device_state(state_dict: dict[str, Any]) -> dict[str, Any]:
|
|
105
|
+
"""Deserialize device state dict (convert hex strings and nested objects)."""
|
|
106
|
+
# Deserialize bytes fields
|
|
107
|
+
state_dict["location_id"] = bytes.fromhex(state_dict["location_id"])
|
|
108
|
+
state_dict["group_id"] = bytes.fromhex(state_dict["group_id"])
|
|
109
|
+
|
|
110
|
+
# Deserialize color
|
|
111
|
+
state_dict["color"] = deserialize_hsbk(state_dict["color"])
|
|
112
|
+
|
|
113
|
+
# Deserialize zone colors if present
|
|
114
|
+
if "zone_colors" in state_dict:
|
|
115
|
+
state_dict["zone_colors"] = [
|
|
116
|
+
deserialize_hsbk(c) for c in state_dict["zone_colors"]
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
# Deserialize tile effect palette if present
|
|
120
|
+
if "tile_effect_palette" in state_dict:
|
|
121
|
+
state_dict["tile_effect_palette"] = [
|
|
122
|
+
deserialize_hsbk(c) for c in state_dict["tile_effect_palette"]
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
# Deserialize tile devices if present
|
|
126
|
+
if "tile_devices" in state_dict:
|
|
127
|
+
for tile_dict in state_dict["tile_devices"]:
|
|
128
|
+
tile_dict["colors"] = [deserialize_hsbk(c) for c in tile_dict["colors"]]
|
|
129
|
+
|
|
130
|
+
return state_dict
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Storage protocol definition for device state persistence.
|
|
2
|
+
|
|
3
|
+
This module defines the common interface that all storage implementations
|
|
4
|
+
must follow, enabling polymorphic use and easier testing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Protocol, runtime_checkable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@runtime_checkable
|
|
13
|
+
class StorageProtocol(Protocol):
|
|
14
|
+
"""Protocol defining the interface for device state storage.
|
|
15
|
+
|
|
16
|
+
Both synchronous (DeviceStorage) and asynchronous (AsyncDeviceStorage)
|
|
17
|
+
implementations must provide these methods.
|
|
18
|
+
|
|
19
|
+
The protocol allows for polymorphic usage and dependency injection,
|
|
20
|
+
improving testability and adherence to SOLID principles.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def load_device_state(self, serial: str) -> dict[str, Any] | None:
|
|
24
|
+
"""Load device state from persistent storage.
|
|
25
|
+
|
|
26
|
+
This method is synchronous in both implementations because loading
|
|
27
|
+
primarily happens at device initialization where blocking is acceptable.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
serial: Device serial number
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Dictionary with device state, or None if not found
|
|
34
|
+
"""
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
def delete_device_state(self, serial: str) -> None:
|
|
38
|
+
"""Delete device state from persistent storage.
|
|
39
|
+
|
|
40
|
+
This method is synchronous because deletion is rare and blocking
|
|
41
|
+
is acceptable for this operation.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
serial: Device serial number
|
|
45
|
+
"""
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
def delete_all_device_states(self) -> int:
|
|
49
|
+
"""Delete all device states from persistent storage.
|
|
50
|
+
|
|
51
|
+
This method is synchronous because it's typically used for cleanup
|
|
52
|
+
operations where blocking is acceptable.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Number of devices deleted
|
|
56
|
+
"""
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
def list_devices(self) -> list[str]:
|
|
60
|
+
"""List all devices with saved state.
|
|
61
|
+
|
|
62
|
+
This method is synchronous because listing is typically used for
|
|
63
|
+
administrative/query operations where blocking is acceptable.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
List of device serial numbers
|
|
67
|
+
"""
|
|
68
|
+
...
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@runtime_checkable
|
|
72
|
+
class AsyncStorageProtocol(StorageProtocol, Protocol):
|
|
73
|
+
"""Extended protocol for asynchronous storage implementations.
|
|
74
|
+
|
|
75
|
+
Adds async save method for high-performance non-blocking writes.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
async def save_device_state(self, device_state: Any) -> None:
|
|
79
|
+
"""Queue device state for saving (non-blocking).
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
device_state: DeviceState instance to persist
|
|
83
|
+
"""
|
|
84
|
+
...
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@runtime_checkable
|
|
88
|
+
class SyncStorageProtocol(StorageProtocol, Protocol):
|
|
89
|
+
"""Extended protocol for synchronous storage implementations.
|
|
90
|
+
|
|
91
|
+
Adds synchronous save method for simple blocking writes.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def save_device_state(self, device_state: Any) -> None:
|
|
95
|
+
"""Save device state to persistent storage (blocking).
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
device_state: DeviceState instance to persist
|
|
99
|
+
"""
|
|
100
|
+
...
|