lifx-emulator 2.3.1__py3-none-any.whl → 3.0.1__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.0.1.dist-info/METADATA +102 -0
- lifx_emulator-3.0.1.dist-info/RECORD +18 -0
- lifx_emulator-3.0.1.dist-info/entry_points.txt +2 -0
- lifx_emulator_app/__init__.py +10 -0
- {lifx_emulator → lifx_emulator_app}/__main__.py +13 -5
- {lifx_emulator → lifx_emulator_app}/api/__init__.py +1 -1
- {lifx_emulator → lifx_emulator_app}/api/app.py +3 -3
- {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/__init__.py +0 -31
- lifx_emulator/api/routers/__init__.py +0 -11
- lifx_emulator/api/services/__init__.py +0 -8
- lifx_emulator/constants.py +0 -33
- lifx_emulator/devices/__init__.py +0 -37
- lifx_emulator/devices/device.py +0 -339
- 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 -377
- lifx_emulator/factories/__init__.py +0 -37
- lifx_emulator/factories/builder.py +0 -373
- lifx_emulator/factories/default_config.py +0 -158
- lifx_emulator/factories/factory.py +0 -221
- 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 -1037
- lifx_emulator/products/registry.py +0 -1496
- lifx_emulator/products/specs.py +0 -284
- lifx_emulator/products/specs.yml +0 -352
- 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.3.1.dist-info/METADATA +0 -107
- lifx_emulator-2.3.1.dist-info/RECORD +0 -62
- lifx_emulator-2.3.1.dist-info/entry_points.txt +0 -2
- lifx_emulator-2.3.1.dist-info/licenses/LICENSE +0 -35
- {lifx_emulator-2.3.1.dist-info → lifx_emulator-3.0.1.dist-info}/WHEEL +0 -0
- {lifx_emulator → lifx_emulator_app}/api/templates/dashboard.html +0 -0
lifx_emulator/server.py
DELETED
|
@@ -1,464 +0,0 @@
|
|
|
1
|
-
"""UDP server that emulates LIFX devices."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import asyncio
|
|
6
|
-
import logging
|
|
7
|
-
import time
|
|
8
|
-
from collections import defaultdict
|
|
9
|
-
from typing import Any
|
|
10
|
-
|
|
11
|
-
from lifx_emulator.constants import LIFX_HEADER_SIZE, LIFX_UDP_PORT
|
|
12
|
-
from lifx_emulator.devices import (
|
|
13
|
-
ActivityLogger,
|
|
14
|
-
ActivityObserver,
|
|
15
|
-
EmulatedLifxDevice,
|
|
16
|
-
IDeviceManager,
|
|
17
|
-
NullObserver,
|
|
18
|
-
PacketEvent,
|
|
19
|
-
)
|
|
20
|
-
from lifx_emulator.protocol.header import LifxHeader
|
|
21
|
-
from lifx_emulator.protocol.packets import get_packet_class
|
|
22
|
-
from lifx_emulator.repositories import IScenarioStorageBackend
|
|
23
|
-
from lifx_emulator.scenarios import HierarchicalScenarioManager
|
|
24
|
-
|
|
25
|
-
logger = logging.getLogger(__name__)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def _get_packet_type_name(pkt_type: int) -> str:
|
|
29
|
-
"""Get human-readable name for packet type.
|
|
30
|
-
|
|
31
|
-
Args:
|
|
32
|
-
pkt_type: Packet type number
|
|
33
|
-
|
|
34
|
-
Returns:
|
|
35
|
-
Packet class name or "Unknown"
|
|
36
|
-
"""
|
|
37
|
-
packet_class = get_packet_class(pkt_type)
|
|
38
|
-
if packet_class:
|
|
39
|
-
return packet_class.__qualname__ # e.g., "Device.GetService"
|
|
40
|
-
return f"Unknown({pkt_type})"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def _format_packet_fields(packet: Any) -> str:
|
|
44
|
-
"""Format packet fields for logging, excluding reserved fields.
|
|
45
|
-
|
|
46
|
-
Args:
|
|
47
|
-
packet: Packet instance to format
|
|
48
|
-
|
|
49
|
-
Returns:
|
|
50
|
-
Formatted string with field names and values
|
|
51
|
-
"""
|
|
52
|
-
if packet is None:
|
|
53
|
-
return "no payload"
|
|
54
|
-
|
|
55
|
-
fields = []
|
|
56
|
-
for field_item in packet._fields:
|
|
57
|
-
# Skip reserved fields (no name)
|
|
58
|
-
if "name" not in field_item:
|
|
59
|
-
continue
|
|
60
|
-
|
|
61
|
-
# Get field value
|
|
62
|
-
field_name = packet._protocol_to_python_name(field_item["name"])
|
|
63
|
-
value = getattr(packet, field_name, None)
|
|
64
|
-
|
|
65
|
-
# Format value based on type
|
|
66
|
-
if isinstance(value, bytes):
|
|
67
|
-
# For bytes, show hex if short, or length if long
|
|
68
|
-
if len(value) <= 8:
|
|
69
|
-
value_str = value.hex()
|
|
70
|
-
else:
|
|
71
|
-
value_str = f"<{len(value)} bytes>"
|
|
72
|
-
elif isinstance(value, list):
|
|
73
|
-
# For lists, show count and sample
|
|
74
|
-
if len(value) <= 3:
|
|
75
|
-
value_str = str(value)
|
|
76
|
-
else:
|
|
77
|
-
value_str = f"[{len(value)} items]"
|
|
78
|
-
elif hasattr(value, "__dict__"):
|
|
79
|
-
# For nested objects, show their string representation
|
|
80
|
-
value_str = str(value)
|
|
81
|
-
else:
|
|
82
|
-
value_str = str(value)
|
|
83
|
-
|
|
84
|
-
fields.append(f"{field_name}={value_str}")
|
|
85
|
-
|
|
86
|
-
return ", ".join(fields) if fields else "no fields"
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
class EmulatedLifxServer:
|
|
90
|
-
"""UDP server that simulates LIFX devices"""
|
|
91
|
-
|
|
92
|
-
def __init__(
|
|
93
|
-
self,
|
|
94
|
-
devices: list[EmulatedLifxDevice],
|
|
95
|
-
device_manager: IDeviceManager,
|
|
96
|
-
bind_address: str = "127.0.0.1",
|
|
97
|
-
port: int = LIFX_UDP_PORT,
|
|
98
|
-
track_activity: bool = True,
|
|
99
|
-
storage=None,
|
|
100
|
-
activity_observer: ActivityObserver | None = None,
|
|
101
|
-
scenario_manager: HierarchicalScenarioManager | None = None,
|
|
102
|
-
persist_scenarios: bool = False,
|
|
103
|
-
scenario_storage: IScenarioStorageBackend | None = None,
|
|
104
|
-
):
|
|
105
|
-
# Device manager (required dependency injection)
|
|
106
|
-
self._device_manager = device_manager
|
|
107
|
-
self.bind_address = bind_address
|
|
108
|
-
self.port = port
|
|
109
|
-
self.transport = None
|
|
110
|
-
self.storage = storage
|
|
111
|
-
|
|
112
|
-
# Scenario storage backend (optional - only needed for persistence)
|
|
113
|
-
self.scenario_persistence: IScenarioStorageBackend | None = None
|
|
114
|
-
if persist_scenarios:
|
|
115
|
-
if scenario_storage is None:
|
|
116
|
-
raise ValueError(
|
|
117
|
-
"scenario_storage is required when persist_scenarios=True"
|
|
118
|
-
)
|
|
119
|
-
if scenario_manager is None:
|
|
120
|
-
raise ValueError(
|
|
121
|
-
"scenario_manager is required when persist_scenarios=True "
|
|
122
|
-
"(must be pre-loaded from storage before server initialization)"
|
|
123
|
-
)
|
|
124
|
-
self.scenario_persistence = scenario_storage
|
|
125
|
-
|
|
126
|
-
# Scenario manager (shared across all devices for runtime updates)
|
|
127
|
-
self.scenario_manager = scenario_manager or HierarchicalScenarioManager()
|
|
128
|
-
|
|
129
|
-
# Add initial devices to the device manager
|
|
130
|
-
for device in devices:
|
|
131
|
-
self._device_manager.add_device(device, self.scenario_manager)
|
|
132
|
-
|
|
133
|
-
# Activity observer - defaults to ActivityLogger if track_activity=True
|
|
134
|
-
if activity_observer is not None:
|
|
135
|
-
self.activity_observer = activity_observer
|
|
136
|
-
elif track_activity:
|
|
137
|
-
self.activity_observer = ActivityLogger(max_events=100)
|
|
138
|
-
else:
|
|
139
|
-
self.activity_observer = NullObserver()
|
|
140
|
-
|
|
141
|
-
# Statistics tracking
|
|
142
|
-
self.start_time = time.time()
|
|
143
|
-
self.packets_received = 0
|
|
144
|
-
self.packets_sent = 0
|
|
145
|
-
self.packets_received_by_type: dict[int, int] = defaultdict(int)
|
|
146
|
-
self.packets_sent_by_type: dict[int, int] = defaultdict(int)
|
|
147
|
-
self.error_count = 0
|
|
148
|
-
|
|
149
|
-
class LifxProtocol(asyncio.DatagramProtocol):
|
|
150
|
-
def __init__(self, server):
|
|
151
|
-
self.server = server
|
|
152
|
-
self.loop = None
|
|
153
|
-
|
|
154
|
-
def connection_made(self, transport):
|
|
155
|
-
self.transport = transport
|
|
156
|
-
self.server.transport = transport
|
|
157
|
-
# Cache event loop reference for optimized task scheduling
|
|
158
|
-
try:
|
|
159
|
-
self.loop = asyncio.get_running_loop()
|
|
160
|
-
except RuntimeError:
|
|
161
|
-
# No running loop yet (happens in tests or edge cases)
|
|
162
|
-
self.loop = None
|
|
163
|
-
logger.info(
|
|
164
|
-
"LIFX emulated server listening on %s:%s",
|
|
165
|
-
self.server.bind_address,
|
|
166
|
-
self.server.port,
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
def datagram_received(self, data, addr):
|
|
170
|
-
# Use direct loop scheduling for lower task creation overhead
|
|
171
|
-
# This is faster than asyncio.create_task() for high-frequency packets
|
|
172
|
-
if self.loop:
|
|
173
|
-
self.loop.call_soon(
|
|
174
|
-
self.loop.create_task, self.server.handle_packet(data, addr)
|
|
175
|
-
)
|
|
176
|
-
else:
|
|
177
|
-
# Fallback for edge case where loop not yet cached
|
|
178
|
-
asyncio.create_task(self.server.handle_packet(data, addr))
|
|
179
|
-
|
|
180
|
-
async def _process_device_packet(
|
|
181
|
-
self,
|
|
182
|
-
device: EmulatedLifxDevice,
|
|
183
|
-
header: LifxHeader,
|
|
184
|
-
packet: Any | None,
|
|
185
|
-
addr: tuple[str, int],
|
|
186
|
-
):
|
|
187
|
-
"""Process packet for a single device and send responses.
|
|
188
|
-
|
|
189
|
-
Args:
|
|
190
|
-
device: The device to process the packet
|
|
191
|
-
header: Parsed LIFX header
|
|
192
|
-
packet: Parsed packet payload (or None)
|
|
193
|
-
addr: Client address (host, port)
|
|
194
|
-
"""
|
|
195
|
-
responses = device.process_packet(header, packet)
|
|
196
|
-
|
|
197
|
-
# Get resolved scenario for response delays
|
|
198
|
-
scenario = device._get_resolved_scenario()
|
|
199
|
-
|
|
200
|
-
# Send responses with delay if configured
|
|
201
|
-
for resp_header, resp_packet in responses:
|
|
202
|
-
delay = scenario.response_delays.get(resp_header.pkt_type, 0.0)
|
|
203
|
-
if delay > 0:
|
|
204
|
-
await asyncio.sleep(delay)
|
|
205
|
-
|
|
206
|
-
# Pack the response packet
|
|
207
|
-
resp_payload = resp_packet.pack() if resp_packet else b""
|
|
208
|
-
response_data = resp_header.pack() + resp_payload
|
|
209
|
-
if self.transport:
|
|
210
|
-
self.transport.sendto(response_data, addr)
|
|
211
|
-
|
|
212
|
-
# Update statistics
|
|
213
|
-
self.packets_sent += 1
|
|
214
|
-
self.packets_sent_by_type[resp_header.pkt_type] += 1
|
|
215
|
-
|
|
216
|
-
# Log sent packet with details
|
|
217
|
-
resp_packet_name = _get_packet_type_name(resp_header.pkt_type)
|
|
218
|
-
resp_fields_str = _format_packet_fields(resp_packet)
|
|
219
|
-
logger.debug(
|
|
220
|
-
"→ TX %s to %s:%s (target=%s, seq=%s) [%s]",
|
|
221
|
-
resp_packet_name,
|
|
222
|
-
addr[0],
|
|
223
|
-
addr[1],
|
|
224
|
-
device.state.serial,
|
|
225
|
-
resp_header.sequence,
|
|
226
|
-
resp_fields_str,
|
|
227
|
-
)
|
|
228
|
-
|
|
229
|
-
# Notify observer
|
|
230
|
-
self.activity_observer.on_packet_sent(
|
|
231
|
-
PacketEvent(
|
|
232
|
-
timestamp=time.time(),
|
|
233
|
-
direction="tx",
|
|
234
|
-
packet_type=resp_header.pkt_type,
|
|
235
|
-
packet_name=resp_packet_name,
|
|
236
|
-
addr=f"{addr[0]}:{addr[1]}",
|
|
237
|
-
device=device.state.serial,
|
|
238
|
-
)
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
async def handle_packet(self, data: bytes, addr: tuple[str, int]):
|
|
242
|
-
"""Handle incoming UDP packet"""
|
|
243
|
-
try:
|
|
244
|
-
# Update statistics
|
|
245
|
-
self.packets_received += 1
|
|
246
|
-
|
|
247
|
-
if len(data) < LIFX_HEADER_SIZE:
|
|
248
|
-
logger.warning("Packet too short: %s bytes from %s", len(data), addr)
|
|
249
|
-
self.error_count += 1
|
|
250
|
-
return
|
|
251
|
-
|
|
252
|
-
# Parse header
|
|
253
|
-
header = LifxHeader.unpack(data)
|
|
254
|
-
payload = (
|
|
255
|
-
data[LIFX_HEADER_SIZE : header.size]
|
|
256
|
-
if header.size > LIFX_HEADER_SIZE
|
|
257
|
-
else b""
|
|
258
|
-
)
|
|
259
|
-
|
|
260
|
-
# Unpack payload into packet object
|
|
261
|
-
packet = None
|
|
262
|
-
packet_class = get_packet_class(header.pkt_type)
|
|
263
|
-
|
|
264
|
-
# Update packet type statistics
|
|
265
|
-
self.packets_received_by_type[header.pkt_type] += 1
|
|
266
|
-
|
|
267
|
-
if packet_class:
|
|
268
|
-
if payload:
|
|
269
|
-
try:
|
|
270
|
-
packet = packet_class.unpack(payload)
|
|
271
|
-
except Exception as e:
|
|
272
|
-
logger.warning(
|
|
273
|
-
"Failed to unpack %s (type %s) from %s:%s: %s",
|
|
274
|
-
_get_packet_type_name(header.pkt_type),
|
|
275
|
-
header.pkt_type,
|
|
276
|
-
addr[0],
|
|
277
|
-
addr[1],
|
|
278
|
-
e,
|
|
279
|
-
)
|
|
280
|
-
logger.debug(
|
|
281
|
-
"Raw payload (%s bytes): %s", len(payload), payload.hex()
|
|
282
|
-
)
|
|
283
|
-
return
|
|
284
|
-
# else: packet_class exists but no payload (valid for some packet types)
|
|
285
|
-
else:
|
|
286
|
-
# Unknown packet type - log it with raw payload
|
|
287
|
-
target_str = "broadcast" if header.tagged else header.target.hex()
|
|
288
|
-
logger.warning(
|
|
289
|
-
"← RX Unknown packet type %s from %s:%s (target=%s, seq=%s)",
|
|
290
|
-
header.pkt_type,
|
|
291
|
-
addr[0],
|
|
292
|
-
addr[1],
|
|
293
|
-
target_str,
|
|
294
|
-
header.sequence,
|
|
295
|
-
)
|
|
296
|
-
if payload:
|
|
297
|
-
logger.info(
|
|
298
|
-
"Unknown packet payload (%s bytes): %s",
|
|
299
|
-
len(payload),
|
|
300
|
-
payload.hex(),
|
|
301
|
-
)
|
|
302
|
-
# Continue processing - device might still want to respond or log it
|
|
303
|
-
|
|
304
|
-
# Log received packet with details
|
|
305
|
-
packet_name = _get_packet_type_name(header.pkt_type)
|
|
306
|
-
target_str = (
|
|
307
|
-
"broadcast" if header.tagged else header.target.hex().rstrip("0000")
|
|
308
|
-
)
|
|
309
|
-
fields_str = _format_packet_fields(packet)
|
|
310
|
-
logger.debug(
|
|
311
|
-
"← RX %s from %s:%s (target=%s, seq=%s) [%s]",
|
|
312
|
-
packet_name,
|
|
313
|
-
addr[0],
|
|
314
|
-
addr[1],
|
|
315
|
-
target_str,
|
|
316
|
-
header.sequence,
|
|
317
|
-
fields_str,
|
|
318
|
-
)
|
|
319
|
-
|
|
320
|
-
# Notify observer
|
|
321
|
-
self.activity_observer.on_packet_received(
|
|
322
|
-
PacketEvent(
|
|
323
|
-
timestamp=time.time(),
|
|
324
|
-
direction="rx",
|
|
325
|
-
packet_type=header.pkt_type,
|
|
326
|
-
packet_name=packet_name,
|
|
327
|
-
addr=f"{addr[0]}:{addr[1]}",
|
|
328
|
-
target=target_str,
|
|
329
|
-
)
|
|
330
|
-
)
|
|
331
|
-
|
|
332
|
-
# Determine target devices using device manager
|
|
333
|
-
target_devices = self._device_manager.resolve_target_devices(header)
|
|
334
|
-
|
|
335
|
-
# Process packet for each target device
|
|
336
|
-
# Use parallel processing for broadcasts to improve scalability
|
|
337
|
-
if len(target_devices) > 1:
|
|
338
|
-
# Broadcast: process all devices concurrently (limited by GIL)
|
|
339
|
-
tasks = [
|
|
340
|
-
self._process_device_packet(device, header, packet, addr)
|
|
341
|
-
for device in target_devices
|
|
342
|
-
]
|
|
343
|
-
await asyncio.gather(*tasks)
|
|
344
|
-
elif target_devices:
|
|
345
|
-
# Single device: process directly without task overhead
|
|
346
|
-
await self._process_device_packet(
|
|
347
|
-
target_devices[0], header, packet, addr
|
|
348
|
-
)
|
|
349
|
-
|
|
350
|
-
except Exception as e:
|
|
351
|
-
self.error_count += 1
|
|
352
|
-
logger.error("Error handling packet from %s: %s", addr, e, exc_info=True)
|
|
353
|
-
|
|
354
|
-
def add_device(self, device: EmulatedLifxDevice) -> bool:
|
|
355
|
-
"""Add a device to the server.
|
|
356
|
-
|
|
357
|
-
Args:
|
|
358
|
-
device: The device to add
|
|
359
|
-
|
|
360
|
-
Returns:
|
|
361
|
-
True if added, False if device with same serial already exists
|
|
362
|
-
"""
|
|
363
|
-
return self._device_manager.add_device(device, self.scenario_manager)
|
|
364
|
-
|
|
365
|
-
def remove_device(self, serial: str) -> bool:
|
|
366
|
-
"""Remove a device from the server.
|
|
367
|
-
|
|
368
|
-
Args:
|
|
369
|
-
serial: Serial number of device to remove (12 hex chars)
|
|
370
|
-
|
|
371
|
-
Returns:
|
|
372
|
-
True if removed, False if device not found
|
|
373
|
-
"""
|
|
374
|
-
return self._device_manager.remove_device(serial, self.storage)
|
|
375
|
-
|
|
376
|
-
def remove_all_devices(self, delete_storage: bool = False) -> int:
|
|
377
|
-
"""Remove all devices from the server.
|
|
378
|
-
|
|
379
|
-
Args:
|
|
380
|
-
delete_storage: If True, also delete persistent storage files
|
|
381
|
-
|
|
382
|
-
Returns:
|
|
383
|
-
Number of devices removed
|
|
384
|
-
"""
|
|
385
|
-
return self._device_manager.remove_all_devices(delete_storage, self.storage)
|
|
386
|
-
|
|
387
|
-
def get_device(self, serial: str) -> EmulatedLifxDevice | None:
|
|
388
|
-
"""Get a device by serial number.
|
|
389
|
-
|
|
390
|
-
Args:
|
|
391
|
-
serial: Serial number (12 hex chars)
|
|
392
|
-
|
|
393
|
-
Returns:
|
|
394
|
-
Device if found, None otherwise
|
|
395
|
-
"""
|
|
396
|
-
return self._device_manager.get_device(serial)
|
|
397
|
-
|
|
398
|
-
def get_all_devices(self) -> list[EmulatedLifxDevice]:
|
|
399
|
-
"""Get all devices.
|
|
400
|
-
|
|
401
|
-
Returns:
|
|
402
|
-
List of all devices
|
|
403
|
-
"""
|
|
404
|
-
return self._device_manager.get_all_devices()
|
|
405
|
-
|
|
406
|
-
def invalidate_all_scenario_caches(self) -> None:
|
|
407
|
-
"""Invalidate scenario cache for all devices.
|
|
408
|
-
|
|
409
|
-
This should be called when scenario configuration changes to ensure
|
|
410
|
-
devices reload their scenario settings from the scenario manager.
|
|
411
|
-
"""
|
|
412
|
-
self._device_manager.invalidate_all_scenario_caches()
|
|
413
|
-
|
|
414
|
-
def get_stats(self) -> dict[str, Any]:
|
|
415
|
-
"""Get server statistics.
|
|
416
|
-
|
|
417
|
-
Returns:
|
|
418
|
-
Dictionary with statistics
|
|
419
|
-
"""
|
|
420
|
-
uptime = time.time() - self.start_time
|
|
421
|
-
return {
|
|
422
|
-
"uptime_seconds": uptime,
|
|
423
|
-
"start_time": self.start_time,
|
|
424
|
-
"device_count": self._device_manager.count_devices(),
|
|
425
|
-
"packets_received": self.packets_received,
|
|
426
|
-
"packets_sent": self.packets_sent,
|
|
427
|
-
"packets_received_by_type": dict(self.packets_received_by_type),
|
|
428
|
-
"packets_sent_by_type": dict(self.packets_sent_by_type),
|
|
429
|
-
"error_count": self.error_count,
|
|
430
|
-
"activity_enabled": isinstance(self.activity_observer, ActivityLogger),
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
def get_recent_activity(self) -> list[dict[str, Any]]:
|
|
434
|
-
"""Get recent activity events.
|
|
435
|
-
|
|
436
|
-
Returns:
|
|
437
|
-
List of activity event dictionaries, or empty list if observer
|
|
438
|
-
doesn't support activity tracking
|
|
439
|
-
"""
|
|
440
|
-
if isinstance(self.activity_observer, ActivityLogger):
|
|
441
|
-
return self.activity_observer.get_recent_activity()
|
|
442
|
-
return []
|
|
443
|
-
|
|
444
|
-
async def start(self):
|
|
445
|
-
"""Start the server"""
|
|
446
|
-
loop = asyncio.get_running_loop()
|
|
447
|
-
self.transport, _ = await loop.create_datagram_endpoint(
|
|
448
|
-
lambda: self.LifxProtocol(self), local_addr=(self.bind_address, self.port)
|
|
449
|
-
)
|
|
450
|
-
|
|
451
|
-
async def stop(self):
|
|
452
|
-
"""Stop the server"""
|
|
453
|
-
if self.transport:
|
|
454
|
-
self.transport.close()
|
|
455
|
-
|
|
456
|
-
async def __aenter__(self):
|
|
457
|
-
"""Async context manager entry"""
|
|
458
|
-
await self.start()
|
|
459
|
-
return self
|
|
460
|
-
|
|
461
|
-
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
462
|
-
"""Async context manager exit"""
|
|
463
|
-
await self.stop()
|
|
464
|
-
return False
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: lifx-emulator
|
|
3
|
-
Version: 2.3.1
|
|
4
|
-
Summary: LIFX Emulator for testing LIFX LAN protocol libraries
|
|
5
|
-
Author-email: Avi Miller <me@dje.li>
|
|
6
|
-
Maintainer-email: Avi Miller <me@dje.li>
|
|
7
|
-
License-Expression: UPL-1.0
|
|
8
|
-
License-File: LICENSE
|
|
9
|
-
Classifier: Framework :: AsyncIO
|
|
10
|
-
Classifier: Framework :: Pytest
|
|
11
|
-
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: Natural Language :: English
|
|
13
|
-
Classifier: Operating System :: OS Independent
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
-
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
-
Classifier: Typing :: Typed
|
|
21
|
-
Requires-Python: >=3.11
|
|
22
|
-
Requires-Dist: cyclopts>=4.2.0
|
|
23
|
-
Requires-Dist: fastapi>=0.115.0
|
|
24
|
-
Requires-Dist: pyyaml>=6.0.3
|
|
25
|
-
Requires-Dist: rich>=14.2.0
|
|
26
|
-
Requires-Dist: uvicorn>=0.34.0
|
|
27
|
-
Description-Content-Type: text/markdown
|
|
28
|
-
|
|
29
|
-
# LIFX Emulator
|
|
30
|
-
|
|
31
|
-
> A comprehensive LIFX device emulator for testing LIFX LAN protocol libraries
|
|
32
|
-
|
|
33
|
-
[](https://codecov.io/gh/Djelibeybi/lifx-emulator)
|
|
34
|
-
[](https://github.com/Djelibeybi/lifx-emulator/actions/workflows/ci.yml)
|
|
35
|
-
[](https://Djelibeybi.github.io/lifx-emulator/)
|
|
36
|
-
|
|
37
|
-
[](https://github.com/Djelibeybi/lifx-emulator/releases)
|
|
38
|
-
[](https://pypi.org/project/lifx-emulator/)
|
|
39
|
-
[](LICENSE)
|
|
40
|
-
[](https://www.python.org)
|
|
41
|
-
## Overview
|
|
42
|
-
|
|
43
|
-
LIFX Emulator implements the complete binary UDP protocol from [lan.developer.lifx.com](https://lan.developer.lifx.com) by providing virtual LIFX devices for testing without physical hardware. The emulator includes a basic web interface and OpenAPI-compliant REST API for device and scenario management at runtime.
|
|
44
|
-
|
|
45
|
-
## Features
|
|
46
|
-
|
|
47
|
-
- **Complete Protocol Support**: 44+ packet types from the LIFX LAN protocol
|
|
48
|
-
- **Multiple Device Types**: Color lights, infrared, HEV, multizone strips, matrix tiles
|
|
49
|
-
- **REST API and Web Interface**: Monitor and manage your virtual devices during testing
|
|
50
|
-
- **Testing Scenarios**: Built-in support for packet loss, delays, malformed responses
|
|
51
|
-
- **Easy Integration**: Simple Python API and comprehensive CLI
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
## Documentation
|
|
55
|
-
|
|
56
|
-
- **[Installation Guide](https://djelibeybi.github.io/lifx-emulator/getting-started/installation/)** - Get started
|
|
57
|
-
- **[Quick Start](https://djelibeybi.github.io/lifx-emulator/getting-started/quickstart/)** - Your first emulated device
|
|
58
|
-
- **[User Guide](https://djelibeybi.github.io/lifx-emulator/guide/overview/)** - Product specifications and testing scenarios
|
|
59
|
-
- **[Advanced Topics](https://djelibeybi.github.io/lifx-emulator/advanced/device-management-api/)** - REST API and persistent storage
|
|
60
|
-
- **[CLI Reference](https://djelibeybi.github.io/lifx-emulator/getting-started/cli/)** - All CLI options
|
|
61
|
-
- **[Device Types](https://djelibeybi.github.io/lifx-emulator/guide/device-types/)** - Supported devices
|
|
62
|
-
- **[API Reference](https://djelibeybi.github.io/lifx-emulator/api/)** - Complete API docs
|
|
63
|
-
- **[Architecture](https://djelibeybi.github.io/lifx-emulator/architecture/overview/)** - How it works
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
## Use Cases
|
|
67
|
-
|
|
68
|
-
- **Library Testing**: Test your LIFX library without physical devices
|
|
69
|
-
- **CI/CD Integration**: Run automated tests in pipelines
|
|
70
|
-
- **Protocol Development**: Experiment with LIFX protocol features
|
|
71
|
-
- **Error Simulation**: Test error handling with configurable scenarios
|
|
72
|
-
- **Performance Testing**: Test concurrent device handling
|
|
73
|
-
|
|
74
|
-
## Development
|
|
75
|
-
|
|
76
|
-
```bash
|
|
77
|
-
# Clone repository
|
|
78
|
-
git clone https://github.com/Djelibeybi/lifx-emulator.git
|
|
79
|
-
cd lifx-emulator
|
|
80
|
-
|
|
81
|
-
# Install with uv (recommended)
|
|
82
|
-
uv sync
|
|
83
|
-
|
|
84
|
-
# Or with pip
|
|
85
|
-
pip install -e ".[dev]"
|
|
86
|
-
|
|
87
|
-
# Run tests
|
|
88
|
-
uv run pytest
|
|
89
|
-
|
|
90
|
-
# Run linter
|
|
91
|
-
uv run ruff check .
|
|
92
|
-
|
|
93
|
-
# Build docs
|
|
94
|
-
uv run mkdocs serve
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
## License
|
|
99
|
-
|
|
100
|
-
[UPL-1.0](LICENSE)
|
|
101
|
-
|
|
102
|
-
## Links
|
|
103
|
-
|
|
104
|
-
- **Documentation**: https://djelibeybi.github.io/lifx-emulator
|
|
105
|
-
- **GitHub**: https://github.com/Djelibeybi/lifx-emulator
|
|
106
|
-
- **PyPI**: https://pypi.org/project/lifx-emulator/
|
|
107
|
-
- **LIFX Protocol**: https://lan.developer.lifx.com
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
lifx_emulator/__init__.py,sha256=vjhtpAQRSsUZtaUGCQKbmPALvwZ_BF8Mko8w6jzVqBw,819
|
|
2
|
-
lifx_emulator/__main__.py,sha256=C1Khr8MdrDJnRUM5Y-CV1MPf7CqS2qM-vynGDJjdMYg,21678
|
|
3
|
-
lifx_emulator/constants.py,sha256=DFZkUsdewE-x_3MgO28tMGkjUCWPeYc3xLj_EXViGOw,1032
|
|
4
|
-
lifx_emulator/server.py,sha256=r2JYFcpZIqqhue-Nfq7FbN0KfC3XDf3XDb6b43DsiCk,16438
|
|
5
|
-
lifx_emulator/api/__init__.py,sha256=FoEPw_In5-H_BDQ-XIIONvgj-UqIDVtejIEVRv9qmV8,647
|
|
6
|
-
lifx_emulator/api/app.py,sha256=IxK8sC7MgdtkoLz8iXcEt02nPDaVgdKJgEiGnzTs-YE,4880
|
|
7
|
-
lifx_emulator/api/models.py,sha256=qFNo0sOl31yuZLtWmLroSW6f6jck-RhP05tx972xsWA,3971
|
|
8
|
-
lifx_emulator/api/mappers/__init__.py,sha256=ZPCOQR9odcwn0C58AjFW6RvBXe5gOll_QS5lAabgorQ,152
|
|
9
|
-
lifx_emulator/api/mappers/device_mapper.py,sha256=EGOpdao9ZS-vT4T8IoV-AoN5WucTnqpQO92dYizo3vw,4151
|
|
10
|
-
lifx_emulator/api/routers/__init__.py,sha256=kbMefnuXrEsYeMA9J4YK_wVs87_XcH7hwkEifR-zgMc,369
|
|
11
|
-
lifx_emulator/api/routers/devices.py,sha256=i0hFxb9-yA3bbNsk1HyDhHfpAB61o5rObH_vC9gDEpk,4210
|
|
12
|
-
lifx_emulator/api/routers/monitoring.py,sha256=qgVBNm6iMESf1W6EE22DvLalMnxkr0pRbGKu_JDDkPw,1456
|
|
13
|
-
lifx_emulator/api/routers/scenarios.py,sha256=0axSQ9r6rByvXLvqRqOU2ma5nTvZgZ0IIzEXdtzoPnM,9743
|
|
14
|
-
lifx_emulator/api/services/__init__.py,sha256=ttjjZfAxbDQC_Ep0LkXjopNiVZOFPsFDSOHhBN98v5s,277
|
|
15
|
-
lifx_emulator/api/services/device_service.py,sha256=r3uFWApC8sVQMCuuzkyjm27K4LDpZnnHmQNgXWX40ok,6294
|
|
16
|
-
lifx_emulator/api/templates/dashboard.html,sha256=h-PeOH_La5bVOUBcXmTY2leRlMdL8D8yJ-NCx3S16-A,33792
|
|
17
|
-
lifx_emulator/devices/__init__.py,sha256=QlBTPnFErJcSKLvGyeDwemh7xcpjYvB_L5siKsjr3s8,1089
|
|
18
|
-
lifx_emulator/devices/device.py,sha256=24rknbLw_EWF8dheED89wvovKjvC2CdfifLvG7g3SiQ,13648
|
|
19
|
-
lifx_emulator/devices/manager.py,sha256=XDrT82um5sgNpNihLj5RsNvHqdVI1bK9YY2eBzWIcf0,8162
|
|
20
|
-
lifx_emulator/devices/observers.py,sha256=-KnUgFcKdhlNo7CNVstP-u0wU2W0JAGg055ZPV15Sj0,3874
|
|
21
|
-
lifx_emulator/devices/persistence.py,sha256=9Mhj46-xrweOmyzjORCi2jKIwa8XJWpQ5CgaKcw6U98,10513
|
|
22
|
-
lifx_emulator/devices/state_restorer.py,sha256=eDsRSW-2RviP_0Qlk2DHqMaB-zhV0X1cNQECv2lD1qc,9809
|
|
23
|
-
lifx_emulator/devices/state_serializer.py,sha256=aws4LUmXBJS8oBrQziJtlV0XMvCTm5X4dGkGlO_QHcM,6281
|
|
24
|
-
lifx_emulator/devices/states.py,sha256=szWmarFjTBZO1UljEdjvS4W-nanYOzgE3P5df36T5bY,12092
|
|
25
|
-
lifx_emulator/factories/__init__.py,sha256=yN8i_Hu_cFEryWZmh0TiOQvWEYFVIApQSs4xeb0EfBk,1170
|
|
26
|
-
lifx_emulator/factories/builder.py,sha256=6b0frEUMnM-RE2yjoVJzKUav3xn9bOElJPOETSG4NWk,12054
|
|
27
|
-
lifx_emulator/factories/default_config.py,sha256=FTcxKDfeTmO49GTSki8nxnEIZQzR0Lg0hL_PwHUrkVQ,4828
|
|
28
|
-
lifx_emulator/factories/factory.py,sha256=Q2Yr21EC2bLOWwLyqqoUIsJKwWt7MTNODERhTRH6llk,7579
|
|
29
|
-
lifx_emulator/factories/firmware_config.py,sha256=tPN5Hq-uNb1xzW9Q0A9jD-G0-NaGfINcD0i1XZRUMoE,2711
|
|
30
|
-
lifx_emulator/factories/serial_generator.py,sha256=MbaXoommsj76ho8_ZoKuUDnffDf98YvwQiXZSWsUsEs,2507
|
|
31
|
-
lifx_emulator/handlers/__init__.py,sha256=3Hj1hRo3yL3E7GKwG9TaYh33ymk_N3bRiQ8nvqSQULA,1306
|
|
32
|
-
lifx_emulator/handlers/base.py,sha256=0avCLXY_rNlw16PpJ5JrRCwXNE4uMpBqF3PfSfNJ0b8,1654
|
|
33
|
-
lifx_emulator/handlers/device_handlers.py,sha256=1AmslA4Ut6L7b3SfduDdvnQizTpzUB3KKWBXmp4WYLQ,9462
|
|
34
|
-
lifx_emulator/handlers/light_handlers.py,sha256=255aoiIjSIL63kbHQa6wqUpEwFzFFx7SG6P1nWM9jgU,17769
|
|
35
|
-
lifx_emulator/handlers/multizone_handlers.py,sha256=2dYsitq0KzEaxEAJmz7ixtir1tvFMOAnfkBQqslqbPM,7914
|
|
36
|
-
lifx_emulator/handlers/registry.py,sha256=s1ht4PmPhXhAcwu1hoY4yW39wy3SPJBMY-9Uxd0FWuE,3292
|
|
37
|
-
lifx_emulator/handlers/tile_handlers.py,sha256=L4fNKGTSSIxRuqKrfDrMSrNPvDJr3aIuaEqbhRCOt04,17176
|
|
38
|
-
lifx_emulator/products/__init__.py,sha256=qcNop_kRYFF3zSjNemzQEgu3jPrIxfyQyLv9GsnaLEI,627
|
|
39
|
-
lifx_emulator/products/generator.py,sha256=WsbAr2dXXMtLyOlFFkt-xM9kT5WpiUzj6_FKRf16Tng,33536
|
|
40
|
-
lifx_emulator/products/registry.py,sha256=qkm2xgGZo_ds3wAbYplLu4gb0cxhjZXjnCc1V8etpHw,46517
|
|
41
|
-
lifx_emulator/products/specs.py,sha256=epqz2DPyNOOOFHhmI_wlk7iEbgN0vCugHz-hWx9FlAI,8728
|
|
42
|
-
lifx_emulator/products/specs.yml,sha256=-91JNzGhwcO_zybOWY8dFBncN2TnnxtSkkHdi31KT94,9675
|
|
43
|
-
lifx_emulator/protocol/__init__.py,sha256=-wjC-wBcb7fxi5I-mJr2Ad8K2YRflJFdLLdobfD-W1Q,56
|
|
44
|
-
lifx_emulator/protocol/base.py,sha256=V6t0baSgIXjrsz2dBuUn_V9xwradSqMxBFJHAUtnfCs,15368
|
|
45
|
-
lifx_emulator/protocol/const.py,sha256=ilhv-KcQpHtKh2MDCaIbMLQAsxKO_uTaxyR63v1W8cc,226
|
|
46
|
-
lifx_emulator/protocol/generator.py,sha256=LUkf-1Z5570Vg5iA1QhDZDWQOrABqmukUgk9qH-IJmg,49524
|
|
47
|
-
lifx_emulator/protocol/header.py,sha256=RXMJ5YZG1jyxl4Mz46ZGJBYX41Jdp7J95BHuY-scYC0,5499
|
|
48
|
-
lifx_emulator/protocol/packets.py,sha256=Yv4O-Uqbj0CR7n04vXhfalJVCmTTvJTWkvZBkcwPx-U,41553
|
|
49
|
-
lifx_emulator/protocol/protocol_types.py,sha256=WX1p4fmFcNJURmEV_B7ubi7fgu-w9loXQ89q8DdbeSA,23970
|
|
50
|
-
lifx_emulator/protocol/serializer.py,sha256=2bZz7TddxaMRO4_6LujRGCS1w7GxD4E3rRk3r-hpEIE,10738
|
|
51
|
-
lifx_emulator/repositories/__init__.py,sha256=x-ncM6T_Q7jNrwhK4a1uAyMrTGHHGeUzPSLC4O-kEUw,645
|
|
52
|
-
lifx_emulator/repositories/device_repository.py,sha256=KsXVg2sg7PGSTsK_PvDYeHHwEPM9Qx2ZZF_ORncBrYQ,3929
|
|
53
|
-
lifx_emulator/repositories/storage_backend.py,sha256=wEgjhnBvAxl6aO1ZGL3ou0dW9P2hBPnK8jEE03sOlL4,3264
|
|
54
|
-
lifx_emulator/scenarios/__init__.py,sha256=CGjudoWvyysvFj2xej11N2cr3mYROGtRb9zVHcOHGrQ,665
|
|
55
|
-
lifx_emulator/scenarios/manager.py,sha256=1esxRdz74UynNk1wb86MGZ2ZFAuMzByuu74nRe3D-Og,11163
|
|
56
|
-
lifx_emulator/scenarios/models.py,sha256=BKS_fGvrbkGe-vK3arZ0w2f9adS1UZhiOoKpu7GENnc,4099
|
|
57
|
-
lifx_emulator/scenarios/persistence.py,sha256=3vjtPNFYfag38tUxuqxkGpWhQ7uBitc1rLroSAuw9N8,8881
|
|
58
|
-
lifx_emulator-2.3.1.dist-info/METADATA,sha256=L3tETYNKBl5pR1wfSXTsWijV0olIPspASI1du2HMO94,4549
|
|
59
|
-
lifx_emulator-2.3.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
60
|
-
lifx_emulator-2.3.1.dist-info/entry_points.txt,sha256=R9C_K_tTgt6yXEmhzH4r2Yx2Tu1rLlnYzeG4RFUVzSc,62
|
|
61
|
-
lifx_emulator-2.3.1.dist-info/licenses/LICENSE,sha256=eBz48GRA3gSiWn3rYZAz2Ewp35snnhV9cSqkVBq7g3k,1832
|
|
62
|
-
lifx_emulator-2.3.1.dist-info/RECORD,,
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
Copyright (c) 2025 Avi Miller <me@dje.li>
|
|
2
|
-
|
|
3
|
-
The Universal Permissive License (UPL), Version 1.0
|
|
4
|
-
|
|
5
|
-
Subject to the condition set forth below, permission is hereby granted to any
|
|
6
|
-
person obtaining a copy of this software, associated documentation and/or data
|
|
7
|
-
(collectively the "Software"), free of charge and under any and all copyright
|
|
8
|
-
rights in the Software, and any and all patent rights owned or freely
|
|
9
|
-
licensable by each licensor hereunder covering either (i) the unmodified
|
|
10
|
-
Software as contributed to or provided by such licensor, or (ii) the Larger
|
|
11
|
-
Works (as defined below), to deal in both
|
|
12
|
-
|
|
13
|
-
(a) the Software, and
|
|
14
|
-
(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
|
|
15
|
-
one is included with the Software (each a "Larger Work" to which the Software
|
|
16
|
-
is contributed by such licensors),
|
|
17
|
-
|
|
18
|
-
without restriction, including without limitation the rights to copy, create
|
|
19
|
-
derivative works of, display, perform, and distribute the Software and make,
|
|
20
|
-
use, sell, offer for sale, import, export, have made, and have sold the
|
|
21
|
-
Software and the Larger Work(s), and to sublicense the foregoing rights on
|
|
22
|
-
either these or other terms.
|
|
23
|
-
|
|
24
|
-
This license is subject to the following condition:
|
|
25
|
-
The above copyright notice and either this complete permission notice or at
|
|
26
|
-
a minimum a reference to the UPL must be included in all copies or
|
|
27
|
-
substantial portions of the Software.
|
|
28
|
-
|
|
29
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
30
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
31
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
32
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
33
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
34
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
35
|
-
SOFTWARE.
|
|
File without changes
|
|
File without changes
|