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.
- lifx_emulator/__init__.py +1 -1
- lifx_emulator/__main__.py +26 -51
- lifx_emulator/api/__init__.py +18 -0
- lifx_emulator/api/app.py +154 -0
- lifx_emulator/api/mappers/__init__.py +5 -0
- lifx_emulator/api/mappers/device_mapper.py +114 -0
- lifx_emulator/api/models.py +133 -0
- lifx_emulator/api/routers/__init__.py +11 -0
- lifx_emulator/api/routers/devices.py +130 -0
- lifx_emulator/api/routers/monitoring.py +52 -0
- lifx_emulator/api/routers/scenarios.py +247 -0
- lifx_emulator/api/services/__init__.py +8 -0
- lifx_emulator/api/services/device_service.py +198 -0
- lifx_emulator/{api.py → api/templates/dashboard.html} +0 -942
- lifx_emulator/devices/__init__.py +37 -0
- lifx_emulator/devices/device.py +333 -0
- lifx_emulator/devices/manager.py +256 -0
- lifx_emulator/{async_storage.py → devices/persistence.py} +3 -3
- lifx_emulator/{state_restorer.py → devices/state_restorer.py} +2 -2
- lifx_emulator/devices/states.py +333 -0
- lifx_emulator/factories/__init__.py +37 -0
- lifx_emulator/factories/builder.py +371 -0
- lifx_emulator/factories/default_config.py +158 -0
- lifx_emulator/factories/factory.py +221 -0
- lifx_emulator/factories/firmware_config.py +59 -0
- lifx_emulator/factories/serial_generator.py +82 -0
- lifx_emulator/handlers/base.py +1 -1
- lifx_emulator/handlers/device_handlers.py +10 -28
- lifx_emulator/handlers/light_handlers.py +5 -9
- lifx_emulator/handlers/multizone_handlers.py +1 -1
- lifx_emulator/handlers/tile_handlers.py +1 -1
- lifx_emulator/products/generator.py +389 -170
- lifx_emulator/products/registry.py +52 -40
- lifx_emulator/products/specs.py +12 -13
- lifx_emulator/protocol/base.py +115 -61
- lifx_emulator/protocol/generator.py +18 -5
- lifx_emulator/protocol/packets.py +7 -7
- lifx_emulator/repositories/__init__.py +22 -0
- lifx_emulator/repositories/device_repository.py +155 -0
- lifx_emulator/repositories/storage_backend.py +107 -0
- lifx_emulator/scenarios/__init__.py +22 -0
- lifx_emulator/{scenario_manager.py → scenarios/manager.py} +11 -91
- lifx_emulator/scenarios/models.py +112 -0
- lifx_emulator/{scenario_persistence.py → scenarios/persistence.py} +82 -47
- lifx_emulator/server.py +38 -64
- {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.0.0.dist-info}/METADATA +1 -1
- lifx_emulator-2.0.0.dist-info/RECORD +62 -0
- lifx_emulator/device.py +0 -750
- lifx_emulator/device_states.py +0 -114
- lifx_emulator/factories.py +0 -380
- lifx_emulator/storage_protocol.py +0 -100
- lifx_emulator-1.0.2.dist-info/RECORD +0 -40
- /lifx_emulator/{observers.py → devices/observers.py} +0 -0
- /lifx_emulator/{state_serializer.py → devices/state_serializer.py} +0 -0
- {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.0.0.dist-info}/WHEEL +0 -0
- {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.0.0.dist-info}/entry_points.txt +0 -0
- {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -11,6 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
from dataclasses import dataclass
|
|
13
13
|
from enum import IntEnum
|
|
14
|
+
from functools import cached_property
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class ProductCapability(IntEnum):
|
|
@@ -128,6 +129,53 @@ class ProductInfo:
|
|
|
128
129
|
return True
|
|
129
130
|
return firmware_version >= self.min_ext_mz_firmware
|
|
130
131
|
|
|
132
|
+
@cached_property
|
|
133
|
+
def caps(self) -> str:
|
|
134
|
+
"""Format product capabilities as a human-readable string.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Comma-separated capability string (e.g., "full color, infrared, multizone")
|
|
138
|
+
"""
|
|
139
|
+
caps = []
|
|
140
|
+
|
|
141
|
+
# Determine base light type
|
|
142
|
+
if self.has_relays:
|
|
143
|
+
# Devices with relays are switches, not lights
|
|
144
|
+
caps.append("switch")
|
|
145
|
+
elif self.has_color:
|
|
146
|
+
caps.append("full color")
|
|
147
|
+
else:
|
|
148
|
+
# Check temperature range to determine white light type
|
|
149
|
+
if self.temperature_range:
|
|
150
|
+
if self.temperature_range.min != self.temperature_range.max:
|
|
151
|
+
caps.append("color temperature")
|
|
152
|
+
else:
|
|
153
|
+
caps.append("brightness only")
|
|
154
|
+
else:
|
|
155
|
+
# No temperature range info, assume basic brightness
|
|
156
|
+
caps.append("brightness only")
|
|
157
|
+
|
|
158
|
+
# Add additional capabilities
|
|
159
|
+
if self.has_infrared:
|
|
160
|
+
caps.append("infrared")
|
|
161
|
+
# Extended multizone is backwards compatible with multizone,
|
|
162
|
+
# so only show multizone if extended multizone is not present
|
|
163
|
+
if self.has_extended_multizone:
|
|
164
|
+
caps.append("extended-multizone")
|
|
165
|
+
elif self.has_multizone:
|
|
166
|
+
caps.append("multizone")
|
|
167
|
+
if self.has_matrix:
|
|
168
|
+
caps.append("matrix")
|
|
169
|
+
if self.has_hev:
|
|
170
|
+
caps.append("HEV")
|
|
171
|
+
if self.has_chain:
|
|
172
|
+
caps.append("chain")
|
|
173
|
+
if self.has_buttons and not self.has_relays:
|
|
174
|
+
# Only show buttons if not already identified as switch
|
|
175
|
+
caps.append("buttons")
|
|
176
|
+
|
|
177
|
+
return ", ".join(caps) if caps else "unknown"
|
|
178
|
+
|
|
131
179
|
|
|
132
180
|
# Pre-generated product definitions
|
|
133
181
|
PRODUCTS: dict[int, ProductInfo] = {
|
|
@@ -457,22 +505,6 @@ PRODUCTS: dict[int, ProductInfo] = {
|
|
|
457
505
|
temperature_range=TemperatureRange(min=1500, max=9000),
|
|
458
506
|
min_ext_mz_firmware=None,
|
|
459
507
|
),
|
|
460
|
-
70: ProductInfo(
|
|
461
|
-
pid=70,
|
|
462
|
-
name="LIFX Switch",
|
|
463
|
-
vendor=1,
|
|
464
|
-
capabilities=ProductCapability.RELAYS | ProductCapability.BUTTONS,
|
|
465
|
-
temperature_range=None,
|
|
466
|
-
min_ext_mz_firmware=None,
|
|
467
|
-
),
|
|
468
|
-
71: ProductInfo(
|
|
469
|
-
pid=71,
|
|
470
|
-
name="LIFX Switch",
|
|
471
|
-
vendor=1,
|
|
472
|
-
capabilities=ProductCapability.RELAYS | ProductCapability.BUTTONS,
|
|
473
|
-
temperature_range=None,
|
|
474
|
-
min_ext_mz_firmware=None,
|
|
475
|
-
),
|
|
476
508
|
81: ProductInfo(
|
|
477
509
|
pid=81,
|
|
478
510
|
name="LIFX Candle White to Warm",
|
|
@@ -513,14 +545,6 @@ PRODUCTS: dict[int, ProductInfo] = {
|
|
|
513
545
|
temperature_range=TemperatureRange(min=2700, max=2700),
|
|
514
546
|
min_ext_mz_firmware=None,
|
|
515
547
|
),
|
|
516
|
-
89: ProductInfo(
|
|
517
|
-
pid=89,
|
|
518
|
-
name="LIFX Switch",
|
|
519
|
-
vendor=1,
|
|
520
|
-
capabilities=ProductCapability.RELAYS | ProductCapability.BUTTONS,
|
|
521
|
-
temperature_range=None,
|
|
522
|
-
min_ext_mz_firmware=None,
|
|
523
|
-
),
|
|
524
548
|
90: ProductInfo(
|
|
525
549
|
pid=90,
|
|
526
550
|
name="LIFX Clean",
|
|
@@ -657,22 +681,6 @@ PRODUCTS: dict[int, ProductInfo] = {
|
|
|
657
681
|
temperature_range=TemperatureRange(min=1500, max=9000),
|
|
658
682
|
min_ext_mz_firmware=None,
|
|
659
683
|
),
|
|
660
|
-
115: ProductInfo(
|
|
661
|
-
pid=115,
|
|
662
|
-
name="LIFX Switch",
|
|
663
|
-
vendor=1,
|
|
664
|
-
capabilities=ProductCapability.RELAYS | ProductCapability.BUTTONS,
|
|
665
|
-
temperature_range=None,
|
|
666
|
-
min_ext_mz_firmware=None,
|
|
667
|
-
),
|
|
668
|
-
116: ProductInfo(
|
|
669
|
-
pid=116,
|
|
670
|
-
name="LIFX Switch",
|
|
671
|
-
vendor=1,
|
|
672
|
-
capabilities=ProductCapability.RELAYS | ProductCapability.BUTTONS,
|
|
673
|
-
temperature_range=None,
|
|
674
|
-
min_ext_mz_firmware=None,
|
|
675
|
-
),
|
|
676
684
|
117: ProductInfo(
|
|
677
685
|
pid=117,
|
|
678
686
|
name="LIFX Z US",
|
|
@@ -1313,6 +1321,10 @@ class ProductRegistry:
|
|
|
1313
1321
|
prod_features = product.get("features", {})
|
|
1314
1322
|
features: dict[str, Any] = {**default_features, **prod_features}
|
|
1315
1323
|
|
|
1324
|
+
# Skip switch products (devices with relays) - these are not lights
|
|
1325
|
+
if features.get("relays"):
|
|
1326
|
+
continue
|
|
1327
|
+
|
|
1316
1328
|
# Build capabilities bitfield
|
|
1317
1329
|
capabilities = 0
|
|
1318
1330
|
if features.get("color"):
|
lifx_emulator/products/specs.py
CHANGED
|
@@ -83,19 +83,18 @@ class SpecsRegistry:
|
|
|
83
83
|
|
|
84
84
|
# Parse product specs
|
|
85
85
|
for pid, specs_data in data["products"].items():
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
)
|
|
86
|
+
self._specs[int(pid)] = ProductSpecs(
|
|
87
|
+
product_id=int(pid),
|
|
88
|
+
default_zone_count=specs_data.get("default_zone_count"),
|
|
89
|
+
min_zone_count=specs_data.get("min_zone_count"),
|
|
90
|
+
max_zone_count=specs_data.get("max_zone_count"),
|
|
91
|
+
default_tile_count=specs_data.get("default_tile_count"),
|
|
92
|
+
min_tile_count=specs_data.get("min_tile_count"),
|
|
93
|
+
max_tile_count=specs_data.get("max_tile_count"),
|
|
94
|
+
tile_width=specs_data.get("tile_width"),
|
|
95
|
+
tile_height=specs_data.get("tile_height"),
|
|
96
|
+
notes=specs_data.get("notes"),
|
|
97
|
+
)
|
|
99
98
|
|
|
100
99
|
self._loaded = True
|
|
101
100
|
|
lifx_emulator/protocol/base.py
CHANGED
|
@@ -137,7 +137,7 @@ class Packet:
|
|
|
137
137
|
|
|
138
138
|
# Unpack field value
|
|
139
139
|
value, current_offset = cls._unpack_field_value(
|
|
140
|
-
data, field_type, size_bytes, current_offset
|
|
140
|
+
data, field_type, size_bytes, current_offset, field_name
|
|
141
141
|
)
|
|
142
142
|
field_values[field_name] = value
|
|
143
143
|
|
|
@@ -195,7 +195,10 @@ class Packet:
|
|
|
195
195
|
result += item.pack()
|
|
196
196
|
return result
|
|
197
197
|
elif base_type in ("uint8", "byte"):
|
|
198
|
-
#
|
|
198
|
+
# Check if value is a string (Label fields)
|
|
199
|
+
if isinstance(value, str):
|
|
200
|
+
return serializer.pack_string(value, size_bytes)
|
|
201
|
+
# Regular byte array
|
|
199
202
|
return serializer.pack_bytes(value, size_bytes)
|
|
200
203
|
else:
|
|
201
204
|
# Array of primitives
|
|
@@ -211,11 +214,105 @@ class Packet:
|
|
|
211
214
|
return serializer.pack_value(value, base_type)
|
|
212
215
|
|
|
213
216
|
@classmethod
|
|
214
|
-
def
|
|
215
|
-
cls,
|
|
217
|
+
def _unpack_array_field(
|
|
218
|
+
cls,
|
|
219
|
+
data: bytes,
|
|
220
|
+
base_type: str,
|
|
221
|
+
array_count: int,
|
|
222
|
+
size_bytes: int,
|
|
223
|
+
offset: int,
|
|
224
|
+
field_name: str,
|
|
225
|
+
is_nested: bool,
|
|
226
|
+
enum_types: dict,
|
|
227
|
+
) -> tuple[Any, int]:
|
|
228
|
+
"""Unpack an array field value."""
|
|
229
|
+
from lifx_emulator.protocol import serializer
|
|
230
|
+
|
|
231
|
+
is_enum = is_nested and base_type in enum_types
|
|
232
|
+
|
|
233
|
+
if is_enum:
|
|
234
|
+
result = []
|
|
235
|
+
current_offset = offset
|
|
236
|
+
enum_class = enum_types[base_type]
|
|
237
|
+
for _ in range(array_count):
|
|
238
|
+
item_raw, current_offset = serializer.unpack_value(
|
|
239
|
+
data, "uint8", current_offset
|
|
240
|
+
)
|
|
241
|
+
result.append(enum_class(item_raw))
|
|
242
|
+
return result, current_offset
|
|
243
|
+
elif is_nested:
|
|
244
|
+
from lifx_emulator.protocol import protocol_types
|
|
245
|
+
|
|
246
|
+
struct_class = getattr(protocol_types, base_type)
|
|
247
|
+
result = []
|
|
248
|
+
current_offset = offset
|
|
249
|
+
for _ in range(array_count):
|
|
250
|
+
if issubclass(struct_class, cls):
|
|
251
|
+
item, current_offset = struct_class._unpack_internal(
|
|
252
|
+
data, current_offset
|
|
253
|
+
)
|
|
254
|
+
else:
|
|
255
|
+
item_result = struct_class.unpack(data, current_offset)
|
|
256
|
+
item, current_offset = item_result # type: ignore[misc]
|
|
257
|
+
result.append(item)
|
|
258
|
+
return result, current_offset
|
|
259
|
+
elif base_type in ("uint8", "byte"):
|
|
260
|
+
if field_name.lower().endswith("label"):
|
|
261
|
+
return serializer.unpack_string(data, size_bytes, offset)
|
|
262
|
+
return serializer.unpack_bytes(data, size_bytes, offset)
|
|
263
|
+
else:
|
|
264
|
+
return serializer.unpack_array(data, base_type, array_count, offset)
|
|
265
|
+
|
|
266
|
+
@classmethod
|
|
267
|
+
def _unpack_single_field(
|
|
268
|
+
cls,
|
|
269
|
+
data: bytes,
|
|
270
|
+
base_type: str,
|
|
271
|
+
offset: int,
|
|
272
|
+
is_nested: bool,
|
|
273
|
+
enum_types: dict,
|
|
216
274
|
) -> tuple[Any, int]:
|
|
217
|
-
"""Unpack a
|
|
275
|
+
"""Unpack a non-array field value."""
|
|
218
276
|
from lifx_emulator.protocol import serializer
|
|
277
|
+
|
|
278
|
+
is_enum = is_nested and base_type in enum_types
|
|
279
|
+
|
|
280
|
+
if is_enum:
|
|
281
|
+
enum_class = enum_types[base_type]
|
|
282
|
+
value_raw, new_offset = serializer.unpack_value(data, "uint8", offset)
|
|
283
|
+
return enum_class(value_raw), new_offset
|
|
284
|
+
elif is_nested:
|
|
285
|
+
from lifx_emulator.protocol import protocol_types
|
|
286
|
+
|
|
287
|
+
struct_class = getattr(protocol_types, base_type)
|
|
288
|
+
if issubclass(struct_class, cls):
|
|
289
|
+
return struct_class._unpack_internal(data, offset)
|
|
290
|
+
else:
|
|
291
|
+
return struct_class.unpack(data, offset)
|
|
292
|
+
else:
|
|
293
|
+
return serializer.unpack_value(data, base_type, offset)
|
|
294
|
+
|
|
295
|
+
@classmethod
|
|
296
|
+
def _unpack_field_value(
|
|
297
|
+
cls,
|
|
298
|
+
data: bytes,
|
|
299
|
+
field_type: str,
|
|
300
|
+
size_bytes: int,
|
|
301
|
+
offset: int,
|
|
302
|
+
field_name: str = "",
|
|
303
|
+
) -> tuple[Any, int]:
|
|
304
|
+
"""Unpack a single field value based on its type.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
data: Bytes to unpack from
|
|
308
|
+
field_type: Protocol field type string
|
|
309
|
+
size_bytes: Size in bytes
|
|
310
|
+
offset: Offset in bytes
|
|
311
|
+
field_name: Optional field name for semantic type detection
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Tuple of (value, new_offset)
|
|
315
|
+
"""
|
|
219
316
|
from lifx_emulator.protocol.protocol_types import (
|
|
220
317
|
DeviceService,
|
|
221
318
|
LightLastHevCycleResult,
|
|
@@ -225,10 +322,8 @@ class Packet:
|
|
|
225
322
|
MultiZoneExtendedApplicationRequest,
|
|
226
323
|
)
|
|
227
324
|
|
|
228
|
-
# Parse field type
|
|
229
325
|
base_type, array_count, is_nested = cls._parse_field_type(field_type)
|
|
230
326
|
|
|
231
|
-
# Check if it's an enum (Button/Relay enums excluded)
|
|
232
327
|
enum_types = {
|
|
233
328
|
"DeviceService": DeviceService,
|
|
234
329
|
"LightLastHevCycleResult": LightLastHevCycleResult,
|
|
@@ -237,63 +332,22 @@ class Packet:
|
|
|
237
332
|
"MultiZoneEffectType": MultiZoneEffectType,
|
|
238
333
|
"MultiZoneExtendedApplicationRequest": MultiZoneExtendedApplicationRequest,
|
|
239
334
|
}
|
|
240
|
-
is_enum = is_nested and base_type in enum_types
|
|
241
335
|
|
|
242
|
-
# Handle different field types
|
|
243
336
|
if array_count:
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
return result, current_offset
|
|
255
|
-
elif is_nested:
|
|
256
|
-
# Array of nested structures - need to import dynamically
|
|
257
|
-
from lifx_emulator.protocol import protocol_types
|
|
258
|
-
|
|
259
|
-
struct_class = getattr(protocol_types, base_type)
|
|
260
|
-
result = []
|
|
261
|
-
current_offset = offset
|
|
262
|
-
for _ in range(array_count):
|
|
263
|
-
# Check if it's a Packet subclass or protocol_types class
|
|
264
|
-
if issubclass(struct_class, cls):
|
|
265
|
-
item, current_offset = struct_class._unpack_internal(
|
|
266
|
-
data, current_offset
|
|
267
|
-
)
|
|
268
|
-
else:
|
|
269
|
-
item_result = struct_class.unpack(data, current_offset)
|
|
270
|
-
item, current_offset = item_result # type: ignore[misc]
|
|
271
|
-
result.append(item)
|
|
272
|
-
return result, current_offset
|
|
273
|
-
elif base_type in ("uint8", "byte"):
|
|
274
|
-
# Byte array
|
|
275
|
-
return serializer.unpack_bytes(data, size_bytes, offset)
|
|
276
|
-
else:
|
|
277
|
-
# Array of primitives
|
|
278
|
-
return serializer.unpack_array(data, base_type, array_count, offset)
|
|
279
|
-
elif is_enum:
|
|
280
|
-
# Single enum
|
|
281
|
-
enum_class = enum_types[base_type]
|
|
282
|
-
value_raw, new_offset = serializer.unpack_value(data, "uint8", offset)
|
|
283
|
-
return enum_class(value_raw), new_offset
|
|
284
|
-
elif is_nested:
|
|
285
|
-
# Nested structure - import dynamically
|
|
286
|
-
from lifx_emulator.protocol import protocol_types
|
|
287
|
-
|
|
288
|
-
struct_class = getattr(protocol_types, base_type)
|
|
289
|
-
# Check if it's a Packet subclass or protocol_types class
|
|
290
|
-
if issubclass(struct_class, cls):
|
|
291
|
-
return struct_class._unpack_internal(data, offset)
|
|
292
|
-
else:
|
|
293
|
-
return struct_class.unpack(data, offset)
|
|
337
|
+
return cls._unpack_array_field(
|
|
338
|
+
data,
|
|
339
|
+
base_type,
|
|
340
|
+
array_count,
|
|
341
|
+
size_bytes,
|
|
342
|
+
offset,
|
|
343
|
+
field_name,
|
|
344
|
+
is_nested,
|
|
345
|
+
enum_types,
|
|
346
|
+
)
|
|
294
347
|
else:
|
|
295
|
-
|
|
296
|
-
|
|
348
|
+
return cls._unpack_single_field(
|
|
349
|
+
data, base_type, offset, is_nested, enum_types
|
|
350
|
+
)
|
|
297
351
|
|
|
298
352
|
@staticmethod
|
|
299
353
|
def _parse_field_type(field_type: str) -> tuple[str, int | None, bool]:
|
|
@@ -300,7 +300,9 @@ def generate_enum_code(enums: dict[str, Any]) -> str:
|
|
|
300
300
|
|
|
301
301
|
|
|
302
302
|
def convert_type_to_python(
|
|
303
|
-
field_type: str,
|
|
303
|
+
field_type: str,
|
|
304
|
+
type_aliases: dict[str, str] | None = None,
|
|
305
|
+
field_name: str | None = None,
|
|
304
306
|
) -> str:
|
|
305
307
|
"""Convert a protocol field type to Python type annotation.
|
|
306
308
|
|
|
@@ -308,6 +310,8 @@ def convert_type_to_python(
|
|
|
308
310
|
field_type: Protocol field type string
|
|
309
311
|
type_aliases: Optional dict mapping type names to their aliases
|
|
310
312
|
(for collision resolution)
|
|
313
|
+
field_name: Optional field name for semantic type detection
|
|
314
|
+
(e.g., "Label" fields are strings, not bytes)
|
|
311
315
|
|
|
312
316
|
Returns:
|
|
313
317
|
Python type annotation string
|
|
@@ -323,7 +327,10 @@ def convert_type_to_python(
|
|
|
323
327
|
type_name = type_aliases.get(base_type, base_type)
|
|
324
328
|
return f"list[{type_name}]"
|
|
325
329
|
elif base_type in ("uint8", "byte"):
|
|
326
|
-
#
|
|
330
|
+
# Check if this is a string field (Label fields are UTF-8 strings)
|
|
331
|
+
if field_name and field_name.lower() == "label":
|
|
332
|
+
return "str"
|
|
333
|
+
# Regular byte arrays
|
|
327
334
|
return "bytes"
|
|
328
335
|
else:
|
|
329
336
|
return "list[int]"
|
|
@@ -664,7 +671,9 @@ def generate_field_code(
|
|
|
664
671
|
protocol_name = field_item["name"]
|
|
665
672
|
attr_type = field_item["type"]
|
|
666
673
|
python_name = to_snake_case(protocol_name)
|
|
667
|
-
python_type = convert_type_to_python(
|
|
674
|
+
python_type = convert_type_to_python(
|
|
675
|
+
attr_type, field_name=protocol_name
|
|
676
|
+
)
|
|
668
677
|
|
|
669
678
|
code.append(f" {python_name}: {python_type}")
|
|
670
679
|
field_map[python_name] = protocol_name
|
|
@@ -673,7 +682,9 @@ def generate_field_code(
|
|
|
673
682
|
# Convert to new format for pack/unpack generation
|
|
674
683
|
for protocol_name, attr_type in field_def.items():
|
|
675
684
|
python_name = to_snake_case(protocol_name)
|
|
676
|
-
python_type = convert_type_to_python(
|
|
685
|
+
python_type = convert_type_to_python(
|
|
686
|
+
attr_type, field_name=protocol_name
|
|
687
|
+
)
|
|
677
688
|
code.append(f" {python_name}: {python_type}")
|
|
678
689
|
field_map[python_name] = protocol_name
|
|
679
690
|
# Build fields_data for old format
|
|
@@ -832,7 +843,9 @@ def generate_nested_packet_code(
|
|
|
832
843
|
protocol_name = field_item["name"]
|
|
833
844
|
field_type = field_item["type"]
|
|
834
845
|
python_name = to_snake_case(protocol_name)
|
|
835
|
-
python_type = convert_type_to_python(
|
|
846
|
+
python_type = convert_type_to_python(
|
|
847
|
+
field_type, type_aliases, field_name=protocol_name
|
|
848
|
+
)
|
|
836
849
|
code.append(f" {python_name}: {python_type}")
|
|
837
850
|
has_fields = True
|
|
838
851
|
|
|
@@ -234,7 +234,7 @@ class Device(Packet):
|
|
|
234
234
|
_requires_response: ClassVar[bool] = False
|
|
235
235
|
|
|
236
236
|
group: bytes
|
|
237
|
-
label:
|
|
237
|
+
label: str
|
|
238
238
|
updated_at: int
|
|
239
239
|
|
|
240
240
|
@dataclass
|
|
@@ -251,7 +251,7 @@ class Device(Packet):
|
|
|
251
251
|
_requires_ack: ClassVar[bool] = True
|
|
252
252
|
_requires_response: ClassVar[bool] = False
|
|
253
253
|
|
|
254
|
-
label:
|
|
254
|
+
label: str
|
|
255
255
|
|
|
256
256
|
@dataclass
|
|
257
257
|
class SetLocation(Packet):
|
|
@@ -270,7 +270,7 @@ class Device(Packet):
|
|
|
270
270
|
_requires_response: ClassVar[bool] = False
|
|
271
271
|
|
|
272
272
|
location: bytes
|
|
273
|
-
label:
|
|
273
|
+
label: str
|
|
274
274
|
updated_at: int
|
|
275
275
|
|
|
276
276
|
@dataclass
|
|
@@ -320,7 +320,7 @@ class Device(Packet):
|
|
|
320
320
|
_requires_response: ClassVar[bool] = False
|
|
321
321
|
|
|
322
322
|
group: bytes
|
|
323
|
-
label:
|
|
323
|
+
label: str
|
|
324
324
|
updated_at: int
|
|
325
325
|
|
|
326
326
|
@dataclass
|
|
@@ -378,7 +378,7 @@ class Device(Packet):
|
|
|
378
378
|
_requires_ack: ClassVar[bool] = False
|
|
379
379
|
_requires_response: ClassVar[bool] = False
|
|
380
380
|
|
|
381
|
-
label:
|
|
381
|
+
label: str
|
|
382
382
|
|
|
383
383
|
@dataclass
|
|
384
384
|
class StateLocation(Packet):
|
|
@@ -397,7 +397,7 @@ class Device(Packet):
|
|
|
397
397
|
_requires_response: ClassVar[bool] = False
|
|
398
398
|
|
|
399
399
|
location: bytes
|
|
400
|
-
label:
|
|
400
|
+
label: str
|
|
401
401
|
updated_at: int
|
|
402
402
|
|
|
403
403
|
@dataclass
|
|
@@ -768,7 +768,7 @@ class Light(Packet):
|
|
|
768
768
|
|
|
769
769
|
color: LightHsbk
|
|
770
770
|
power: int
|
|
771
|
-
label:
|
|
771
|
+
label: str
|
|
772
772
|
|
|
773
773
|
@dataclass
|
|
774
774
|
class StateHevCycle(Packet):
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Repository interfaces and implementations for LIFX emulator.
|
|
2
|
+
|
|
3
|
+
This module defines repository abstractions following the Repository Pattern
|
|
4
|
+
and Dependency Inversion Principle. Repositories encapsulate data access logic
|
|
5
|
+
and provide a clean separation between domain logic and data persistence.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from lifx_emulator.repositories.device_repository import (
|
|
9
|
+
DeviceRepository,
|
|
10
|
+
IDeviceRepository,
|
|
11
|
+
)
|
|
12
|
+
from lifx_emulator.repositories.storage_backend import (
|
|
13
|
+
IDeviceStorageBackend,
|
|
14
|
+
IScenarioStorageBackend,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"IDeviceRepository",
|
|
19
|
+
"DeviceRepository",
|
|
20
|
+
"IDeviceStorageBackend",
|
|
21
|
+
"IScenarioStorageBackend",
|
|
22
|
+
]
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Device repository interface and implementation.
|
|
2
|
+
|
|
3
|
+
Provides abstraction for device storage and retrieval operations,
|
|
4
|
+
following the Repository Pattern and Dependency Inversion Principle.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Protocol, runtime_checkable
|
|
10
|
+
|
|
11
|
+
from lifx_emulator.devices import EmulatedLifxDevice
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@runtime_checkable
|
|
15
|
+
class IDeviceRepository(Protocol):
|
|
16
|
+
"""Interface for device repository operations.
|
|
17
|
+
|
|
18
|
+
This protocol defines the contract for managing device storage and retrieval.
|
|
19
|
+
Concrete implementations can use in-memory storage, databases, or other backends.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def add(self, device: EmulatedLifxDevice) -> bool:
|
|
23
|
+
"""Add a device to the repository.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
device: Device to add
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
True if device was added, False if device with same serial already exists
|
|
30
|
+
"""
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
def remove(self, serial: str) -> bool:
|
|
34
|
+
"""Remove a device from the repository.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
serial: Serial number of device to remove
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
True if device was removed, False if not found
|
|
41
|
+
"""
|
|
42
|
+
...
|
|
43
|
+
|
|
44
|
+
def get(self, serial: str) -> EmulatedLifxDevice | None:
|
|
45
|
+
"""Get a device by serial number.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
serial: Serial number to look up
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Device if found, None otherwise
|
|
52
|
+
"""
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
def get_all(self) -> list[EmulatedLifxDevice]:
|
|
56
|
+
"""Get all devices.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
List of all devices in the repository
|
|
60
|
+
"""
|
|
61
|
+
...
|
|
62
|
+
|
|
63
|
+
def clear(self) -> int:
|
|
64
|
+
"""Remove all devices from the repository.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Number of devices removed
|
|
68
|
+
"""
|
|
69
|
+
...
|
|
70
|
+
|
|
71
|
+
def count(self) -> int:
|
|
72
|
+
"""Get the number of devices in the repository.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Number of devices
|
|
76
|
+
"""
|
|
77
|
+
...
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class DeviceRepository:
|
|
81
|
+
"""In-memory device repository implementation.
|
|
82
|
+
|
|
83
|
+
Stores devices in a dictionary keyed by serial number.
|
|
84
|
+
This is the default implementation used by EmulatedLifxServer.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(self) -> None:
|
|
88
|
+
"""Initialize empty device repository."""
|
|
89
|
+
self._devices: dict[str, EmulatedLifxDevice] = {}
|
|
90
|
+
|
|
91
|
+
def add(self, device: EmulatedLifxDevice) -> bool:
|
|
92
|
+
"""Add a device to the repository.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
device: Device to add
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
True if device was added, False if device with same serial already exists
|
|
99
|
+
"""
|
|
100
|
+
serial = device.state.serial
|
|
101
|
+
if serial in self._devices:
|
|
102
|
+
return False
|
|
103
|
+
self._devices[serial] = device
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
def remove(self, serial: str) -> bool:
|
|
107
|
+
"""Remove a device from the repository.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
serial: Serial number of device to remove
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
True if device was removed, False if not found
|
|
114
|
+
"""
|
|
115
|
+
if serial in self._devices:
|
|
116
|
+
del self._devices[serial]
|
|
117
|
+
return True
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
def get(self, serial: str) -> EmulatedLifxDevice | None:
|
|
121
|
+
"""Get a device by serial number.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
serial: Serial number to look up
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Device if found, None otherwise
|
|
128
|
+
"""
|
|
129
|
+
return self._devices.get(serial)
|
|
130
|
+
|
|
131
|
+
def get_all(self) -> list[EmulatedLifxDevice]:
|
|
132
|
+
"""Get all devices.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
List of all devices in the repository
|
|
136
|
+
"""
|
|
137
|
+
return list(self._devices.values())
|
|
138
|
+
|
|
139
|
+
def clear(self) -> int:
|
|
140
|
+
"""Remove all devices from the repository.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Number of devices removed
|
|
144
|
+
"""
|
|
145
|
+
count = len(self._devices)
|
|
146
|
+
self._devices.clear()
|
|
147
|
+
return count
|
|
148
|
+
|
|
149
|
+
def count(self) -> int:
|
|
150
|
+
"""Get the number of devices in the repository.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Number of devices
|
|
154
|
+
"""
|
|
155
|
+
return len(self._devices)
|