lifx-emulator 2.3.1__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 +13 -5
  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 -339
  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 -377
  29. lifx_emulator/factories/__init__.py +0 -37
  30. lifx_emulator/factories/builder.py +0 -373
  31. lifx_emulator/factories/default_config.py +0 -158
  32. lifx_emulator/factories/factory.py +0 -221
  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 -1037
  44. lifx_emulator/products/registry.py +0 -1496
  45. lifx_emulator/products/specs.py +0 -284
  46. lifx_emulator/products/specs.yml +0 -352
  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.3.1.dist-info/METADATA +0 -107
  64. lifx_emulator-2.3.1.dist-info/RECORD +0 -62
  65. lifx_emulator-2.3.1.dist-info/entry_points.txt +0 -2
  66. lifx_emulator-2.3.1.dist-info/licenses/LICENSE +0 -35
  67. {lifx_emulator-2.3.1.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,446 +0,0 @@
1
- """Base packet class for LIFX protocol.
2
-
3
- Provides generic pack/unpack functionality for all packet types.
4
-
5
- Performance optimizations:
6
- - Pre-compiled regex patterns for field name conversion
7
- - Cached field type parsing results
8
- - Pre-computed field info for faster unpacking
9
- """
10
-
11
- from __future__ import annotations
12
-
13
- import logging
14
- import re
15
- from dataclasses import asdict, dataclass
16
- from typing import Any, ClassVar
17
-
18
- _LOGGER = logging.getLogger(__name__)
19
-
20
- # Performance optimization: Pre-compiled regex patterns
21
- _CAMEL_TO_SNAKE_PATTERN = re.compile(r"(?<!^)(?=[A-Z])")
22
- _ARRAY_TYPE_PATTERN = re.compile(r"\[(\d+)\](.+)")
23
-
24
- # Performance optimization: Field type parsing cache
25
- _FIELD_TYPE_CACHE: dict[str, tuple[str, int | None, bool]] = {}
26
-
27
- # Performance optimization: Field name conversion cache (per-class)
28
- _FIELD_NAME_CACHE: dict[type, dict[str, str]] = {}
29
-
30
-
31
- @dataclass
32
- class Packet:
33
- """Base class for all LIFX protocol packets.
34
-
35
- Each packet subclass defines:
36
- - PKT_TYPE: ClassVar[int] - The packet type number
37
- - _fields: ClassVar[list[dict]] - Field metadata from protocol.yml
38
- - Actual field attributes as dataclass fields
39
- """
40
-
41
- PKT_TYPE: ClassVar[int]
42
- _fields: ClassVar[list[dict[str, Any]]]
43
- _field_info: ClassVar[list[tuple[str, str, int]] | None] = None
44
-
45
- @property
46
- def as_dict(self) -> dict[str, Any]:
47
- """Return packet as dictionary."""
48
- return asdict(self)
49
-
50
- def pack(self) -> bytes:
51
- """Pack packet to bytes using field metadata.
52
-
53
- Returns:
54
- Packed bytes ready to send in a LIFX message payload
55
- """
56
- from lifx_emulator.protocol import serializer
57
-
58
- result = b""
59
-
60
- for field_item in self._fields:
61
- # Handle reserved fields (no name)
62
- if "name" not in field_item:
63
- size_bytes = field_item.get("size_bytes", 0)
64
- result += serializer.pack_reserved(size_bytes)
65
- continue
66
-
67
- # Get field value from instance
68
- field_name = self._protocol_to_python_name(field_item["name"])
69
- value = getattr(self, field_name)
70
-
71
- # Pack based on type
72
- field_type = field_item["type"]
73
- size_bytes = field_item.get("size_bytes", 0)
74
- result += self._pack_field_value(value, field_type, size_bytes)
75
-
76
- return result
77
-
78
- @classmethod
79
- def unpack(cls, data: bytes, offset: int = 0) -> Packet:
80
- """Unpack packet from bytes using field metadata.
81
-
82
- Args:
83
- data: Bytes to unpack from
84
- offset: Offset in bytes to start unpacking
85
-
86
- Returns:
87
- Packet instance with label fields decoded to strings
88
- """
89
- packet, _ = cls._unpack_internal(data, offset)
90
-
91
- # Decode label fields from bytes to string in-place
92
- # This ensures all State packets have human-readable labels
93
- cls._decode_labels_inplace(packet)
94
-
95
- # Log packet values after unpacking and decoding labels
96
- packet_values = asdict(packet)
97
- _LOGGER.debug(
98
- {
99
- "class": "Packet",
100
- "method": "unpack",
101
- "packet_type": type(packet).__name__,
102
- "values": packet_values,
103
- }
104
- )
105
-
106
- return packet
107
-
108
- @classmethod
109
- def _compute_field_info(cls) -> list[tuple[str, str, int]]:
110
- """Pre-compute parsed field metadata for faster unpacking.
111
-
112
- This optimization caches the parsing of field metadata to avoid
113
- repeated dictionary lookups and name conversions during unpacking.
114
-
115
- Returns:
116
- List of tuples: (field_name, field_type, size_bytes)
117
- Reserved fields have empty string as field_name
118
- """
119
- info: list[tuple[str, str, int]] = []
120
-
121
- for field_item in cls._fields:
122
- size_bytes = field_item.get("size_bytes", 0)
123
-
124
- # Handle reserved fields (no name)
125
- if "name" not in field_item:
126
- info.append(("", "", size_bytes))
127
- continue
128
-
129
- # Regular field
130
- field_name = cls._protocol_to_python_name(field_item["name"])
131
- field_type = field_item["type"]
132
- info.append((field_name, field_type, size_bytes))
133
-
134
- return info
135
-
136
- @classmethod
137
- def _unpack_internal(cls, data: bytes, offset: int) -> tuple[Packet, int]:
138
- """Internal method for unpacking packets with offset tracking.
139
-
140
- This method handles the recursion needed for nested structures.
141
-
142
- Args:
143
- data: Bytes to unpack from
144
- offset: Offset in bytes to start unpacking
145
-
146
- Returns:
147
- Tuple of (packet_instance, new_offset)
148
- """
149
- # Pre-compute field info on first use (lazy initialization)
150
- if cls._field_info is None:
151
- cls._field_info = cls._compute_field_info()
152
-
153
- current_offset = offset
154
- field_values: dict[str, Any] = {}
155
-
156
- for field_name, field_type, size_bytes in cls._field_info:
157
- # Handle reserved fields (empty name)
158
- if not field_name:
159
- current_offset += size_bytes
160
- continue
161
-
162
- # Unpack field value
163
- value, current_offset = cls._unpack_field_value(
164
- data, field_type, size_bytes, current_offset, field_name
165
- )
166
- field_values[field_name] = value
167
-
168
- return cls(**field_values), current_offset
169
-
170
- @classmethod
171
- def _protocol_to_python_name(cls, name: str) -> str:
172
- """Convert protocol name (PascalCase) to Python name (snake_case).
173
-
174
- Performance optimized with caching and pre-compiled regex.
175
- """
176
- # Check class-specific cache first
177
- if cls not in _FIELD_NAME_CACHE:
178
- _FIELD_NAME_CACHE[cls] = {}
179
-
180
- cache = _FIELD_NAME_CACHE[cls]
181
-
182
- if name not in cache:
183
- # Convert using pre-compiled pattern
184
- snake = _CAMEL_TO_SNAKE_PATTERN.sub("_", name)
185
- cache[name] = snake.lower()
186
-
187
- return cache[name]
188
-
189
- def _pack_field_value(self, value: Any, field_type: str, size_bytes: int) -> bytes:
190
- """Pack a single field value based on its type."""
191
- from lifx_emulator.protocol import serializer
192
-
193
- # Parse field type
194
- base_type, array_count, is_nested = self._parse_field_type(field_type)
195
-
196
- # Check if it's an enum (Button/Relay enums excluded)
197
- enum_types = {
198
- "DeviceService",
199
- "LightLastHevCycleResult",
200
- "LightWaveform",
201
- "MultiZoneApplicationRequest",
202
- "MultiZoneEffectType",
203
- "MultiZoneExtendedApplicationRequest",
204
- "TileEffectSkyPalette",
205
- "TileEffectSkyType",
206
- "TileEffectType",
207
- }
208
- is_enum = is_nested and base_type in enum_types
209
-
210
- # Handle different field types
211
- if array_count:
212
- if is_enum:
213
- # Array of enums
214
- result = b""
215
- for item in value:
216
- result += serializer.pack_value(int(item), "uint8")
217
- return result
218
- elif is_nested:
219
- # Array of nested structures
220
- result = b""
221
- for item in value:
222
- result += item.pack()
223
- return result
224
- elif base_type in ("uint8", "byte"):
225
- # Check if value is a string (Label fields)
226
- if isinstance(value, str):
227
- return serializer.pack_string(value, size_bytes)
228
- # Regular byte array
229
- return serializer.pack_bytes(value, size_bytes)
230
- else:
231
- # Array of primitives
232
- return serializer.pack_array(value, base_type, array_count)
233
- elif is_enum:
234
- # Single enum
235
- return serializer.pack_value(int(value), "uint8")
236
- elif is_nested:
237
- # Nested structure
238
- return value.pack()
239
- else:
240
- # Primitive type
241
- return serializer.pack_value(value, base_type)
242
-
243
- @classmethod
244
- def _unpack_array_field(
245
- cls,
246
- data: bytes,
247
- base_type: str,
248
- array_count: int,
249
- size_bytes: int,
250
- offset: int,
251
- field_name: str,
252
- is_nested: bool,
253
- enum_types: dict,
254
- ) -> tuple[Any, int]:
255
- """Unpack an array field value."""
256
- from lifx_emulator.protocol import serializer
257
-
258
- is_enum = is_nested and base_type in enum_types
259
-
260
- if is_enum:
261
- result = []
262
- current_offset = offset
263
- enum_class = enum_types[base_type]
264
- for _ in range(array_count):
265
- item_raw, current_offset = serializer.unpack_value(
266
- data, "uint8", current_offset
267
- )
268
- result.append(enum_class(item_raw))
269
- return result, current_offset
270
- elif is_nested:
271
- from lifx_emulator.protocol import protocol_types
272
-
273
- struct_class = getattr(protocol_types, base_type)
274
- result = []
275
- current_offset = offset
276
- for _ in range(array_count):
277
- if issubclass(struct_class, cls):
278
- item, current_offset = struct_class._unpack_internal(
279
- data, current_offset
280
- )
281
- else:
282
- item_result = struct_class.unpack(data, current_offset)
283
- item, current_offset = item_result # type: ignore[misc]
284
- result.append(item)
285
- return result, current_offset
286
- elif base_type in ("uint8", "byte"):
287
- if field_name.lower().endswith("label"):
288
- return serializer.unpack_string(data, size_bytes, offset)
289
- return serializer.unpack_bytes(data, size_bytes, offset)
290
- else:
291
- return serializer.unpack_array(data, base_type, array_count, offset)
292
-
293
- @classmethod
294
- def _unpack_single_field(
295
- cls,
296
- data: bytes,
297
- base_type: str,
298
- offset: int,
299
- is_nested: bool,
300
- enum_types: dict,
301
- ) -> tuple[Any, int]:
302
- """Unpack a non-array field value."""
303
- from lifx_emulator.protocol import serializer
304
-
305
- is_enum = is_nested and base_type in enum_types
306
-
307
- if is_enum:
308
- enum_class = enum_types[base_type]
309
- value_raw, new_offset = serializer.unpack_value(data, "uint8", offset)
310
- return enum_class(value_raw), new_offset
311
- elif is_nested:
312
- from lifx_emulator.protocol import protocol_types
313
-
314
- struct_class = getattr(protocol_types, base_type)
315
- if issubclass(struct_class, cls):
316
- return struct_class._unpack_internal(data, offset)
317
- else:
318
- return struct_class.unpack(data, offset)
319
- else:
320
- return serializer.unpack_value(data, base_type, offset)
321
-
322
- @classmethod
323
- def _unpack_field_value(
324
- cls,
325
- data: bytes,
326
- field_type: str,
327
- size_bytes: int,
328
- offset: int,
329
- field_name: str = "",
330
- ) -> tuple[Any, int]:
331
- """Unpack a single field value based on its type.
332
-
333
- Args:
334
- data: Bytes to unpack from
335
- field_type: Protocol field type string
336
- size_bytes: Size in bytes
337
- offset: Offset in bytes
338
- field_name: Optional field name for semantic type detection
339
-
340
- Returns:
341
- Tuple of (value, new_offset)
342
- """
343
- from lifx_emulator.protocol.protocol_types import (
344
- DeviceService,
345
- LightLastHevCycleResult,
346
- LightWaveform,
347
- MultiZoneApplicationRequest,
348
- MultiZoneEffectType,
349
- MultiZoneExtendedApplicationRequest,
350
- TileEffectSkyPalette,
351
- TileEffectSkyType,
352
- TileEffectType,
353
- )
354
-
355
- base_type, array_count, is_nested = cls._parse_field_type(field_type)
356
-
357
- enum_types = {
358
- "DeviceService": DeviceService,
359
- "LightLastHevCycleResult": LightLastHevCycleResult,
360
- "LightWaveform": LightWaveform,
361
- "MultiZoneApplicationRequest": MultiZoneApplicationRequest,
362
- "MultiZoneEffectType": MultiZoneEffectType,
363
- "MultiZoneExtendedApplicationRequest": MultiZoneExtendedApplicationRequest,
364
- "TileEffectSkyPalette": TileEffectSkyPalette,
365
- "TileEffectSkyType": TileEffectSkyType,
366
- "TileEffectType": TileEffectType,
367
- }
368
-
369
- if array_count:
370
- return cls._unpack_array_field(
371
- data,
372
- base_type,
373
- array_count,
374
- size_bytes,
375
- offset,
376
- field_name,
377
- is_nested,
378
- enum_types,
379
- )
380
- else:
381
- return cls._unpack_single_field(
382
- data, base_type, offset, is_nested, enum_types
383
- )
384
-
385
- @staticmethod
386
- def _parse_field_type(field_type: str) -> tuple[str, int | None, bool]:
387
- """Parse a field type string with caching.
388
-
389
- Performance optimized with global cache and pre-compiled regex.
390
-
391
- Args:
392
- field_type: Field type (e.g., 'uint16', '[32]uint8', '<HSBK>')
393
-
394
- Returns:
395
- Tuple of (base_type, array_count, is_nested)
396
- """
397
- # Check cache first
398
- if field_type in _FIELD_TYPE_CACHE:
399
- return _FIELD_TYPE_CACHE[field_type]
400
-
401
- # Parse type
402
- # Check for array: [N]type
403
- array_match = _ARRAY_TYPE_PATTERN.match(field_type)
404
- if array_match:
405
- count = int(array_match.group(1))
406
- inner_type = array_match.group(2)
407
- # Check if inner type is nested
408
- if inner_type.startswith("<") and inner_type.endswith(">"):
409
- result = (inner_type[1:-1], count, True)
410
- else:
411
- result = (inner_type, count, False)
412
- # Check for nested structure: <Type>
413
- elif field_type.startswith("<") and field_type.endswith(">"):
414
- result = (field_type[1:-1], None, True)
415
- else:
416
- # Simple type
417
- result = (field_type, None, False)
418
-
419
- # Cache the result
420
- _FIELD_TYPE_CACHE[field_type] = result
421
- return result
422
-
423
- @staticmethod
424
- def _decode_labels_inplace(packet: object) -> None:
425
- """Decode label fields from bytes to string in-place.
426
-
427
- Automatically finds and decodes any field named 'label' or ending with '_label'
428
- for all State packets. This ensures human-readable labels in all contexts.
429
-
430
- Args:
431
- packet: Packet instance to process (modified in-place)
432
- """
433
- from dataclasses import fields, is_dataclass
434
-
435
- if not is_dataclass(packet):
436
- return
437
-
438
- for field_info in fields(packet):
439
- # Check if this looks like a label field
440
- if field_info.name == "label" or field_info.name.endswith("_label"):
441
- value = getattr(packet, field_info.name)
442
- if isinstance(value, bytes):
443
- # Decode: strip null terminator, decode UTF-8
444
- decoded = value.rstrip(b"\x00").decode("utf-8")
445
- # Use object.__setattr__ to bypass frozen dataclass if needed
446
- object.__setattr__(packet, field_info.name, decoded)
@@ -1,8 +0,0 @@
1
- # lifx-emulator protocol constants
2
-
3
- from typing import Final
4
-
5
- # Official LIFX protocol specification URL
6
- PROTOCOL_URL: Final[str] = (
7
- "https://raw.githubusercontent.com/LIFX/public-protocol/refs/heads/main/protocol.yml"
8
- )