lifx-emulator-core 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 (47) hide show
  1. lifx_emulator/__init__.py +31 -0
  2. lifx_emulator/constants.py +33 -0
  3. lifx_emulator/devices/__init__.py +37 -0
  4. lifx_emulator/devices/device.py +395 -0
  5. lifx_emulator/devices/manager.py +256 -0
  6. lifx_emulator/devices/observers.py +139 -0
  7. lifx_emulator/devices/persistence.py +308 -0
  8. lifx_emulator/devices/state_restorer.py +259 -0
  9. lifx_emulator/devices/state_serializer.py +157 -0
  10. lifx_emulator/devices/states.py +381 -0
  11. lifx_emulator/factories/__init__.py +39 -0
  12. lifx_emulator/factories/builder.py +375 -0
  13. lifx_emulator/factories/default_config.py +158 -0
  14. lifx_emulator/factories/factory.py +252 -0
  15. lifx_emulator/factories/firmware_config.py +77 -0
  16. lifx_emulator/factories/serial_generator.py +82 -0
  17. lifx_emulator/handlers/__init__.py +39 -0
  18. lifx_emulator/handlers/base.py +49 -0
  19. lifx_emulator/handlers/device_handlers.py +322 -0
  20. lifx_emulator/handlers/light_handlers.py +503 -0
  21. lifx_emulator/handlers/multizone_handlers.py +249 -0
  22. lifx_emulator/handlers/registry.py +110 -0
  23. lifx_emulator/handlers/tile_handlers.py +488 -0
  24. lifx_emulator/products/__init__.py +28 -0
  25. lifx_emulator/products/generator.py +1079 -0
  26. lifx_emulator/products/registry.py +1530 -0
  27. lifx_emulator/products/specs.py +284 -0
  28. lifx_emulator/products/specs.yml +386 -0
  29. lifx_emulator/protocol/__init__.py +1 -0
  30. lifx_emulator/protocol/base.py +446 -0
  31. lifx_emulator/protocol/const.py +8 -0
  32. lifx_emulator/protocol/generator.py +1384 -0
  33. lifx_emulator/protocol/header.py +159 -0
  34. lifx_emulator/protocol/packets.py +1351 -0
  35. lifx_emulator/protocol/protocol_types.py +817 -0
  36. lifx_emulator/protocol/serializer.py +379 -0
  37. lifx_emulator/repositories/__init__.py +22 -0
  38. lifx_emulator/repositories/device_repository.py +155 -0
  39. lifx_emulator/repositories/storage_backend.py +107 -0
  40. lifx_emulator/scenarios/__init__.py +22 -0
  41. lifx_emulator/scenarios/manager.py +322 -0
  42. lifx_emulator/scenarios/models.py +112 -0
  43. lifx_emulator/scenarios/persistence.py +241 -0
  44. lifx_emulator/server.py +464 -0
  45. lifx_emulator_core-3.0.1.dist-info/METADATA +99 -0
  46. lifx_emulator_core-3.0.1.dist-info/RECORD +47 -0
  47. lifx_emulator_core-3.0.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,464 @@
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
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: lifx-emulator-core
3
+ Version: 3.0.1
4
+ Summary: Core LIFX Emulator library 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
+ Classifier: Framework :: AsyncIO
9
+ Classifier: Framework :: Pytest
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Natural Language :: English
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Topic :: Software Development :: Testing
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: pyyaml>=6.0.3
23
+ Description-Content-Type: text/markdown
24
+
25
+ # lifx-emulator-core
26
+
27
+ Core Python library for emulating LIFX devices using the LAN protocol.
28
+
29
+ This package provides the embeddable library for creating virtual LIFX devices in your own projects. It implements the binary UDP protocol from the [LIFX LAN Protocol](https://lan.developer.lifx.com) specification.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install lifx-emulator-core
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ ```python
40
+ import asyncio
41
+ from lifx_emulator import EmulatedLifxServer, DeviceManager
42
+ from lifx_emulator.factories import create_color_light
43
+ from lifx_emulator.repositories import DeviceRepository
44
+
45
+ async def main():
46
+ # Create devices
47
+ devices = [
48
+ create_color_light(serial="d073d5000001"),
49
+ create_color_light(serial="d073d5000002"),
50
+ ]
51
+
52
+ # Create repository and manager
53
+ repository = DeviceRepository()
54
+ manager = DeviceManager(repository)
55
+
56
+ # Start the emulator server
57
+ server = EmulatedLifxServer(
58
+ devices=devices,
59
+ device_manager=manager,
60
+ bind_address="127.0.0.1",
61
+ port=56700,
62
+ )
63
+
64
+ await server.start()
65
+ print("LIFX Emulator running on 127.0.0.1:56700")
66
+
67
+ # Keep running until interrupted
68
+ try:
69
+ await asyncio.Event().wait()
70
+ finally:
71
+ await server.stop()
72
+
73
+ asyncio.run(main())
74
+ ```
75
+
76
+ ## Features
77
+
78
+ - Emulate color lights, multizone strips, tiles, infrared, HEV, and switch devices
79
+ - Full LIFX LAN protocol implementation
80
+ - Persistent device state storage
81
+ - Testing scenarios for simulating edge cases
82
+ - Product registry with 137+ official LIFX products
83
+
84
+ ## Documentation
85
+
86
+ Full documentation is available at: **https://djelibeybi.github.io/lifx-emulator**
87
+
88
+ - [Installation Guide](https://djelibeybi.github.io/lifx-emulator/getting-started/installation/)
89
+ - [Quick Start](https://djelibeybi.github.io/lifx-emulator/getting-started/quickstart/)
90
+ - [API Reference](https://djelibeybi.github.io/lifx-emulator/library/)
91
+ - [Architecture](https://djelibeybi.github.io/lifx-emulator/architecture/)
92
+
93
+ ## Related Packages
94
+
95
+ - **[lifx-emulator](https://pypi.org/project/lifx-emulator/)**: Standalone CLI tool and HTTP management API built on this library
96
+
97
+ ## License
98
+
99
+ [UPL-1.0](https://opensource.org/licenses/UPL)
@@ -0,0 +1,47 @@
1
+ lifx_emulator/__init__.py,sha256=SSnQg0RiCaID7DhHOGZoVIDQ3_8Lyt0J9cA_2StF63s,824
2
+ lifx_emulator/constants.py,sha256=DFZkUsdewE-x_3MgO28tMGkjUCWPeYc3xLj_EXViGOw,1032
3
+ lifx_emulator/server.py,sha256=r2JYFcpZIqqhue-Nfq7FbN0KfC3XDf3XDb6b43DsiCk,16438
4
+ lifx_emulator/devices/__init__.py,sha256=QlBTPnFErJcSKLvGyeDwemh7xcpjYvB_L5siKsjr3s8,1089
5
+ lifx_emulator/devices/device.py,sha256=yEOXc_xr1X45bJzG2qB-A-oIHwnA8qqYlIsFialobGc,15780
6
+ lifx_emulator/devices/manager.py,sha256=XDrT82um5sgNpNihLj5RsNvHqdVI1bK9YY2eBzWIcf0,8162
7
+ lifx_emulator/devices/observers.py,sha256=-KnUgFcKdhlNo7CNVstP-u0wU2W0JAGg055ZPV15Sj0,3874
8
+ lifx_emulator/devices/persistence.py,sha256=9Mhj46-xrweOmyzjORCi2jKIwa8XJWpQ5CgaKcw6U98,10513
9
+ lifx_emulator/devices/state_restorer.py,sha256=eDsRSW-2RviP_0Qlk2DHqMaB-zhV0X1cNQECv2lD1qc,9809
10
+ lifx_emulator/devices/state_serializer.py,sha256=aws4LUmXBJS8oBrQziJtlV0XMvCTm5X4dGkGlO_QHcM,6281
11
+ lifx_emulator/devices/states.py,sha256=kNv-VV1UCDxPduixU1-5xBGKRzeCfE-bYzzEh_1GnUU,12204
12
+ lifx_emulator/factories/__init__.py,sha256=CsryMcf_80hTjOAgrukA6vRZaZow_2VQkSewrpP9gEI,1210
13
+ lifx_emulator/factories/builder.py,sha256=xs3g3_-euUqgdcBu_3umPZb-xlzDeoDeOrwEGJShOwA,12164
14
+ lifx_emulator/factories/default_config.py,sha256=FTcxKDfeTmO49GTSki8nxnEIZQzR0Lg0hL_PwHUrkVQ,4828
15
+ lifx_emulator/factories/factory.py,sha256=MyGG-pW7EV2BFP5ZzgMuFF5TfNFvfyFDoE5dmd3LC8w,8623
16
+ lifx_emulator/factories/firmware_config.py,sha256=tPN5Hq-uNb1xzW9Q0A9jD-G0-NaGfINcD0i1XZRUMoE,2711
17
+ lifx_emulator/factories/serial_generator.py,sha256=MbaXoommsj76ho8_ZoKuUDnffDf98YvwQiXZSWsUsEs,2507
18
+ lifx_emulator/handlers/__init__.py,sha256=3Hj1hRo3yL3E7GKwG9TaYh33ymk_N3bRiQ8nvqSQULA,1306
19
+ lifx_emulator/handlers/base.py,sha256=0avCLXY_rNlw16PpJ5JrRCwXNE4uMpBqF3PfSfNJ0b8,1654
20
+ lifx_emulator/handlers/device_handlers.py,sha256=1AmslA4Ut6L7b3SfduDdvnQizTpzUB3KKWBXmp4WYLQ,9462
21
+ lifx_emulator/handlers/light_handlers.py,sha256=255aoiIjSIL63kbHQa6wqUpEwFzFFx7SG6P1nWM9jgU,17769
22
+ lifx_emulator/handlers/multizone_handlers.py,sha256=2dYsitq0KzEaxEAJmz7ixtir1tvFMOAnfkBQqslqbPM,7914
23
+ lifx_emulator/handlers/registry.py,sha256=s1ht4PmPhXhAcwu1hoY4yW39wy3SPJBMY-9Uxd0FWuE,3292
24
+ lifx_emulator/handlers/tile_handlers.py,sha256=L4fNKGTSSIxRuqKrfDrMSrNPvDJr3aIuaEqbhRCOt04,17176
25
+ lifx_emulator/products/__init__.py,sha256=qcNop_kRYFF3zSjNemzQEgu3jPrIxfyQyLv9GsnaLEI,627
26
+ lifx_emulator/products/generator.py,sha256=fvrhw_b7shLCtEtUFxWF5VBEQAeSrsaiXxoGIP5Vn4g,34675
27
+ lifx_emulator/products/registry.py,sha256=1SZ3fXVFFL8jhKYIZBqwtIQDN3qL1Lvf86P3N1_Kdx8,47323
28
+ lifx_emulator/products/specs.py,sha256=epqz2DPyNOOOFHhmI_wlk7iEbgN0vCugHz-hWx9FlAI,8728
29
+ lifx_emulator/products/specs.yml,sha256=6hh7V-953uN4t3WD2rY9Nn8zKFZuQDHgYVo7LgZcGEA,10399
30
+ lifx_emulator/protocol/__init__.py,sha256=-wjC-wBcb7fxi5I-mJr2Ad8K2YRflJFdLLdobfD-W1Q,56
31
+ lifx_emulator/protocol/base.py,sha256=V6t0baSgIXjrsz2dBuUn_V9xwradSqMxBFJHAUtnfCs,15368
32
+ lifx_emulator/protocol/const.py,sha256=ilhv-KcQpHtKh2MDCaIbMLQAsxKO_uTaxyR63v1W8cc,226
33
+ lifx_emulator/protocol/generator.py,sha256=LUkf-1Z5570Vg5iA1QhDZDWQOrABqmukUgk9qH-IJmg,49524
34
+ lifx_emulator/protocol/header.py,sha256=RXMJ5YZG1jyxl4Mz46ZGJBYX41Jdp7J95BHuY-scYC0,5499
35
+ lifx_emulator/protocol/packets.py,sha256=Yv4O-Uqbj0CR7n04vXhfalJVCmTTvJTWkvZBkcwPx-U,41553
36
+ lifx_emulator/protocol/protocol_types.py,sha256=WX1p4fmFcNJURmEV_B7ubi7fgu-w9loXQ89q8DdbeSA,23970
37
+ lifx_emulator/protocol/serializer.py,sha256=2bZz7TddxaMRO4_6LujRGCS1w7GxD4E3rRk3r-hpEIE,10738
38
+ lifx_emulator/repositories/__init__.py,sha256=x-ncM6T_Q7jNrwhK4a1uAyMrTGHHGeUzPSLC4O-kEUw,645
39
+ lifx_emulator/repositories/device_repository.py,sha256=KsXVg2sg7PGSTsK_PvDYeHHwEPM9Qx2ZZF_ORncBrYQ,3929
40
+ lifx_emulator/repositories/storage_backend.py,sha256=wEgjhnBvAxl6aO1ZGL3ou0dW9P2hBPnK8jEE03sOlL4,3264
41
+ lifx_emulator/scenarios/__init__.py,sha256=CGjudoWvyysvFj2xej11N2cr3mYROGtRb9zVHcOHGrQ,665
42
+ lifx_emulator/scenarios/manager.py,sha256=1esxRdz74UynNk1wb86MGZ2ZFAuMzByuu74nRe3D-Og,11163
43
+ lifx_emulator/scenarios/models.py,sha256=BKS_fGvrbkGe-vK3arZ0w2f9adS1UZhiOoKpu7GENnc,4099
44
+ lifx_emulator/scenarios/persistence.py,sha256=3vjtPNFYfag38tUxuqxkGpWhQ7uBitc1rLroSAuw9N8,8881
45
+ lifx_emulator_core-3.0.1.dist-info/METADATA,sha256=9y8uSmGZxFTONknzHLN1dypHQb-UE6F44PA8OPHINLc,3135
46
+ lifx_emulator_core-3.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
47
+ lifx_emulator_core-3.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any