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,159 @@
1
+ """LIFX protocol header implementation.
2
+
3
+ The LIFX header is 36 bytes total, consisting of:
4
+ - Frame (8 bytes)
5
+ - Frame Address (16 bytes)
6
+ - Protocol Header (12 bytes)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import struct
12
+ from dataclasses import dataclass
13
+ from typing import ClassVar
14
+
15
+ from lifx_emulator.constants import LIFX_PROTOCOL_VERSION
16
+
17
+ # Pre-compiled struct for entire 36-byte header (performance optimization)
18
+ _HEADER_STRUCT = struct.Struct("<HHI Q6sBB QHH")
19
+
20
+
21
+ @dataclass
22
+ class LifxHeader:
23
+ """LIFX protocol header (36 bytes).
24
+
25
+ Attributes:
26
+ size: Total packet size in bytes (header + payload)
27
+ protocol: Protocol number (must be 1024)
28
+ source: Unique client identifier
29
+ target: Device serial (8 bytes)
30
+ tagged: True for broadcast discovery, False for targeted messages
31
+ ack_required: Request acknowledgement from device
32
+ res_required: Request response from device
33
+ sequence: Sequence number for matching requests/responses
34
+ pkt_type: Packet type identifier
35
+ """
36
+
37
+ HEADER_SIZE: ClassVar[int] = 36
38
+ PROTOCOL_NUMBER: ClassVar[int] = 1024
39
+ ORIGIN: ClassVar[int] = 0 # Always 0
40
+ ADDRESSABLE: ClassVar[int] = 1 # Always 1
41
+
42
+ size: int = 0
43
+ protocol: int = LIFX_PROTOCOL_VERSION
44
+ source: int = 0
45
+ target: bytes = b"\x00" * 8 # Stored as 8 bytes internally
46
+ tagged: bool = False
47
+ ack_required: bool = False
48
+ res_required: bool = False
49
+ sequence: int = 0
50
+ pkt_type: int = 0
51
+
52
+ def __post_init__(self) -> None:
53
+ """Validate header fields and auto-pad serial if needed."""
54
+ # Ensure target is 8 bytes
55
+ if len(self.target) < 8:
56
+ self.target = self.target + b"\x00" * (8 - len(self.target))
57
+ elif len(self.target) > 8:
58
+ self.target = self.target[:8]
59
+
60
+ # Validate protocol
61
+ if self.protocol != self.PROTOCOL_NUMBER:
62
+ raise ValueError(
63
+ f"Invalid protocol number: {self.protocol}"
64
+ f"(expected {self.PROTOCOL_NUMBER})"
65
+ )
66
+
67
+ def pack(self) -> bytes:
68
+ """Pack header into 36 bytes using optimized single struct call.
69
+
70
+ Returns:
71
+ Packed header bytes
72
+ """
73
+ # Calculate flag fields
74
+ frame_flags = (
75
+ (self.protocol & 0xFFF)
76
+ | (self.ADDRESSABLE << 12)
77
+ | ((1 if self.tagged else 0) << 13)
78
+ | ((self.ORIGIN & 0x3) << 14)
79
+ )
80
+ target_int = int.from_bytes(self.target[:8], byteorder="little")
81
+ addr_flags = (1 if self.res_required else 0) | (
82
+ (1 if self.ack_required else 0) << 1
83
+ )
84
+
85
+ # Pack entire header in single struct call (15-20% faster than 3 separate calls)
86
+ return _HEADER_STRUCT.pack(
87
+ self.size, # H - Frame: size
88
+ frame_flags, # H - Frame: flags (protocol, tagged, addressable, origin)
89
+ self.source, # I - Frame: source
90
+ target_int, # Q - Frame Address: target (8 bytes as uint64)
91
+ b"\x00" * 6, # 6s - Frame Address: reserved (6 bytes)
92
+ addr_flags, # B - Frame Address: flags (ack_required, res_required)
93
+ self.sequence, # B - Frame Address: sequence
94
+ 0, # Q - Protocol Header: reserved (8 bytes)
95
+ self.pkt_type, # H - Protocol Header: packet type
96
+ 0, # H - Protocol Header: reserved (2 bytes)
97
+ )
98
+
99
+ @classmethod
100
+ def unpack(cls, data: bytes) -> LifxHeader:
101
+ """Unpack header from bytes.
102
+
103
+ Args:
104
+ data: Header bytes (at least 36 bytes)
105
+
106
+ Returns:
107
+ LifxHeader instance
108
+
109
+ Raises:
110
+ ValueError: If data is too short or invalid
111
+ """
112
+ if len(data) < cls.HEADER_SIZE:
113
+ raise ValueError(f"Header data must be at least {cls.HEADER_SIZE} bytes")
114
+
115
+ # Unpack Frame (8 bytes)
116
+ size, protocol_field, source = struct.unpack("<HHI", data[0:8])
117
+
118
+ # Extract protocol field components
119
+ origin = (protocol_field >> 14) & 0b11
120
+ tagged = bool((protocol_field >> 13) & 0b1)
121
+ addressable = bool((protocol_field >> 12) & 0b1)
122
+ protocol = protocol_field & 0xFFF
123
+
124
+ # Validate origin and addressable
125
+ if origin != cls.ORIGIN:
126
+ raise ValueError(f"Invalid origin: {origin}")
127
+ if not addressable:
128
+ raise ValueError("Addressable bit must be set")
129
+
130
+ # Unpack Frame Address (16 bytes)
131
+ target_int, _reserved, flags, sequence = struct.unpack("<Q6sBB", data[8:24])
132
+ target = target_int.to_bytes(8, byteorder="little")
133
+
134
+ res_required = bool(flags & 0b1)
135
+ ack_required = bool((flags >> 1) & 0b1)
136
+
137
+ # Unpack Protocol Header (12 bytes)
138
+ _reserved1, pkt_type, _reserved2 = struct.unpack("<QHH", data[24:36])
139
+
140
+ return cls(
141
+ size=size,
142
+ protocol=protocol,
143
+ source=source,
144
+ target=target,
145
+ tagged=tagged,
146
+ ack_required=ack_required,
147
+ res_required=res_required,
148
+ sequence=sequence,
149
+ pkt_type=pkt_type,
150
+ )
151
+
152
+ def __repr__(self) -> str:
153
+ """String representation of header."""
154
+ return (
155
+ f"LifxHeader(size={self.size}, protocol={self.protocol}, "
156
+ f"source={self.source}, target={self.target.hex()}, "
157
+ f"tagged={self.tagged}, ack={self.ack_required}, "
158
+ f"res={self.res_required}, seq={self.sequence}, pkt_type={self.pkt_type})"
159
+ )