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,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
|
+
)
|