lifx-emulator 2.4.0__py3-none-any.whl → 3.1.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-3.1.0.dist-info/METADATA +103 -0
- lifx_emulator-3.1.0.dist-info/RECORD +19 -0
- {lifx_emulator-2.4.0.dist-info → lifx_emulator-3.1.0.dist-info}/WHEEL +1 -1
- lifx_emulator-3.1.0.dist-info/entry_points.txt +2 -0
- lifx_emulator_app/__init__.py +10 -0
- {lifx_emulator → lifx_emulator_app}/__main__.py +2 -3
- {lifx_emulator → lifx_emulator_app}/api/__init__.py +1 -1
- {lifx_emulator → lifx_emulator_app}/api/app.py +9 -4
- {lifx_emulator → lifx_emulator_app}/api/mappers/__init__.py +1 -1
- {lifx_emulator → lifx_emulator_app}/api/mappers/device_mapper.py +1 -1
- {lifx_emulator → lifx_emulator_app}/api/models.py +1 -2
- lifx_emulator_app/api/routers/__init__.py +11 -0
- {lifx_emulator → lifx_emulator_app}/api/routers/devices.py +2 -2
- {lifx_emulator → lifx_emulator_app}/api/routers/monitoring.py +1 -1
- {lifx_emulator → lifx_emulator_app}/api/routers/scenarios.py +1 -1
- lifx_emulator_app/api/services/__init__.py +8 -0
- {lifx_emulator → lifx_emulator_app}/api/services/device_service.py +3 -2
- lifx_emulator_app/api/static/dashboard.js +588 -0
- lifx_emulator_app/api/templates/dashboard.html +357 -0
- lifx_emulator/__init__.py +0 -31
- lifx_emulator/api/routers/__init__.py +0 -11
- lifx_emulator/api/services/__init__.py +0 -8
- lifx_emulator/api/templates/dashboard.html +0 -899
- lifx_emulator/constants.py +0 -33
- lifx_emulator/devices/__init__.py +0 -37
- lifx_emulator/devices/device.py +0 -395
- lifx_emulator/devices/manager.py +0 -256
- lifx_emulator/devices/observers.py +0 -139
- lifx_emulator/devices/persistence.py +0 -308
- lifx_emulator/devices/state_restorer.py +0 -259
- lifx_emulator/devices/state_serializer.py +0 -157
- lifx_emulator/devices/states.py +0 -381
- lifx_emulator/factories/__init__.py +0 -39
- lifx_emulator/factories/builder.py +0 -375
- lifx_emulator/factories/default_config.py +0 -158
- lifx_emulator/factories/factory.py +0 -252
- lifx_emulator/factories/firmware_config.py +0 -77
- lifx_emulator/factories/serial_generator.py +0 -82
- lifx_emulator/handlers/__init__.py +0 -39
- lifx_emulator/handlers/base.py +0 -49
- lifx_emulator/handlers/device_handlers.py +0 -322
- lifx_emulator/handlers/light_handlers.py +0 -503
- lifx_emulator/handlers/multizone_handlers.py +0 -249
- lifx_emulator/handlers/registry.py +0 -110
- lifx_emulator/handlers/tile_handlers.py +0 -488
- lifx_emulator/products/__init__.py +0 -28
- lifx_emulator/products/generator.py +0 -1079
- lifx_emulator/products/registry.py +0 -1530
- lifx_emulator/products/specs.py +0 -284
- lifx_emulator/products/specs.yml +0 -386
- lifx_emulator/protocol/__init__.py +0 -1
- lifx_emulator/protocol/base.py +0 -446
- lifx_emulator/protocol/const.py +0 -8
- lifx_emulator/protocol/generator.py +0 -1384
- lifx_emulator/protocol/header.py +0 -159
- lifx_emulator/protocol/packets.py +0 -1351
- lifx_emulator/protocol/protocol_types.py +0 -817
- lifx_emulator/protocol/serializer.py +0 -379
- lifx_emulator/repositories/__init__.py +0 -22
- lifx_emulator/repositories/device_repository.py +0 -155
- lifx_emulator/repositories/storage_backend.py +0 -107
- lifx_emulator/scenarios/__init__.py +0 -22
- lifx_emulator/scenarios/manager.py +0 -322
- lifx_emulator/scenarios/models.py +0 -112
- lifx_emulator/scenarios/persistence.py +0 -241
- lifx_emulator/server.py +0 -464
- lifx_emulator-2.4.0.dist-info/METADATA +0 -107
- lifx_emulator-2.4.0.dist-info/RECORD +0 -62
- lifx_emulator-2.4.0.dist-info/entry_points.txt +0 -2
- lifx_emulator-2.4.0.dist-info/licenses/LICENSE +0 -35
lifx_emulator/constants.py
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
"""Protocol constants for LIFX LAN Protocol"""
|
|
2
|
-
|
|
3
|
-
from typing import Final
|
|
4
|
-
|
|
5
|
-
# ============================================================================
|
|
6
|
-
# Network Constants
|
|
7
|
-
# ============================================================================
|
|
8
|
-
|
|
9
|
-
# LIFX UDP port for device communication
|
|
10
|
-
LIFX_UDP_PORT: Final[int] = 56700
|
|
11
|
-
|
|
12
|
-
# LIFX Protocol version
|
|
13
|
-
LIFX_PROTOCOL_VERSION: Final[int] = 1024
|
|
14
|
-
|
|
15
|
-
# Header size in bytes
|
|
16
|
-
LIFX_HEADER_SIZE: Final[int] = 36
|
|
17
|
-
|
|
18
|
-
# Backward compatibility alias
|
|
19
|
-
HEADER_SIZE = LIFX_HEADER_SIZE
|
|
20
|
-
|
|
21
|
-
# ============================================================================
|
|
22
|
-
# Official LIFX Repository URLs
|
|
23
|
-
# ============================================================================
|
|
24
|
-
|
|
25
|
-
# Official LIFX protocol specification URL
|
|
26
|
-
PROTOCOL_URL: Final[str] = (
|
|
27
|
-
"https://raw.githubusercontent.com/LIFX/public-protocol/refs/heads/main/protocol.yml"
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
# Official LIFX products specification URL
|
|
31
|
-
PRODUCTS_URL: Final[str] = (
|
|
32
|
-
"https://raw.githubusercontent.com/LIFX/products/refs/heads/master/products.json"
|
|
33
|
-
)
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
"""Device management module for LIFX emulator.
|
|
2
|
-
|
|
3
|
-
This module contains all device-related functionality including:
|
|
4
|
-
- Device core (EmulatedLifxDevice)
|
|
5
|
-
- Device manager (DeviceManager, IDeviceManager)
|
|
6
|
-
- Device states (DeviceState and related dataclasses)
|
|
7
|
-
- Device persistence (async file storage)
|
|
8
|
-
- State restoration and serialization
|
|
9
|
-
- Device state observers (ActivityObserver, ActivityLogger, PacketEvent, NullObserver)
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
from lifx_emulator.devices.device import EmulatedLifxDevice
|
|
13
|
-
from lifx_emulator.devices.manager import DeviceManager, IDeviceManager
|
|
14
|
-
from lifx_emulator.devices.observers import (
|
|
15
|
-
ActivityLogger,
|
|
16
|
-
ActivityObserver,
|
|
17
|
-
NullObserver,
|
|
18
|
-
PacketEvent,
|
|
19
|
-
)
|
|
20
|
-
from lifx_emulator.devices.persistence import (
|
|
21
|
-
DEFAULT_STORAGE_DIR,
|
|
22
|
-
DevicePersistenceAsyncFile,
|
|
23
|
-
)
|
|
24
|
-
from lifx_emulator.devices.states import DeviceState
|
|
25
|
-
|
|
26
|
-
__all__ = [
|
|
27
|
-
"EmulatedLifxDevice",
|
|
28
|
-
"DeviceManager",
|
|
29
|
-
"IDeviceManager",
|
|
30
|
-
"DeviceState",
|
|
31
|
-
"DevicePersistenceAsyncFile",
|
|
32
|
-
"DEFAULT_STORAGE_DIR",
|
|
33
|
-
"ActivityObserver",
|
|
34
|
-
"ActivityLogger",
|
|
35
|
-
"PacketEvent",
|
|
36
|
-
"NullObserver",
|
|
37
|
-
]
|
lifx_emulator/devices/device.py
DELETED
|
@@ -1,395 +0,0 @@
|
|
|
1
|
-
"""Device state and emulated device implementation."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import asyncio
|
|
6
|
-
import copy
|
|
7
|
-
import logging
|
|
8
|
-
import time
|
|
9
|
-
from typing import Any
|
|
10
|
-
|
|
11
|
-
from lifx_emulator.constants import LIFX_HEADER_SIZE
|
|
12
|
-
from lifx_emulator.devices.states import DeviceState, TileFramebuffers
|
|
13
|
-
from lifx_emulator.handlers import HandlerRegistry, create_default_registry
|
|
14
|
-
from lifx_emulator.protocol.header import LifxHeader
|
|
15
|
-
from lifx_emulator.protocol.packets import (
|
|
16
|
-
Device,
|
|
17
|
-
)
|
|
18
|
-
from lifx_emulator.protocol.protocol_types import LightHsbk
|
|
19
|
-
from lifx_emulator.scenarios import (
|
|
20
|
-
HierarchicalScenarioManager,
|
|
21
|
-
ScenarioConfig,
|
|
22
|
-
get_device_type,
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
logger = logging.getLogger(__name__)
|
|
26
|
-
|
|
27
|
-
# Forward declaration for type hinting
|
|
28
|
-
TYPE_CHECKING = False
|
|
29
|
-
if TYPE_CHECKING:
|
|
30
|
-
from lifx_emulator.devices.persistence import DevicePersistenceAsyncFile
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class EmulatedLifxDevice:
|
|
34
|
-
"""Emulated LIFX device with configurable scenarios and state management."""
|
|
35
|
-
|
|
36
|
-
def __init__(
|
|
37
|
-
self,
|
|
38
|
-
device_state: DeviceState,
|
|
39
|
-
storage: DevicePersistenceAsyncFile | None = None,
|
|
40
|
-
handler_registry: HandlerRegistry | None = None,
|
|
41
|
-
scenario_manager: HierarchicalScenarioManager | None = None,
|
|
42
|
-
):
|
|
43
|
-
self.state = device_state
|
|
44
|
-
# Use provided scenario manager or create a default empty one
|
|
45
|
-
if scenario_manager is not None:
|
|
46
|
-
self.scenario_manager = scenario_manager
|
|
47
|
-
else:
|
|
48
|
-
self.scenario_manager = HierarchicalScenarioManager()
|
|
49
|
-
self.start_time = time.time()
|
|
50
|
-
self.storage = storage
|
|
51
|
-
|
|
52
|
-
# Scenario caching for performance (HierarchicalScenarioManager only)
|
|
53
|
-
self._cached_scenario: ScenarioConfig | None = None
|
|
54
|
-
|
|
55
|
-
# Track background save tasks to prevent garbage collection
|
|
56
|
-
self.background_save_tasks: set[asyncio.Task] = set()
|
|
57
|
-
|
|
58
|
-
# Use provided registry or create default one
|
|
59
|
-
self.handlers = handler_registry or create_default_registry()
|
|
60
|
-
|
|
61
|
-
# Pre-allocate response header template for performance (10-15% gain)
|
|
62
|
-
# This avoids creating a new LifxHeader object for every response
|
|
63
|
-
self._response_header_template = LifxHeader(
|
|
64
|
-
source=0,
|
|
65
|
-
target=self.state.get_target_bytes(),
|
|
66
|
-
sequence=0,
|
|
67
|
-
tagged=False,
|
|
68
|
-
pkt_type=0,
|
|
69
|
-
size=0,
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
# Initialize multizone colors if needed
|
|
73
|
-
# Note: State restoration is handled by StateRestorer in factories
|
|
74
|
-
if self.state.has_multizone and self.state.zone_count > 0:
|
|
75
|
-
if not self.state.zone_colors:
|
|
76
|
-
# Initialize with rainbow pattern using list comprehension
|
|
77
|
-
# (performance optimization)
|
|
78
|
-
self.state.zone_colors = [
|
|
79
|
-
LightHsbk(
|
|
80
|
-
hue=int((i / self.state.zone_count) * 65535),
|
|
81
|
-
saturation=65535,
|
|
82
|
-
brightness=32768,
|
|
83
|
-
kelvin=3500,
|
|
84
|
-
)
|
|
85
|
-
for i in range(self.state.zone_count)
|
|
86
|
-
]
|
|
87
|
-
|
|
88
|
-
# Initialize tile state if needed
|
|
89
|
-
# Note: Saved tile data is restored by StateRestorer in factories
|
|
90
|
-
if self.state.has_matrix and self.state.tile_count > 0:
|
|
91
|
-
if not self.state.tile_devices:
|
|
92
|
-
for i in range(self.state.tile_count):
|
|
93
|
-
zones = self.state.tile_width * self.state.tile_height
|
|
94
|
-
tile_colors = [
|
|
95
|
-
LightHsbk(hue=0, saturation=0, brightness=32768, kelvin=3500)
|
|
96
|
-
for _ in range(zones)
|
|
97
|
-
]
|
|
98
|
-
|
|
99
|
-
self.state.tile_devices.append(
|
|
100
|
-
{
|
|
101
|
-
"accel_meas_x": 0,
|
|
102
|
-
"accel_meas_y": 0,
|
|
103
|
-
"accel_meas_z": 0,
|
|
104
|
-
"user_x": float(i * self.state.tile_width),
|
|
105
|
-
"user_y": 0.0,
|
|
106
|
-
"width": self.state.tile_width,
|
|
107
|
-
"height": self.state.tile_height,
|
|
108
|
-
"device_version_vendor": 1,
|
|
109
|
-
"device_version_product": self.state.product,
|
|
110
|
-
"firmware_build": int(time.time()),
|
|
111
|
-
"firmware_version_minor": 70,
|
|
112
|
-
"firmware_version_major": 3,
|
|
113
|
-
"colors": tile_colors,
|
|
114
|
-
}
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
# Initialize framebuffer storage for each tile (framebuffers 1-7)
|
|
118
|
-
# Framebuffer 0 is stored in tile_devices[i]["colors"]
|
|
119
|
-
if not self.state.tile_framebuffers:
|
|
120
|
-
for i in range(self.state.tile_count):
|
|
121
|
-
self.state.tile_framebuffers.append(TileFramebuffers(tile_index=i))
|
|
122
|
-
|
|
123
|
-
# Save initial state if persistence is enabled
|
|
124
|
-
# This ensures newly created devices are immediately persisted
|
|
125
|
-
if self.storage:
|
|
126
|
-
self._save_state()
|
|
127
|
-
|
|
128
|
-
def get_uptime_ns(self) -> int:
|
|
129
|
-
"""Calculate current uptime in nanoseconds"""
|
|
130
|
-
return int((time.time() - self.start_time) * 1e9)
|
|
131
|
-
|
|
132
|
-
def _save_state(self) -> None:
|
|
133
|
-
"""Save device state asynchronously (non-blocking).
|
|
134
|
-
|
|
135
|
-
Creates a background task to save state without blocking the event loop.
|
|
136
|
-
The task is tracked to prevent garbage collection.
|
|
137
|
-
|
|
138
|
-
Note: Only DevicePersistenceAsyncFile is supported in production. For testing,
|
|
139
|
-
you can pass None to disable persistence.
|
|
140
|
-
"""
|
|
141
|
-
if not self.storage:
|
|
142
|
-
return
|
|
143
|
-
|
|
144
|
-
try:
|
|
145
|
-
loop = asyncio.get_running_loop()
|
|
146
|
-
task = loop.create_task(self.storage.save_device_state(self.state))
|
|
147
|
-
self._track_save_task(task)
|
|
148
|
-
except RuntimeError:
|
|
149
|
-
# No event loop (shouldn't happen in normal operation)
|
|
150
|
-
logger.error("Cannot save state for %s: no event loop", self.state.serial)
|
|
151
|
-
|
|
152
|
-
def _track_save_task(self, task: asyncio.Task) -> None:
|
|
153
|
-
"""Track background save task to prevent garbage collection.
|
|
154
|
-
|
|
155
|
-
Args:
|
|
156
|
-
task: Save task to track
|
|
157
|
-
"""
|
|
158
|
-
self.background_save_tasks.add(task)
|
|
159
|
-
task.add_done_callback(self.background_save_tasks.discard)
|
|
160
|
-
|
|
161
|
-
def _get_resolved_scenario(self) -> ScenarioConfig:
|
|
162
|
-
"""Get resolved scenario configuration with caching.
|
|
163
|
-
|
|
164
|
-
Resolves scenario from all applicable scopes and caches the result
|
|
165
|
-
for performance.
|
|
166
|
-
|
|
167
|
-
Returns:
|
|
168
|
-
ScenarioConfig with resolved settings
|
|
169
|
-
"""
|
|
170
|
-
if self._cached_scenario is not None:
|
|
171
|
-
return self._cached_scenario
|
|
172
|
-
|
|
173
|
-
# Resolve scenario with hierarchical scoping
|
|
174
|
-
self._cached_scenario = self.scenario_manager.get_scenario_for_device(
|
|
175
|
-
serial=self.state.serial,
|
|
176
|
-
device_type=get_device_type(self),
|
|
177
|
-
location=self.state.location_label,
|
|
178
|
-
group=self.state.group_label,
|
|
179
|
-
)
|
|
180
|
-
return self._cached_scenario
|
|
181
|
-
|
|
182
|
-
def invalidate_scenario_cache(self) -> None:
|
|
183
|
-
"""Invalidate cached scenario configuration.
|
|
184
|
-
|
|
185
|
-
Call this when scenarios are updated at runtime to force
|
|
186
|
-
recalculation on the next packet.
|
|
187
|
-
"""
|
|
188
|
-
self._cached_scenario = None
|
|
189
|
-
|
|
190
|
-
def _create_response_header(
|
|
191
|
-
self, source: int, sequence: int, pkt_type: int, payload_size: int
|
|
192
|
-
) -> LifxHeader:
|
|
193
|
-
"""Create response header using pre-allocated template (performance).
|
|
194
|
-
|
|
195
|
-
This method uses a pre-allocated template and creates a shallow copy,
|
|
196
|
-
then updates the fields. This avoids full __init__ and __post_init__
|
|
197
|
-
overhead while ensuring each response gets its own header object,
|
|
198
|
-
providing ~10% improvement in response generation.
|
|
199
|
-
|
|
200
|
-
Args:
|
|
201
|
-
source: Source identifier from request
|
|
202
|
-
sequence: Sequence number from request
|
|
203
|
-
pkt_type: Packet type for response
|
|
204
|
-
payload_size: Size of packed payload in bytes
|
|
205
|
-
|
|
206
|
-
Returns:
|
|
207
|
-
Configured LifxHeader ready to use
|
|
208
|
-
"""
|
|
209
|
-
# Shallow copy of template is faster than full construction with validation
|
|
210
|
-
header = copy.copy(self._response_header_template)
|
|
211
|
-
# Update fields for this specific response
|
|
212
|
-
header.source = source
|
|
213
|
-
header.sequence = sequence
|
|
214
|
-
header.pkt_type = pkt_type
|
|
215
|
-
header.size = LIFX_HEADER_SIZE + payload_size
|
|
216
|
-
return header
|
|
217
|
-
|
|
218
|
-
def _should_handle_packet(self, pkt_type: int) -> bool:
|
|
219
|
-
"""Check if device should handle a packet type based on capabilities.
|
|
220
|
-
|
|
221
|
-
Args:
|
|
222
|
-
pkt_type: Packet type number
|
|
223
|
-
|
|
224
|
-
Returns:
|
|
225
|
-
True if device should handle, False if should return StateUnhandled
|
|
226
|
-
"""
|
|
227
|
-
# Device.* packets are always handled (2-59)
|
|
228
|
-
if 2 <= pkt_type <= 59:
|
|
229
|
-
return True
|
|
230
|
-
|
|
231
|
-
# Light.* packets (101-149) require light capabilities
|
|
232
|
-
# Switches (devices with relays) don't support light operations
|
|
233
|
-
if 101 <= pkt_type <= 149:
|
|
234
|
-
return not self.state.has_relays
|
|
235
|
-
|
|
236
|
-
# MultiZone.* packets (501-512) require multizone capability
|
|
237
|
-
if 501 <= pkt_type <= 512:
|
|
238
|
-
return self.state.has_multizone
|
|
239
|
-
|
|
240
|
-
# Tile.* packets (701-720) require matrix capability
|
|
241
|
-
if 701 <= pkt_type <= 720:
|
|
242
|
-
return self.state.has_matrix
|
|
243
|
-
|
|
244
|
-
# Unknown packets - let handler decide
|
|
245
|
-
return True
|
|
246
|
-
|
|
247
|
-
def process_packet(
|
|
248
|
-
self, header: LifxHeader, packet: Any | None
|
|
249
|
-
) -> list[tuple[LifxHeader, Any]]:
|
|
250
|
-
"""Process incoming packet and return response packets"""
|
|
251
|
-
responses = []
|
|
252
|
-
|
|
253
|
-
# Get resolved scenario configuration (cached for performance)
|
|
254
|
-
scenario = self._get_resolved_scenario()
|
|
255
|
-
|
|
256
|
-
# Check if packet should be dropped (with probabilistic drops)
|
|
257
|
-
if not self.scenario_manager.should_respond(header.pkt_type, scenario):
|
|
258
|
-
logger.info("Dropping packet type %s per scenario", header.pkt_type)
|
|
259
|
-
return responses
|
|
260
|
-
|
|
261
|
-
# Check if device should handle this packet type (capability-based filtering)
|
|
262
|
-
if not self._should_handle_packet(header.pkt_type):
|
|
263
|
-
# Return StateUnhandled for unsupported packet types
|
|
264
|
-
state_unhandled = Device.StateUnhandled(unhandled_type=header.pkt_type)
|
|
265
|
-
unhandled_payload = state_unhandled.pack()
|
|
266
|
-
unhandled_header = self._create_response_header(
|
|
267
|
-
header.source,
|
|
268
|
-
header.sequence,
|
|
269
|
-
state_unhandled.PKT_TYPE,
|
|
270
|
-
len(unhandled_payload),
|
|
271
|
-
)
|
|
272
|
-
responses.append((unhandled_header, state_unhandled))
|
|
273
|
-
|
|
274
|
-
# Still send acknowledgment if requested
|
|
275
|
-
if header.ack_required:
|
|
276
|
-
ack_packet = Device.Acknowledgement()
|
|
277
|
-
ack_payload = ack_packet.pack()
|
|
278
|
-
ack_header = self._create_response_header(
|
|
279
|
-
header.source,
|
|
280
|
-
header.sequence,
|
|
281
|
-
ack_packet.PKT_TYPE,
|
|
282
|
-
len(ack_payload),
|
|
283
|
-
)
|
|
284
|
-
responses.append((ack_header, ack_packet))
|
|
285
|
-
|
|
286
|
-
return responses
|
|
287
|
-
|
|
288
|
-
# Update uptime
|
|
289
|
-
self.state.uptime_ns = self.get_uptime_ns()
|
|
290
|
-
|
|
291
|
-
# Handle acknowledgment (packet type 45, no payload)
|
|
292
|
-
if header.ack_required:
|
|
293
|
-
ack_packet = Device.Acknowledgement()
|
|
294
|
-
ack_payload = ack_packet.pack()
|
|
295
|
-
ack_header = self._create_response_header(
|
|
296
|
-
header.source,
|
|
297
|
-
header.sequence,
|
|
298
|
-
ack_packet.PKT_TYPE,
|
|
299
|
-
len(ack_payload),
|
|
300
|
-
)
|
|
301
|
-
# Store header, packet, and pre-packed payload
|
|
302
|
-
# (consistent with response format)
|
|
303
|
-
responses.append((ack_header, ack_packet, ack_payload))
|
|
304
|
-
|
|
305
|
-
# Handle specific packet types - handlers always return list
|
|
306
|
-
response_packets = self._handle_packet_type(header, packet)
|
|
307
|
-
# Handlers now always return list (empty if no response)
|
|
308
|
-
for resp_packet in response_packets:
|
|
309
|
-
# Cache packed payload to avoid double packing (performance optimization)
|
|
310
|
-
resp_payload = resp_packet.pack()
|
|
311
|
-
resp_header = self._create_response_header(
|
|
312
|
-
header.source,
|
|
313
|
-
header.sequence,
|
|
314
|
-
resp_packet.PKT_TYPE,
|
|
315
|
-
len(resp_payload),
|
|
316
|
-
)
|
|
317
|
-
# Store both header and pre-packed payload for error scenario processing
|
|
318
|
-
responses.append((resp_header, resp_packet, resp_payload))
|
|
319
|
-
|
|
320
|
-
# Apply error scenarios to responses
|
|
321
|
-
modified_responses = []
|
|
322
|
-
for resp_header, resp_packet, resp_payload in responses:
|
|
323
|
-
# Check if we should send malformed packet (truncate payload)
|
|
324
|
-
if resp_header.pkt_type in scenario.malformed_packets:
|
|
325
|
-
# For malformed packets, truncate the pre-packed payload
|
|
326
|
-
truncated_len = len(resp_payload) // 2
|
|
327
|
-
resp_payload_modified = resp_payload[:truncated_len]
|
|
328
|
-
resp_header.size = LIFX_HEADER_SIZE + truncated_len + 10 # Wrong size
|
|
329
|
-
# Convert back to bytes for malformed case
|
|
330
|
-
modified_responses.append((resp_header, resp_payload_modified))
|
|
331
|
-
logger.info(
|
|
332
|
-
"Sending malformed packet type %s (truncated)", resp_header.pkt_type
|
|
333
|
-
)
|
|
334
|
-
continue
|
|
335
|
-
|
|
336
|
-
# Check if we should send invalid field values
|
|
337
|
-
if resp_header.pkt_type in scenario.invalid_field_values:
|
|
338
|
-
# Corrupt the pre-packed payload
|
|
339
|
-
resp_payload_modified = b"\xff" * len(resp_payload)
|
|
340
|
-
modified_responses.append((resp_header, resp_payload_modified))
|
|
341
|
-
pkt_type = resp_header.pkt_type
|
|
342
|
-
logger.info("Sending invalid field values for packet type %s", pkt_type)
|
|
343
|
-
continue
|
|
344
|
-
|
|
345
|
-
# Normal case: use original packet object (will be packed later by server)
|
|
346
|
-
modified_responses.append((resp_header, resp_packet))
|
|
347
|
-
|
|
348
|
-
return modified_responses
|
|
349
|
-
|
|
350
|
-
def _handle_packet_type(self, header: LifxHeader, packet: Any | None) -> list[Any]:
|
|
351
|
-
"""Handle specific packet types using registered handlers.
|
|
352
|
-
|
|
353
|
-
Returns:
|
|
354
|
-
List of response packets (empty list if no response)
|
|
355
|
-
"""
|
|
356
|
-
pkt_type = header.pkt_type
|
|
357
|
-
|
|
358
|
-
# Update uptime for this packet
|
|
359
|
-
self.state.uptime_ns = self.get_uptime_ns()
|
|
360
|
-
|
|
361
|
-
# Find handler for this packet type
|
|
362
|
-
handler = self.handlers.get_handler(pkt_type)
|
|
363
|
-
|
|
364
|
-
if handler:
|
|
365
|
-
# Delegate to handler (always returns list now)
|
|
366
|
-
response = handler.handle(self.state, packet, header.res_required)
|
|
367
|
-
|
|
368
|
-
# Save state if storage is enabled (for SET operations)
|
|
369
|
-
if packet and self.storage:
|
|
370
|
-
self._save_state()
|
|
371
|
-
|
|
372
|
-
return response
|
|
373
|
-
else:
|
|
374
|
-
# Unknown/unimplemented packet type
|
|
375
|
-
from lifx_emulator.protocol.packets import get_packet_class
|
|
376
|
-
|
|
377
|
-
packet_class = get_packet_class(pkt_type)
|
|
378
|
-
if packet_class:
|
|
379
|
-
logger.info(
|
|
380
|
-
"Device %s: Received %s (type %s) but no handler registered",
|
|
381
|
-
self.state.serial,
|
|
382
|
-
packet_class.__qualname__,
|
|
383
|
-
pkt_type,
|
|
384
|
-
)
|
|
385
|
-
else:
|
|
386
|
-
serial = self.state.serial
|
|
387
|
-
logger.warning(
|
|
388
|
-
"Device %s: Received unknown packet type %s", serial, pkt_type
|
|
389
|
-
)
|
|
390
|
-
|
|
391
|
-
# Check scenario for StateUnhandled response
|
|
392
|
-
scenario = self._get_resolved_scenario()
|
|
393
|
-
if scenario.send_unhandled:
|
|
394
|
-
return [Device.StateUnhandled(unhandled_type=pkt_type)]
|
|
395
|
-
return []
|