lifx-emulator 2.4.0__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.
Files changed (68) hide show
  1. lifx_emulator-3.0.1.dist-info/METADATA +102 -0
  2. lifx_emulator-3.0.1.dist-info/RECORD +18 -0
  3. lifx_emulator-3.0.1.dist-info/entry_points.txt +2 -0
  4. lifx_emulator_app/__init__.py +10 -0
  5. {lifx_emulator → lifx_emulator_app}/__main__.py +2 -3
  6. {lifx_emulator → lifx_emulator_app}/api/__init__.py +1 -1
  7. {lifx_emulator → lifx_emulator_app}/api/app.py +3 -3
  8. {lifx_emulator → lifx_emulator_app}/api/mappers/__init__.py +1 -1
  9. {lifx_emulator → lifx_emulator_app}/api/mappers/device_mapper.py +1 -1
  10. {lifx_emulator → lifx_emulator_app}/api/models.py +1 -2
  11. lifx_emulator_app/api/routers/__init__.py +11 -0
  12. {lifx_emulator → lifx_emulator_app}/api/routers/devices.py +2 -2
  13. {lifx_emulator → lifx_emulator_app}/api/routers/monitoring.py +1 -1
  14. {lifx_emulator → lifx_emulator_app}/api/routers/scenarios.py +1 -1
  15. lifx_emulator_app/api/services/__init__.py +8 -0
  16. {lifx_emulator → lifx_emulator_app}/api/services/device_service.py +3 -2
  17. lifx_emulator/__init__.py +0 -31
  18. lifx_emulator/api/routers/__init__.py +0 -11
  19. lifx_emulator/api/services/__init__.py +0 -8
  20. lifx_emulator/constants.py +0 -33
  21. lifx_emulator/devices/__init__.py +0 -37
  22. lifx_emulator/devices/device.py +0 -395
  23. lifx_emulator/devices/manager.py +0 -256
  24. lifx_emulator/devices/observers.py +0 -139
  25. lifx_emulator/devices/persistence.py +0 -308
  26. lifx_emulator/devices/state_restorer.py +0 -259
  27. lifx_emulator/devices/state_serializer.py +0 -157
  28. lifx_emulator/devices/states.py +0 -381
  29. lifx_emulator/factories/__init__.py +0 -39
  30. lifx_emulator/factories/builder.py +0 -375
  31. lifx_emulator/factories/default_config.py +0 -158
  32. lifx_emulator/factories/factory.py +0 -252
  33. lifx_emulator/factories/firmware_config.py +0 -77
  34. lifx_emulator/factories/serial_generator.py +0 -82
  35. lifx_emulator/handlers/__init__.py +0 -39
  36. lifx_emulator/handlers/base.py +0 -49
  37. lifx_emulator/handlers/device_handlers.py +0 -322
  38. lifx_emulator/handlers/light_handlers.py +0 -503
  39. lifx_emulator/handlers/multizone_handlers.py +0 -249
  40. lifx_emulator/handlers/registry.py +0 -110
  41. lifx_emulator/handlers/tile_handlers.py +0 -488
  42. lifx_emulator/products/__init__.py +0 -28
  43. lifx_emulator/products/generator.py +0 -1079
  44. lifx_emulator/products/registry.py +0 -1530
  45. lifx_emulator/products/specs.py +0 -284
  46. lifx_emulator/products/specs.yml +0 -386
  47. lifx_emulator/protocol/__init__.py +0 -1
  48. lifx_emulator/protocol/base.py +0 -446
  49. lifx_emulator/protocol/const.py +0 -8
  50. lifx_emulator/protocol/generator.py +0 -1384
  51. lifx_emulator/protocol/header.py +0 -159
  52. lifx_emulator/protocol/packets.py +0 -1351
  53. lifx_emulator/protocol/protocol_types.py +0 -817
  54. lifx_emulator/protocol/serializer.py +0 -379
  55. lifx_emulator/repositories/__init__.py +0 -22
  56. lifx_emulator/repositories/device_repository.py +0 -155
  57. lifx_emulator/repositories/storage_backend.py +0 -107
  58. lifx_emulator/scenarios/__init__.py +0 -22
  59. lifx_emulator/scenarios/manager.py +0 -322
  60. lifx_emulator/scenarios/models.py +0 -112
  61. lifx_emulator/scenarios/persistence.py +0 -241
  62. lifx_emulator/server.py +0 -464
  63. lifx_emulator-2.4.0.dist-info/METADATA +0 -107
  64. lifx_emulator-2.4.0.dist-info/RECORD +0 -62
  65. lifx_emulator-2.4.0.dist-info/entry_points.txt +0 -2
  66. lifx_emulator-2.4.0.dist-info/licenses/LICENSE +0 -35
  67. {lifx_emulator-2.4.0.dist-info → lifx_emulator-3.0.1.dist-info}/WHEEL +0 -0
  68. {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.4.0
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
- [![Codecov](https://codecov.io/gh/Djelibeybi/lifx-emulator/branch/main/graph/badge.svg)](https://codecov.io/gh/Djelibeybi/lifx-emulator)
34
- [![CI](https://github.com/Djelibeybi/lifx-emulator/actions/workflows/ci.yml/badge.svg)](https://github.com/Djelibeybi/lifx-emulator/actions/workflows/ci.yml)
35
- [![Docs](https://github.com/Djelibeybi/lifx-emulator/workflows/Documentation/badge.svg)](https://Djelibeybi.github.io/lifx-emulator/)
36
-
37
- [![GitHub](https://img.shields.io/github/v/release/Djelibeybi/lifx-emulator)](https://github.com/Djelibeybi/lifx-emulator/releases)
38
- [![PyPI](https://img.shields.io/pypi/v/lifx-emulator)](https://pypi.org/project/lifx-emulator/)
39
- [![License](https://img.shields.io/badge/License-UPL--1.0-blue.svg)](LICENSE)
40
- [![Python](https://img.shields.io/badge/python-3.11%20|%203.12%20|%203.13%20|%203.14-blue)](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=0rNT1ua1uckq9f2l7g6wIC5VDj9kmWzuL-UZBwm5_tE,22057
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=yEOXc_xr1X45bJzG2qB-A-oIHwnA8qqYlIsFialobGc,15780
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=kNv-VV1UCDxPduixU1-5xBGKRzeCfE-bYzzEh_1GnUU,12204
25
- lifx_emulator/factories/__init__.py,sha256=CsryMcf_80hTjOAgrukA6vRZaZow_2VQkSewrpP9gEI,1210
26
- lifx_emulator/factories/builder.py,sha256=xs3g3_-euUqgdcBu_3umPZb-xlzDeoDeOrwEGJShOwA,12164
27
- lifx_emulator/factories/default_config.py,sha256=FTcxKDfeTmO49GTSki8nxnEIZQzR0Lg0hL_PwHUrkVQ,4828
28
- lifx_emulator/factories/factory.py,sha256=MyGG-pW7EV2BFP5ZzgMuFF5TfNFvfyFDoE5dmd3LC8w,8623
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=fvrhw_b7shLCtEtUFxWF5VBEQAeSrsaiXxoGIP5Vn4g,34675
40
- lifx_emulator/products/registry.py,sha256=1SZ3fXVFFL8jhKYIZBqwtIQDN3qL1Lvf86P3N1_Kdx8,47323
41
- lifx_emulator/products/specs.py,sha256=epqz2DPyNOOOFHhmI_wlk7iEbgN0vCugHz-hWx9FlAI,8728
42
- lifx_emulator/products/specs.yml,sha256=6hh7V-953uN4t3WD2rY9Nn8zKFZuQDHgYVo7LgZcGEA,10399
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.4.0.dist-info/METADATA,sha256=qohy51CSwGIwGXstHI26VBqfY3_aYWgBEQDYE4nEnI8,4549
59
- lifx_emulator-2.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
60
- lifx_emulator-2.4.0.dist-info/entry_points.txt,sha256=R9C_K_tTgt6yXEmhzH4r2Yx2Tu1rLlnYzeG4RFUVzSc,62
61
- lifx_emulator-2.4.0.dist-info/licenses/LICENSE,sha256=eBz48GRA3gSiWn3rYZAz2Ewp35snnhV9cSqkVBq7g3k,1832
62
- lifx_emulator-2.4.0.dist-info/RECORD,,
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- lifx-emulator = lifx_emulator.__main__:main
@@ -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.