lifx-emulator-core 3.2.0__py3-none-any.whl → 3.4.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/devices/device.py +25 -12
- lifx_emulator/devices/states.py +2 -0
- lifx_emulator/factories/builder.py +1 -0
- lifx_emulator/scenarios/models.py +12 -0
- lifx_emulator/server.py +55 -4
- {lifx_emulator_core-3.2.0.dist-info → lifx_emulator_core-3.4.0.dist-info}/METADATA +1 -1
- {lifx_emulator_core-3.2.0.dist-info → lifx_emulator_core-3.4.0.dist-info}/RECORD +8 -8
- {lifx_emulator_core-3.2.0.dist-info → lifx_emulator_core-3.4.0.dist-info}/WHEEL +0 -0
lifx_emulator/devices/device.py
CHANGED
|
@@ -270,10 +270,8 @@ class EmulatedLifxDevice:
|
|
|
270
270
|
state_unhandled.PKT_TYPE,
|
|
271
271
|
len(unhandled_payload),
|
|
272
272
|
)
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
# Still send acknowledgment if requested
|
|
276
|
-
if header.ack_required:
|
|
273
|
+
# Send ack before StateUnhandled when scenario controls ack behavior
|
|
274
|
+
if header.ack_required and scenario.affects_acks:
|
|
277
275
|
ack_packet = Device.Acknowledgement()
|
|
278
276
|
ack_payload = ack_packet.pack()
|
|
279
277
|
ack_header = self._create_response_header(
|
|
@@ -282,15 +280,18 @@ class EmulatedLifxDevice:
|
|
|
282
280
|
ack_packet.PKT_TYPE,
|
|
283
281
|
len(ack_payload),
|
|
284
282
|
)
|
|
285
|
-
responses.append((ack_header, ack_packet))
|
|
283
|
+
responses.append((ack_header, ack_packet, ack_payload))
|
|
286
284
|
|
|
287
|
-
|
|
285
|
+
responses.append((unhandled_header, state_unhandled, unhandled_payload))
|
|
286
|
+
return self._apply_error_scenarios(responses, scenario)
|
|
288
287
|
|
|
289
288
|
# Update uptime
|
|
290
289
|
self.state.uptime_ns = self.get_uptime_ns()
|
|
291
290
|
|
|
292
291
|
# Handle acknowledgment (packet type 45, no payload)
|
|
293
|
-
|
|
292
|
+
# Only generate ack here when a scenario targets ack behavior;
|
|
293
|
+
# otherwise the server sends the ack immediately before calling us.
|
|
294
|
+
if header.ack_required and scenario.affects_acks:
|
|
294
295
|
ack_packet = Device.Acknowledgement()
|
|
295
296
|
ack_payload = ack_packet.pack()
|
|
296
297
|
ack_header = self._create_response_header(
|
|
@@ -336,16 +337,29 @@ class EmulatedLifxDevice:
|
|
|
336
337
|
# Store both header and pre-packed payload for error scenario processing
|
|
337
338
|
responses.append((resp_header, resp_packet, resp_payload))
|
|
338
339
|
|
|
339
|
-
|
|
340
|
-
|
|
340
|
+
return self._apply_error_scenarios(responses, scenario)
|
|
341
|
+
|
|
342
|
+
def _apply_error_scenarios(
|
|
343
|
+
self,
|
|
344
|
+
responses: list[tuple],
|
|
345
|
+
scenario: ScenarioConfig,
|
|
346
|
+
) -> list[tuple[LifxHeader, Any]]:
|
|
347
|
+
"""Apply malformed/invalid-field error scenarios to response packets.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
responses: List of (header, packet, payload) tuples
|
|
351
|
+
scenario: Resolved scenario config
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
List of (header, packet) tuples with error scenarios applied
|
|
355
|
+
"""
|
|
356
|
+
modified_responses: list[tuple[LifxHeader, Any]] = []
|
|
341
357
|
for resp_header, resp_packet, resp_payload in responses:
|
|
342
358
|
# Check if we should send malformed packet (truncate payload)
|
|
343
359
|
if resp_header.pkt_type in scenario.malformed_packets:
|
|
344
|
-
# For malformed packets, truncate the pre-packed payload
|
|
345
360
|
truncated_len = len(resp_payload) // 2
|
|
346
361
|
resp_payload_modified = resp_payload[:truncated_len]
|
|
347
362
|
resp_header.size = LIFX_HEADER_SIZE + truncated_len + 10 # Wrong size
|
|
348
|
-
# Convert back to bytes for malformed case
|
|
349
363
|
modified_responses.append((resp_header, resp_payload_modified))
|
|
350
364
|
logger.info(
|
|
351
365
|
"Sending malformed packet type %s (truncated)", resp_header.pkt_type
|
|
@@ -354,7 +368,6 @@ class EmulatedLifxDevice:
|
|
|
354
368
|
|
|
355
369
|
# Check if we should send invalid field values
|
|
356
370
|
if resp_header.pkt_type in scenario.invalid_field_values:
|
|
357
|
-
# Corrupt the pre-packed payload
|
|
358
371
|
resp_payload_modified = b"\xff" * len(resp_payload)
|
|
359
372
|
modified_responses.append((resp_header, resp_payload_modified))
|
|
360
373
|
pkt_type = resp_header.pkt_type
|
lifx_emulator/devices/states.py
CHANGED
|
@@ -182,6 +182,7 @@ class DeviceState:
|
|
|
182
182
|
has_multizone: bool = False
|
|
183
183
|
has_extended_multizone: bool = False
|
|
184
184
|
has_matrix: bool = False
|
|
185
|
+
has_chain: bool = False
|
|
185
186
|
has_hev: bool = False
|
|
186
187
|
has_relays: bool = False
|
|
187
188
|
has_buttons: bool = False
|
|
@@ -348,6 +349,7 @@ class DeviceState:
|
|
|
348
349
|
"has_multizone",
|
|
349
350
|
"has_extended_multizone",
|
|
350
351
|
"has_matrix",
|
|
352
|
+
"has_chain",
|
|
351
353
|
"has_hev",
|
|
352
354
|
"has_relays",
|
|
353
355
|
"has_buttons",
|
|
@@ -256,6 +256,7 @@ class DeviceBuilder:
|
|
|
256
256
|
has_multizone=self._product_info.has_multizone,
|
|
257
257
|
has_extended_multizone=has_extended_multizone,
|
|
258
258
|
has_matrix=self._product_info.has_matrix,
|
|
259
|
+
has_chain=self._product_info.has_chain,
|
|
259
260
|
has_hev=self._product_info.has_hev,
|
|
260
261
|
has_relays=self._product_info.has_relays,
|
|
261
262
|
has_buttons=self._product_info.has_buttons,
|
|
@@ -6,6 +6,8 @@ of the application (domain, API, persistence, etc.).
|
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel, Field, field_validator
|
|
8
8
|
|
|
9
|
+
ACK_PACKET_TYPE = 45
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
class ScenarioConfig(BaseModel):
|
|
11
13
|
"""Scenario configuration for testing LIFX protocol behavior.
|
|
@@ -52,6 +54,16 @@ class ScenarioConfig(BaseModel):
|
|
|
52
54
|
False, description="Send unhandled message responses for unknown packet types"
|
|
53
55
|
)
|
|
54
56
|
|
|
57
|
+
@property
|
|
58
|
+
def affects_acks(self) -> bool:
|
|
59
|
+
"""Whether this scenario configuration modifies acknowledgment behavior."""
|
|
60
|
+
return (
|
|
61
|
+
ACK_PACKET_TYPE in self.drop_packets
|
|
62
|
+
or ACK_PACKET_TYPE in self.response_delays
|
|
63
|
+
or ACK_PACKET_TYPE in self.malformed_packets
|
|
64
|
+
or ACK_PACKET_TYPE in self.invalid_field_values
|
|
65
|
+
)
|
|
66
|
+
|
|
55
67
|
@field_validator("drop_packets", mode="before")
|
|
56
68
|
@classmethod
|
|
57
69
|
def convert_drop_packets_keys(cls, v):
|
lifx_emulator/server.py
CHANGED
|
@@ -18,7 +18,7 @@ from lifx_emulator.devices import (
|
|
|
18
18
|
PacketEvent,
|
|
19
19
|
)
|
|
20
20
|
from lifx_emulator.protocol.header import LifxHeader
|
|
21
|
-
from lifx_emulator.protocol.packets import get_packet_class
|
|
21
|
+
from lifx_emulator.protocol.packets import Device, get_packet_class
|
|
22
22
|
from lifx_emulator.repositories import IScenarioStorageBackend
|
|
23
23
|
from lifx_emulator.scenarios import HierarchicalScenarioManager
|
|
24
24
|
|
|
@@ -179,6 +179,54 @@ class EmulatedLifxServer:
|
|
|
179
179
|
# Fallback for edge case where loop not yet cached
|
|
180
180
|
asyncio.create_task(self.server.handle_packet(data, addr))
|
|
181
181
|
|
|
182
|
+
def _send_ack(
|
|
183
|
+
self,
|
|
184
|
+
device: EmulatedLifxDevice,
|
|
185
|
+
header: LifxHeader,
|
|
186
|
+
addr: tuple[str, int],
|
|
187
|
+
) -> None:
|
|
188
|
+
"""Send an acknowledgment packet immediately via UDP.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
device: The device acknowledging the packet
|
|
192
|
+
header: Request header (source/sequence are copied)
|
|
193
|
+
addr: Client address (host, port)
|
|
194
|
+
"""
|
|
195
|
+
ack_packet = Device.Acknowledgement()
|
|
196
|
+
ack_payload = ack_packet.pack()
|
|
197
|
+
ack_header = device._create_response_header(
|
|
198
|
+
header.source,
|
|
199
|
+
header.sequence,
|
|
200
|
+
ack_packet.PKT_TYPE,
|
|
201
|
+
len(ack_payload),
|
|
202
|
+
)
|
|
203
|
+
response_data = ack_header.pack() + ack_payload
|
|
204
|
+
if self.transport:
|
|
205
|
+
self.transport.sendto(response_data, addr)
|
|
206
|
+
|
|
207
|
+
self.packets_sent += 1
|
|
208
|
+
self.packets_sent_by_type[ack_header.pkt_type] += 1
|
|
209
|
+
|
|
210
|
+
logger.debug(
|
|
211
|
+
"→ TX %s to %s:%s (target=%s, seq=%s) [no fields]",
|
|
212
|
+
_get_packet_type_name(ack_header.pkt_type),
|
|
213
|
+
addr[0],
|
|
214
|
+
addr[1],
|
|
215
|
+
device.state.serial,
|
|
216
|
+
ack_header.sequence,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
self.activity_observer.on_packet_sent(
|
|
220
|
+
PacketEvent(
|
|
221
|
+
timestamp=time.time(),
|
|
222
|
+
direction="tx",
|
|
223
|
+
packet_type=ack_header.pkt_type,
|
|
224
|
+
packet_name=_get_packet_type_name(ack_header.pkt_type),
|
|
225
|
+
addr=f"{addr[0]}:{addr[1]}",
|
|
226
|
+
device=device.state.serial,
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
|
|
182
230
|
async def _process_device_packet(
|
|
183
231
|
self,
|
|
184
232
|
device: EmulatedLifxDevice,
|
|
@@ -194,10 +242,13 @@ class EmulatedLifxServer:
|
|
|
194
242
|
packet: Parsed packet payload (or None)
|
|
195
243
|
addr: Client address (host, port)
|
|
196
244
|
"""
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
# Get resolved scenario for response delays
|
|
245
|
+
# Send ack immediately before device processing when no scenario
|
|
246
|
+
# targets ack behavior (fast path for the common case)
|
|
200
247
|
scenario = device._get_resolved_scenario()
|
|
248
|
+
if header.ack_required and not scenario.affects_acks:
|
|
249
|
+
self._send_ack(device, header, addr)
|
|
250
|
+
|
|
251
|
+
responses = device.process_packet(header, packet)
|
|
201
252
|
|
|
202
253
|
# Send responses with delay if configured
|
|
203
254
|
for resp_header, resp_packet in responses:
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
lifx_emulator/__init__.py,sha256=SSnQg0RiCaID7DhHOGZoVIDQ3_8Lyt0J9cA_2StF63s,824
|
|
2
2
|
lifx_emulator/constants.py,sha256=DFZkUsdewE-x_3MgO28tMGkjUCWPeYc3xLj_EXViGOw,1032
|
|
3
|
-
lifx_emulator/server.py,sha256=
|
|
3
|
+
lifx_emulator/server.py,sha256=x-RDfxwmzUfSSROfBzVm2LcGK68sVIM4o7IkcgzkSfI,18363
|
|
4
4
|
lifx_emulator/devices/__init__.py,sha256=QlBTPnFErJcSKLvGyeDwemh7xcpjYvB_L5siKsjr3s8,1089
|
|
5
|
-
lifx_emulator/devices/device.py,sha256=
|
|
5
|
+
lifx_emulator/devices/device.py,sha256=w7Y9VYux3cyGus9mFmruFn6WYtoD7zwhzCL2xrICY-Q,17180
|
|
6
6
|
lifx_emulator/devices/manager.py,sha256=XDrT82um5sgNpNihLj5RsNvHqdVI1bK9YY2eBzWIcf0,8162
|
|
7
7
|
lifx_emulator/devices/observers.py,sha256=-KnUgFcKdhlNo7CNVstP-u0wU2W0JAGg055ZPV15Sj0,3874
|
|
8
8
|
lifx_emulator/devices/persistence.py,sha256=9Mhj46-xrweOmyzjORCi2jKIwa8XJWpQ5CgaKcw6U98,10513
|
|
9
9
|
lifx_emulator/devices/state_restorer.py,sha256=eDsRSW-2RviP_0Qlk2DHqMaB-zhV0X1cNQECv2lD1qc,9809
|
|
10
10
|
lifx_emulator/devices/state_serializer.py,sha256=aws4LUmXBJS8oBrQziJtlV0XMvCTm5X4dGkGlO_QHcM,6281
|
|
11
|
-
lifx_emulator/devices/states.py,sha256=
|
|
11
|
+
lifx_emulator/devices/states.py,sha256=7UfGoFbgV5TZNGNm4PIthFw9s7-tG1tF_5AfQu5q6d0,12257
|
|
12
12
|
lifx_emulator/factories/__init__.py,sha256=CsryMcf_80hTjOAgrukA6vRZaZow_2VQkSewrpP9gEI,1210
|
|
13
|
-
lifx_emulator/factories/builder.py,sha256=
|
|
13
|
+
lifx_emulator/factories/builder.py,sha256=f70iH1MnO_UrsyHVXgE2BlWCYFZvZ6df6rwvxutby6g,12216
|
|
14
14
|
lifx_emulator/factories/default_config.py,sha256=FTcxKDfeTmO49GTSki8nxnEIZQzR0Lg0hL_PwHUrkVQ,4828
|
|
15
15
|
lifx_emulator/factories/factory.py,sha256=MyGG-pW7EV2BFP5ZzgMuFF5TfNFvfyFDoE5dmd3LC8w,8623
|
|
16
16
|
lifx_emulator/factories/firmware_config.py,sha256=tPN5Hq-uNb1xzW9Q0A9jD-G0-NaGfINcD0i1XZRUMoE,2711
|
|
@@ -40,8 +40,8 @@ lifx_emulator/repositories/device_repository.py,sha256=KsXVg2sg7PGSTsK_PvDYeHHwE
|
|
|
40
40
|
lifx_emulator/repositories/storage_backend.py,sha256=wEgjhnBvAxl6aO1ZGL3ou0dW9P2hBPnK8jEE03sOlL4,3264
|
|
41
41
|
lifx_emulator/scenarios/__init__.py,sha256=CGjudoWvyysvFj2xej11N2cr3mYROGtRb9zVHcOHGrQ,665
|
|
42
42
|
lifx_emulator/scenarios/manager.py,sha256=1esxRdz74UynNk1wb86MGZ2ZFAuMzByuu74nRe3D-Og,11163
|
|
43
|
-
lifx_emulator/scenarios/models.py,sha256=
|
|
43
|
+
lifx_emulator/scenarios/models.py,sha256=1cX399JcTYVo29-8Rc4BwYPRty7sMR4fcn0njGfspZg,4504
|
|
44
44
|
lifx_emulator/scenarios/persistence.py,sha256=3vjtPNFYfag38tUxuqxkGpWhQ7uBitc1rLroSAuw9N8,8881
|
|
45
|
-
lifx_emulator_core-3.
|
|
46
|
-
lifx_emulator_core-3.
|
|
47
|
-
lifx_emulator_core-3.
|
|
45
|
+
lifx_emulator_core-3.4.0.dist-info/METADATA,sha256=FvDJG20jN-BGRJ03z6HG69c7eJ5E27zhV6EuVIB-qsA,3217
|
|
46
|
+
lifx_emulator_core-3.4.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
47
|
+
lifx_emulator_core-3.4.0.dist-info/RECORD,,
|
|
File without changes
|