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.
Files changed (40) hide show
  1. lifx_emulator/__init__.py +31 -0
  2. lifx_emulator/__main__.py +607 -0
  3. lifx_emulator/api.py +1825 -0
  4. lifx_emulator/async_storage.py +308 -0
  5. lifx_emulator/constants.py +33 -0
  6. lifx_emulator/device.py +750 -0
  7. lifx_emulator/device_states.py +114 -0
  8. lifx_emulator/factories.py +380 -0
  9. lifx_emulator/handlers/__init__.py +39 -0
  10. lifx_emulator/handlers/base.py +49 -0
  11. lifx_emulator/handlers/device_handlers.py +340 -0
  12. lifx_emulator/handlers/light_handlers.py +372 -0
  13. lifx_emulator/handlers/multizone_handlers.py +249 -0
  14. lifx_emulator/handlers/registry.py +110 -0
  15. lifx_emulator/handlers/tile_handlers.py +309 -0
  16. lifx_emulator/observers.py +139 -0
  17. lifx_emulator/products/__init__.py +28 -0
  18. lifx_emulator/products/generator.py +771 -0
  19. lifx_emulator/products/registry.py +1446 -0
  20. lifx_emulator/products/specs.py +242 -0
  21. lifx_emulator/products/specs.yml +327 -0
  22. lifx_emulator/protocol/__init__.py +1 -0
  23. lifx_emulator/protocol/base.py +334 -0
  24. lifx_emulator/protocol/const.py +8 -0
  25. lifx_emulator/protocol/generator.py +1371 -0
  26. lifx_emulator/protocol/header.py +159 -0
  27. lifx_emulator/protocol/packets.py +1351 -0
  28. lifx_emulator/protocol/protocol_types.py +844 -0
  29. lifx_emulator/protocol/serializer.py +379 -0
  30. lifx_emulator/scenario_manager.py +402 -0
  31. lifx_emulator/scenario_persistence.py +206 -0
  32. lifx_emulator/server.py +482 -0
  33. lifx_emulator/state_restorer.py +259 -0
  34. lifx_emulator/state_serializer.py +130 -0
  35. lifx_emulator/storage_protocol.py +100 -0
  36. lifx_emulator-1.0.0.dist-info/METADATA +445 -0
  37. lifx_emulator-1.0.0.dist-info/RECORD +40 -0
  38. lifx_emulator-1.0.0.dist-info/WHEEL +4 -0
  39. lifx_emulator-1.0.0.dist-info/entry_points.txt +2 -0
  40. lifx_emulator-1.0.0.dist-info/licenses/LICENSE +35 -0
@@ -0,0 +1,334 @@
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 re
14
+ from dataclasses import dataclass
15
+ from typing import Any, ClassVar
16
+
17
+ # Performance optimization: Pre-compiled regex patterns
18
+ _CAMEL_TO_SNAKE_PATTERN = re.compile(r"(?<!^)(?=[A-Z])")
19
+ _ARRAY_TYPE_PATTERN = re.compile(r"\[(\d+)\](.+)")
20
+
21
+ # Performance optimization: Field type parsing cache
22
+ _FIELD_TYPE_CACHE: dict[str, tuple[str, int | None, bool]] = {}
23
+
24
+ # Performance optimization: Field name conversion cache (per-class)
25
+ _FIELD_NAME_CACHE: dict[type, dict[str, str]] = {}
26
+
27
+
28
+ @dataclass
29
+ class Packet:
30
+ """Base class for all LIFX protocol packets.
31
+
32
+ Each packet subclass defines:
33
+ - PKT_TYPE: ClassVar[int] - The packet type number
34
+ - _fields: ClassVar[list[dict]] - Field metadata from protocol.yml
35
+ - Actual field attributes as dataclass fields
36
+ """
37
+
38
+ PKT_TYPE: ClassVar[int]
39
+ _fields: ClassVar[list[dict[str, Any]]]
40
+ _field_info: ClassVar[list[tuple[str, str, int]] | None] = None
41
+
42
+ def pack(self) -> bytes:
43
+ """Pack packet to bytes using field metadata.
44
+
45
+ Returns:
46
+ Packed bytes ready to send in a LIFX message payload
47
+ """
48
+ from lifx_emulator.protocol import serializer
49
+
50
+ result = b""
51
+
52
+ for field_item in self._fields:
53
+ # Handle reserved fields (no name)
54
+ if "name" not in field_item:
55
+ size_bytes = field_item.get("size_bytes", 0)
56
+ result += serializer.pack_reserved(size_bytes)
57
+ continue
58
+
59
+ # Get field value from instance
60
+ field_name = self._protocol_to_python_name(field_item["name"])
61
+ value = getattr(self, field_name)
62
+
63
+ # Pack based on type
64
+ field_type = field_item["type"]
65
+ size_bytes = field_item.get("size_bytes", 0)
66
+ result += self._pack_field_value(value, field_type, size_bytes)
67
+
68
+ return result
69
+
70
+ @classmethod
71
+ def unpack(cls, data: bytes, offset: int = 0) -> Packet:
72
+ """Unpack packet from bytes using field metadata.
73
+
74
+ Args:
75
+ data: Bytes to unpack from
76
+ offset: Offset in bytes to start unpacking
77
+
78
+ Returns:
79
+ Packet instance
80
+ """
81
+ packet, _ = cls._unpack_internal(data, offset)
82
+ return packet
83
+
84
+ @classmethod
85
+ def _compute_field_info(cls) -> list[tuple[str, str, int]]:
86
+ """Pre-compute parsed field metadata for faster unpacking.
87
+
88
+ This optimization caches the parsing of field metadata to avoid
89
+ repeated dictionary lookups and name conversions during unpacking.
90
+
91
+ Returns:
92
+ List of tuples: (field_name, field_type, size_bytes)
93
+ Reserved fields have empty string as field_name
94
+ """
95
+ info: list[tuple[str, str, int]] = []
96
+
97
+ for field_item in cls._fields:
98
+ size_bytes = field_item.get("size_bytes", 0)
99
+
100
+ # Handle reserved fields (no name)
101
+ if "name" not in field_item:
102
+ info.append(("", "", size_bytes))
103
+ continue
104
+
105
+ # Regular field
106
+ field_name = cls._protocol_to_python_name(field_item["name"])
107
+ field_type = field_item["type"]
108
+ info.append((field_name, field_type, size_bytes))
109
+
110
+ return info
111
+
112
+ @classmethod
113
+ def _unpack_internal(cls, data: bytes, offset: int) -> tuple[Packet, int]:
114
+ """Internal method for unpacking packets with offset tracking.
115
+
116
+ This method handles the recursion needed for nested structures.
117
+
118
+ Args:
119
+ data: Bytes to unpack from
120
+ offset: Offset in bytes to start unpacking
121
+
122
+ Returns:
123
+ Tuple of (packet_instance, new_offset)
124
+ """
125
+ # Pre-compute field info on first use (lazy initialization)
126
+ if cls._field_info is None:
127
+ cls._field_info = cls._compute_field_info()
128
+
129
+ current_offset = offset
130
+ field_values: dict[str, Any] = {}
131
+
132
+ for field_name, field_type, size_bytes in cls._field_info:
133
+ # Handle reserved fields (empty name)
134
+ if not field_name:
135
+ current_offset += size_bytes
136
+ continue
137
+
138
+ # Unpack field value
139
+ value, current_offset = cls._unpack_field_value(
140
+ data, field_type, size_bytes, current_offset
141
+ )
142
+ field_values[field_name] = value
143
+
144
+ return cls(**field_values), current_offset
145
+
146
+ @classmethod
147
+ def _protocol_to_python_name(cls, name: str) -> str:
148
+ """Convert protocol name (PascalCase) to Python name (snake_case).
149
+
150
+ Performance optimized with caching and pre-compiled regex.
151
+ """
152
+ # Check class-specific cache first
153
+ if cls not in _FIELD_NAME_CACHE:
154
+ _FIELD_NAME_CACHE[cls] = {}
155
+
156
+ cache = _FIELD_NAME_CACHE[cls]
157
+
158
+ if name not in cache:
159
+ # Convert using pre-compiled pattern
160
+ snake = _CAMEL_TO_SNAKE_PATTERN.sub("_", name)
161
+ cache[name] = snake.lower()
162
+
163
+ return cache[name]
164
+
165
+ def _pack_field_value(self, value: Any, field_type: str, size_bytes: int) -> bytes:
166
+ """Pack a single field value based on its type."""
167
+ from lifx_emulator.protocol import serializer
168
+
169
+ # Parse field type
170
+ base_type, array_count, is_nested = self._parse_field_type(field_type)
171
+
172
+ # Check if it's an enum (Button/Relay enums excluded)
173
+ enum_types = {
174
+ "DeviceService",
175
+ "LightLastHevCycleResult",
176
+ "LightWaveform",
177
+ "MultiZoneApplicationRequest",
178
+ "MultiZoneEffectType",
179
+ "MultiZoneExtendedApplicationRequest",
180
+ }
181
+ is_enum = is_nested and base_type in enum_types
182
+
183
+ # Handle different field types
184
+ if array_count:
185
+ if is_enum:
186
+ # Array of enums
187
+ result = b""
188
+ for item in value:
189
+ result += serializer.pack_value(int(item), "uint8")
190
+ return result
191
+ elif is_nested:
192
+ # Array of nested structures
193
+ result = b""
194
+ for item in value:
195
+ result += item.pack()
196
+ return result
197
+ elif base_type in ("uint8", "byte"):
198
+ # Byte array
199
+ return serializer.pack_bytes(value, size_bytes)
200
+ else:
201
+ # Array of primitives
202
+ return serializer.pack_array(value, base_type, array_count)
203
+ elif is_enum:
204
+ # Single enum
205
+ return serializer.pack_value(int(value), "uint8")
206
+ elif is_nested:
207
+ # Nested structure
208
+ return value.pack()
209
+ else:
210
+ # Primitive type
211
+ return serializer.pack_value(value, base_type)
212
+
213
+ @classmethod
214
+ def _unpack_field_value(
215
+ cls, data: bytes, field_type: str, size_bytes: int, offset: int
216
+ ) -> tuple[Any, int]:
217
+ """Unpack a single field value based on its type."""
218
+ from lifx_emulator.protocol import serializer
219
+ from lifx_emulator.protocol.protocol_types import (
220
+ DeviceService,
221
+ LightLastHevCycleResult,
222
+ LightWaveform,
223
+ MultiZoneApplicationRequest,
224
+ MultiZoneEffectType,
225
+ MultiZoneExtendedApplicationRequest,
226
+ )
227
+
228
+ # Parse field type
229
+ base_type, array_count, is_nested = cls._parse_field_type(field_type)
230
+
231
+ # Check if it's an enum (Button/Relay enums excluded)
232
+ enum_types = {
233
+ "DeviceService": DeviceService,
234
+ "LightLastHevCycleResult": LightLastHevCycleResult,
235
+ "LightWaveform": LightWaveform,
236
+ "MultiZoneApplicationRequest": MultiZoneApplicationRequest,
237
+ "MultiZoneEffectType": MultiZoneEffectType,
238
+ "MultiZoneExtendedApplicationRequest": MultiZoneExtendedApplicationRequest,
239
+ }
240
+ is_enum = is_nested and base_type in enum_types
241
+
242
+ # Handle different field types
243
+ if array_count:
244
+ if is_enum:
245
+ # Array of enums
246
+ result = []
247
+ current_offset = offset
248
+ enum_class = enum_types[base_type]
249
+ for _ in range(array_count):
250
+ item_raw, current_offset = serializer.unpack_value(
251
+ data, "uint8", current_offset
252
+ )
253
+ result.append(enum_class(item_raw))
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)
294
+ else:
295
+ # Primitive type
296
+ return serializer.unpack_value(data, base_type, offset)
297
+
298
+ @staticmethod
299
+ def _parse_field_type(field_type: str) -> tuple[str, int | None, bool]:
300
+ """Parse a field type string with caching.
301
+
302
+ Performance optimized with global cache and pre-compiled regex.
303
+
304
+ Args:
305
+ field_type: Field type (e.g., 'uint16', '[32]uint8', '<HSBK>')
306
+
307
+ Returns:
308
+ Tuple of (base_type, array_count, is_nested)
309
+ """
310
+ # Check cache first
311
+ if field_type in _FIELD_TYPE_CACHE:
312
+ return _FIELD_TYPE_CACHE[field_type]
313
+
314
+ # Parse type
315
+ # Check for array: [N]type
316
+ array_match = _ARRAY_TYPE_PATTERN.match(field_type)
317
+ if array_match:
318
+ count = int(array_match.group(1))
319
+ inner_type = array_match.group(2)
320
+ # Check if inner type is nested
321
+ if inner_type.startswith("<") and inner_type.endswith(">"):
322
+ result = (inner_type[1:-1], count, True)
323
+ else:
324
+ result = (inner_type, count, False)
325
+ # Check for nested structure: <Type>
326
+ elif field_type.startswith("<") and field_type.endswith(">"):
327
+ result = (field_type[1:-1], None, True)
328
+ else:
329
+ # Simple type
330
+ result = (field_type, None, False)
331
+
332
+ # Cache the result
333
+ _FIELD_TYPE_CACHE[field_type] = result
334
+ return result
@@ -0,0 +1,8 @@
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
+ )