lifx-emulator 2.4.0__py3-none-any.whl → 3.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. lifx_emulator-3.1.0.dist-info/METADATA +103 -0
  2. lifx_emulator-3.1.0.dist-info/RECORD +19 -0
  3. {lifx_emulator-2.4.0.dist-info → lifx_emulator-3.1.0.dist-info}/WHEEL +1 -1
  4. lifx_emulator-3.1.0.dist-info/entry_points.txt +2 -0
  5. lifx_emulator_app/__init__.py +10 -0
  6. {lifx_emulator → lifx_emulator_app}/__main__.py +2 -3
  7. {lifx_emulator → lifx_emulator_app}/api/__init__.py +1 -1
  8. {lifx_emulator → lifx_emulator_app}/api/app.py +9 -4
  9. {lifx_emulator → lifx_emulator_app}/api/mappers/__init__.py +1 -1
  10. {lifx_emulator → lifx_emulator_app}/api/mappers/device_mapper.py +1 -1
  11. {lifx_emulator → lifx_emulator_app}/api/models.py +1 -2
  12. lifx_emulator_app/api/routers/__init__.py +11 -0
  13. {lifx_emulator → lifx_emulator_app}/api/routers/devices.py +2 -2
  14. {lifx_emulator → lifx_emulator_app}/api/routers/monitoring.py +1 -1
  15. {lifx_emulator → lifx_emulator_app}/api/routers/scenarios.py +1 -1
  16. lifx_emulator_app/api/services/__init__.py +8 -0
  17. {lifx_emulator → lifx_emulator_app}/api/services/device_service.py +3 -2
  18. lifx_emulator_app/api/static/dashboard.js +588 -0
  19. lifx_emulator_app/api/templates/dashboard.html +357 -0
  20. lifx_emulator/__init__.py +0 -31
  21. lifx_emulator/api/routers/__init__.py +0 -11
  22. lifx_emulator/api/services/__init__.py +0 -8
  23. lifx_emulator/api/templates/dashboard.html +0 -899
  24. lifx_emulator/constants.py +0 -33
  25. lifx_emulator/devices/__init__.py +0 -37
  26. lifx_emulator/devices/device.py +0 -395
  27. lifx_emulator/devices/manager.py +0 -256
  28. lifx_emulator/devices/observers.py +0 -139
  29. lifx_emulator/devices/persistence.py +0 -308
  30. lifx_emulator/devices/state_restorer.py +0 -259
  31. lifx_emulator/devices/state_serializer.py +0 -157
  32. lifx_emulator/devices/states.py +0 -381
  33. lifx_emulator/factories/__init__.py +0 -39
  34. lifx_emulator/factories/builder.py +0 -375
  35. lifx_emulator/factories/default_config.py +0 -158
  36. lifx_emulator/factories/factory.py +0 -252
  37. lifx_emulator/factories/firmware_config.py +0 -77
  38. lifx_emulator/factories/serial_generator.py +0 -82
  39. lifx_emulator/handlers/__init__.py +0 -39
  40. lifx_emulator/handlers/base.py +0 -49
  41. lifx_emulator/handlers/device_handlers.py +0 -322
  42. lifx_emulator/handlers/light_handlers.py +0 -503
  43. lifx_emulator/handlers/multizone_handlers.py +0 -249
  44. lifx_emulator/handlers/registry.py +0 -110
  45. lifx_emulator/handlers/tile_handlers.py +0 -488
  46. lifx_emulator/products/__init__.py +0 -28
  47. lifx_emulator/products/generator.py +0 -1079
  48. lifx_emulator/products/registry.py +0 -1530
  49. lifx_emulator/products/specs.py +0 -284
  50. lifx_emulator/products/specs.yml +0 -386
  51. lifx_emulator/protocol/__init__.py +0 -1
  52. lifx_emulator/protocol/base.py +0 -446
  53. lifx_emulator/protocol/const.py +0 -8
  54. lifx_emulator/protocol/generator.py +0 -1384
  55. lifx_emulator/protocol/header.py +0 -159
  56. lifx_emulator/protocol/packets.py +0 -1351
  57. lifx_emulator/protocol/protocol_types.py +0 -817
  58. lifx_emulator/protocol/serializer.py +0 -379
  59. lifx_emulator/repositories/__init__.py +0 -22
  60. lifx_emulator/repositories/device_repository.py +0 -155
  61. lifx_emulator/repositories/storage_backend.py +0 -107
  62. lifx_emulator/scenarios/__init__.py +0 -22
  63. lifx_emulator/scenarios/manager.py +0 -322
  64. lifx_emulator/scenarios/models.py +0 -112
  65. lifx_emulator/scenarios/persistence.py +0 -241
  66. lifx_emulator/server.py +0 -464
  67. lifx_emulator-2.4.0.dist-info/METADATA +0 -107
  68. lifx_emulator-2.4.0.dist-info/RECORD +0 -62
  69. lifx_emulator-2.4.0.dist-info/entry_points.txt +0 -2
  70. lifx_emulator-2.4.0.dist-info/licenses/LICENSE +0 -35
@@ -1,379 +0,0 @@
1
- """Binary serialization for LIFX protocol packets.
2
-
3
- Handles packing and unpacking of protocol structures using struct module.
4
- All multi-byte values use little-endian byte order per LIFX specification.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- import struct
10
- from typing import Any
11
-
12
- # Type format mapping for struct module (little-endian)
13
- TYPE_FORMATS: dict[str, str] = {
14
- "uint8": "B",
15
- "uint16": "H",
16
- "uint32": "I",
17
- "uint64": "Q",
18
- "int8": "b",
19
- "int16": "h",
20
- "int32": "i",
21
- "int64": "q",
22
- "float32": "f",
23
- "bool": "?",
24
- }
25
-
26
- # Type sizes in bytes
27
- TYPE_SIZES: dict[str, int] = {
28
- "uint8": 1,
29
- "uint16": 2,
30
- "uint32": 4,
31
- "uint64": 8,
32
- "int8": 1,
33
- "int16": 2,
34
- "int32": 4,
35
- "int64": 8,
36
- "float32": 4,
37
- "bool": 1,
38
- }
39
-
40
- # Pre-compiled struct.Struct objects for faster pack/unpack (optimization)
41
- _STRUCT_CACHE: dict[str, struct.Struct] = {
42
- "uint8": struct.Struct("<B"),
43
- "uint16": struct.Struct("<H"),
44
- "uint32": struct.Struct("<I"),
45
- "uint64": struct.Struct("<Q"),
46
- "int8": struct.Struct("<b"),
47
- "int16": struct.Struct("<h"),
48
- "int32": struct.Struct("<i"),
49
- "int64": struct.Struct("<q"),
50
- "float32": struct.Struct("<f"),
51
- "bool": struct.Struct("<?"),
52
- }
53
-
54
-
55
- def get_type_size(type_name: str) -> int:
56
- """Get the size in bytes of a type.
57
-
58
- Args:
59
- type_name: Type name (e.g., 'uint16', 'float32')
60
-
61
- Returns:
62
- Size in bytes
63
-
64
- Raises:
65
- ValueError: If type is unknown
66
- """
67
- if type_name not in TYPE_SIZES:
68
- raise ValueError(f"Unknown type: {type_name}")
69
- return TYPE_SIZES[type_name]
70
-
71
-
72
- def pack_value(value: Any, type_name: str) -> bytes:
73
- """Pack a single value into bytes.
74
-
75
- Args:
76
- value: Value to pack
77
- type_name: Type name (e.g., 'uint16', 'float32')
78
-
79
- Returns:
80
- Packed bytes
81
-
82
- Raises:
83
- ValueError: If type is unknown
84
- struct.error: If value doesn't match type
85
- """
86
- if type_name not in _STRUCT_CACHE:
87
- raise ValueError(f"Unknown type: {type_name}")
88
-
89
- return _STRUCT_CACHE[type_name].pack(value)
90
-
91
-
92
- def unpack_value(data: bytes, type_name: str, offset: int = 0) -> tuple[Any, int]:
93
- """Unpack a single value from bytes.
94
-
95
- Args:
96
- data: Bytes to unpack from
97
- type_name: Type name (e.g., 'uint16', 'float32')
98
- offset: Offset in bytes to start unpacking
99
-
100
- Returns:
101
- Tuple of (unpacked_value, new_offset)
102
-
103
- Raises:
104
- ValueError: If type is unknown or data is too short
105
- struct.error: If data format is invalid
106
- """
107
- if type_name not in _STRUCT_CACHE:
108
- raise ValueError(f"Unknown type: {type_name}")
109
-
110
- size = TYPE_SIZES[type_name]
111
-
112
- if len(data) < offset + size:
113
- raise ValueError(
114
- f"Not enough data to unpack {type_name}: "
115
- f"need {offset + size} bytes, got {len(data)}"
116
- )
117
-
118
- value = _STRUCT_CACHE[type_name].unpack_from(data, offset)[0]
119
- return value, offset + size
120
-
121
-
122
- def pack_array(values: list[Any], element_type: str, count: int) -> bytes:
123
- """Pack an array of values into bytes.
124
-
125
- Args:
126
- values: List of values to pack
127
- element_type: Type of each element (e.g., 'uint8', 'uint16')
128
- count: Expected number of elements
129
-
130
- Returns:
131
- Packed bytes
132
-
133
- Raises:
134
- ValueError: If values length doesn't match count or type is unknown
135
- """
136
- if len(values) != count:
137
- raise ValueError(f"Expected {count} values, got {len(values)}")
138
-
139
- # Optimization: Pack entire primitive array at once with single struct call
140
- if element_type in TYPE_FORMATS:
141
- format_str = f"<{count}{TYPE_FORMATS[element_type]}"
142
- return struct.pack(format_str, *values)
143
-
144
- # Fall back to element-by-element for complex types
145
- result = b""
146
- for value in values:
147
- result += pack_value(value, element_type)
148
- return result
149
-
150
-
151
- def unpack_array(
152
- data: bytes, element_type: str, count: int, offset: int = 0
153
- ) -> tuple[list[Any], int]:
154
- """Unpack an array of values from bytes.
155
-
156
- Args:
157
- data: Bytes to unpack from
158
- element_type: Type of each element
159
- count: Number of elements to unpack
160
- offset: Offset in bytes to start unpacking
161
-
162
- Returns:
163
- Tuple of (list_of_values, new_offset)
164
- """
165
- # Optimization: Unpack entire primitive array at once with single struct call
166
- if element_type in TYPE_FORMATS:
167
- format_str = f"<{count}{TYPE_FORMATS[element_type]}"
168
- size = TYPE_SIZES[element_type] * count
169
-
170
- if len(data) < offset + size:
171
- raise ValueError(
172
- f"Not enough data to unpack array: "
173
- f"need {offset + size} bytes, got {len(data)}"
174
- )
175
-
176
- values = list(struct.unpack_from(format_str, data, offset))
177
- return values, offset + size
178
-
179
- # Fall back to element-by-element for complex types
180
- values = []
181
- current_offset = offset
182
-
183
- for _ in range(count):
184
- value, current_offset = unpack_value(data, element_type, current_offset)
185
- values.append(value)
186
-
187
- return values, current_offset
188
-
189
-
190
- def pack_string(value: str, length: int) -> bytes:
191
- """Pack a string into fixed-length byte array.
192
-
193
- Safely truncates at UTF-8 character boundaries to avoid creating
194
- invalid UTF-8 sequences that could crash device firmware (VUL-002 mitigation).
195
-
196
- Args:
197
- value: String to pack
198
- length: Fixed length in bytes
199
-
200
- Returns:
201
- Packed bytes (null-padded if necessary)
202
- """
203
- encoded = value.encode("utf-8")
204
-
205
- # Safe truncation at character boundary
206
- if len(encoded) > length:
207
- # Decode and re-encode to find safe truncation point
208
- truncated = encoded[:length]
209
- # Find valid UTF-8 boundary by trying to decode
210
- while truncated:
211
- try:
212
- truncated.decode("utf-8")
213
- break
214
- except UnicodeDecodeError:
215
- # Remove last byte and try again
216
- truncated = truncated[:-1]
217
- encoded = truncated
218
-
219
- return encoded.ljust(length, b"\x00")
220
-
221
-
222
- def unpack_string(data: bytes, length: int, offset: int = 0) -> tuple[str, int]:
223
- """Unpack a fixed-length string from bytes.
224
-
225
- Args:
226
- data: Bytes to unpack from
227
- length: Length in bytes to read
228
- offset: Offset in bytes to start unpacking
229
-
230
- Returns:
231
- Tuple of (string, new_offset)
232
- """
233
- if len(data) < offset + length:
234
- raise ValueError(
235
- f"Not enough data to unpack string: "
236
- f"need {offset + length} bytes, got {len(data)}"
237
- )
238
-
239
- raw_bytes = data[offset : offset + length]
240
- # Strip null bytes and decode
241
- string = raw_bytes.rstrip(b"\x00").decode("utf-8", errors="replace")
242
- return string, offset + length
243
-
244
-
245
- def pack_reserved(size: int) -> bytes:
246
- """Pack reserved (zero) bytes.
247
-
248
- Args:
249
- size: Number of bytes
250
-
251
- Returns:
252
- Zero bytes
253
- """
254
- return b"\x00" * size
255
-
256
-
257
- def pack_bytes(data: bytes, length: int) -> bytes:
258
- """Pack bytes into fixed-length byte array.
259
-
260
- Args:
261
- data: Bytes to pack
262
- length: Fixed length in bytes
263
-
264
- Returns:
265
- Packed bytes (null-padded or truncated if necessary)
266
- """
267
- if len(data) >= length:
268
- return data[:length]
269
- return data + b"\x00" * (length - len(data))
270
-
271
-
272
- def unpack_bytes(data: bytes, length: int, offset: int = 0) -> tuple[bytes, int]:
273
- """Unpack fixed-length byte array from bytes.
274
-
275
- Args:
276
- data: Bytes to unpack from
277
- length: Length in bytes to read
278
- offset: Offset in bytes to start unpacking
279
-
280
- Returns:
281
- Tuple of (bytes, new_offset)
282
-
283
- Raises:
284
- ValueError: If data is too short
285
- """
286
- if len(data) < offset + length:
287
- raise ValueError(
288
- f"Not enough data to unpack bytes: "
289
- f"need {offset + length} bytes, got {len(data)}"
290
- )
291
-
292
- raw_bytes = data[offset : offset + length]
293
- return raw_bytes, offset + length
294
-
295
-
296
- class FieldSerializer:
297
- """Serializer for structured fields with nested types."""
298
-
299
- def __init__(self, field_definitions: dict[str, dict[str, str]]):
300
- """Initialize serializer with field definitions.
301
-
302
- Args:
303
- field_definitions: Dict mapping field names to their structure definitions
304
- (e.g., {"HSBK": {"hue": "uint16", "saturation": "uint16", ...}})
305
- """
306
- self.field_definitions = field_definitions
307
-
308
- def pack_field(self, field_data: dict[str, Any], field_name: str) -> bytes:
309
- """Pack a structured field.
310
-
311
- Args:
312
- field_data: Dictionary of field values
313
- field_name: Name of the field structure (e.g., "HSBK")
314
-
315
- Returns:
316
- Packed bytes
317
-
318
- Raises:
319
- ValueError: If field_name is unknown
320
- """
321
- if field_name not in self.field_definitions:
322
- raise ValueError(f"Unknown field: {field_name}")
323
-
324
- field_def = self.field_definitions[field_name]
325
- result = b""
326
-
327
- for attr_name, attr_type in field_def.items():
328
- if attr_name not in field_data:
329
- raise ValueError(f"Missing attribute {attr_name} in {field_name}")
330
- result += pack_value(field_data[attr_name], attr_type)
331
-
332
- return result
333
-
334
- def unpack_field(
335
- self, data: bytes, field_name: str, offset: int = 0
336
- ) -> tuple[dict[str, Any], int]:
337
- """Unpack a structured field.
338
-
339
- Args:
340
- data: Bytes to unpack from
341
- field_name: Name of the field structure
342
- offset: Offset to start unpacking
343
-
344
- Returns:
345
- Tuple of (field_dict, new_offset)
346
-
347
- Raises:
348
- ValueError: If field_name is unknown
349
- """
350
- if field_name not in self.field_definitions:
351
- raise ValueError(f"Unknown field: {field_name}")
352
-
353
- field_def = self.field_definitions[field_name]
354
- field_data: dict[str, Any] = {}
355
- current_offset = offset
356
-
357
- for attr_name, attr_type in field_def.items():
358
- value, current_offset = unpack_value(data, attr_type, current_offset)
359
- field_data[attr_name] = value
360
-
361
- return field_data, current_offset
362
-
363
- def get_field_size(self, field_name: str) -> int:
364
- """Get the size in bytes of a field structure.
365
-
366
- Args:
367
- field_name: Name of the field structure
368
-
369
- Returns:
370
- Size in bytes
371
-
372
- Raises:
373
- ValueError: If field_name is unknown
374
- """
375
- if field_name not in self.field_definitions:
376
- raise ValueError(f"Unknown field: {field_name}")
377
-
378
- field_def = self.field_definitions[field_name]
379
- return sum(TYPE_SIZES[attr_type] for attr_type in field_def.values())
@@ -1,22 +0,0 @@
1
- """Repository interfaces and implementations for LIFX emulator.
2
-
3
- This module defines repository abstractions following the Repository Pattern
4
- and Dependency Inversion Principle. Repositories encapsulate data access logic
5
- and provide a clean separation between domain logic and data persistence.
6
- """
7
-
8
- from lifx_emulator.repositories.device_repository import (
9
- DeviceRepository,
10
- IDeviceRepository,
11
- )
12
- from lifx_emulator.repositories.storage_backend import (
13
- IDeviceStorageBackend,
14
- IScenarioStorageBackend,
15
- )
16
-
17
- __all__ = [
18
- "IDeviceRepository",
19
- "DeviceRepository",
20
- "IDeviceStorageBackend",
21
- "IScenarioStorageBackend",
22
- ]
@@ -1,155 +0,0 @@
1
- """Device repository interface and implementation.
2
-
3
- Provides abstraction for device storage and retrieval operations,
4
- following the Repository Pattern and Dependency Inversion Principle.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- from typing import Protocol, runtime_checkable
10
-
11
- from lifx_emulator.devices import EmulatedLifxDevice
12
-
13
-
14
- @runtime_checkable
15
- class IDeviceRepository(Protocol):
16
- """Interface for device repository operations.
17
-
18
- This protocol defines the contract for managing device storage and retrieval.
19
- Concrete implementations can use in-memory storage, databases, or other backends.
20
- """
21
-
22
- def add(self, device: EmulatedLifxDevice) -> bool:
23
- """Add a device to the repository.
24
-
25
- Args:
26
- device: Device to add
27
-
28
- Returns:
29
- True if device was added, False if device with same serial already exists
30
- """
31
- ...
32
-
33
- def remove(self, serial: str) -> bool:
34
- """Remove a device from the repository.
35
-
36
- Args:
37
- serial: Serial number of device to remove
38
-
39
- Returns:
40
- True if device was removed, False if not found
41
- """
42
- ...
43
-
44
- def get(self, serial: str) -> EmulatedLifxDevice | None:
45
- """Get a device by serial number.
46
-
47
- Args:
48
- serial: Serial number to look up
49
-
50
- Returns:
51
- Device if found, None otherwise
52
- """
53
- ...
54
-
55
- def get_all(self) -> list[EmulatedLifxDevice]:
56
- """Get all devices.
57
-
58
- Returns:
59
- List of all devices in the repository
60
- """
61
- ...
62
-
63
- def clear(self) -> int:
64
- """Remove all devices from the repository.
65
-
66
- Returns:
67
- Number of devices removed
68
- """
69
- ...
70
-
71
- def count(self) -> int:
72
- """Get the number of devices in the repository.
73
-
74
- Returns:
75
- Number of devices
76
- """
77
- ...
78
-
79
-
80
- class DeviceRepository:
81
- """In-memory device repository implementation.
82
-
83
- Stores devices in a dictionary keyed by serial number.
84
- This is the default implementation used by EmulatedLifxServer.
85
- """
86
-
87
- def __init__(self) -> None:
88
- """Initialize empty device repository."""
89
- self._devices: dict[str, EmulatedLifxDevice] = {}
90
-
91
- def add(self, device: EmulatedLifxDevice) -> bool:
92
- """Add a device to the repository.
93
-
94
- Args:
95
- device: Device to add
96
-
97
- Returns:
98
- True if device was added, False if device with same serial already exists
99
- """
100
- serial = device.state.serial
101
- if serial in self._devices:
102
- return False
103
- self._devices[serial] = device
104
- return True
105
-
106
- def remove(self, serial: str) -> bool:
107
- """Remove a device from the repository.
108
-
109
- Args:
110
- serial: Serial number of device to remove
111
-
112
- Returns:
113
- True if device was removed, False if not found
114
- """
115
- if serial in self._devices:
116
- del self._devices[serial]
117
- return True
118
- return False
119
-
120
- def get(self, serial: str) -> EmulatedLifxDevice | None:
121
- """Get a device by serial number.
122
-
123
- Args:
124
- serial: Serial number to look up
125
-
126
- Returns:
127
- Device if found, None otherwise
128
- """
129
- return self._devices.get(serial)
130
-
131
- def get_all(self) -> list[EmulatedLifxDevice]:
132
- """Get all devices.
133
-
134
- Returns:
135
- List of all devices in the repository
136
- """
137
- return list(self._devices.values())
138
-
139
- def clear(self) -> int:
140
- """Remove all devices from the repository.
141
-
142
- Returns:
143
- Number of devices removed
144
- """
145
- count = len(self._devices)
146
- self._devices.clear()
147
- return count
148
-
149
- def count(self) -> int:
150
- """Get the number of devices in the repository.
151
-
152
- Returns:
153
- Number of devices
154
- """
155
- return len(self._devices)
@@ -1,107 +0,0 @@
1
- """Storage backend interfaces for device and scenario persistence.
2
-
3
- Provides abstraction for persistent storage operations,
4
- following the Repository Pattern and Dependency Inversion Principle.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
10
-
11
- if TYPE_CHECKING:
12
- from lifx_emulator.scenarios import HierarchicalScenarioManager
13
-
14
-
15
- @runtime_checkable
16
- class IDeviceStorageBackend(Protocol):
17
- """Interface for device state persistence operations.
18
-
19
- This protocol defines the contract for loading and saving device state.
20
- Concrete implementations can use async file I/O, databases,
21
- or other storage backends.
22
- """
23
-
24
- async def save_device_state(self, device_state: Any) -> None:
25
- """Save device state to persistent storage (async).
26
-
27
- Args:
28
- device_state: DeviceState instance to persist
29
- """
30
- raise NotImplementedError
31
-
32
- def load_device_state(self, serial: str) -> dict | None:
33
- """Load device state from persistent storage (sync).
34
-
35
- Args:
36
- serial: Device serial number (12-character hex string)
37
-
38
- Returns:
39
- Dictionary with device state data, or None if not found
40
- """
41
- raise NotImplementedError
42
-
43
- def delete_device_state(self, serial: str) -> bool:
44
- """Delete device state from persistent storage.
45
-
46
- Args:
47
- serial: Device serial number
48
-
49
- Returns:
50
- True if state was deleted, False if not found
51
- """
52
- raise NotImplementedError
53
-
54
- def list_devices(self) -> list[str]:
55
- """List all device serials with saved state.
56
-
57
- Returns:
58
- List of serial numbers
59
- """
60
- raise NotImplementedError
61
-
62
- def delete_all_device_states(self) -> int:
63
- """Delete all device states from persistent storage.
64
-
65
- Returns:
66
- Number of device states deleted
67
- """
68
- raise NotImplementedError
69
-
70
- async def shutdown(self) -> None:
71
- """Gracefully shutdown storage backend, flushing pending writes."""
72
- raise NotImplementedError
73
-
74
-
75
- @runtime_checkable
76
- class IScenarioStorageBackend(Protocol):
77
- """Interface for scenario configuration persistence operations.
78
-
79
- This protocol defines the contract for loading and saving scenario configurations.
80
- Concrete implementations can use async file I/O, databases,
81
- or other storage backends.
82
- """
83
-
84
- async def load(self) -> HierarchicalScenarioManager:
85
- """Load scenario configuration from persistent storage (async).
86
-
87
- Returns:
88
- Scenario manager with loaded configuration, or default manager
89
- if no saved data
90
- """
91
- raise NotImplementedError
92
-
93
- async def save(self, manager: HierarchicalScenarioManager) -> None:
94
- """Save scenario configuration to persistent storage (async).
95
-
96
- Args:
97
- manager: Scenario manager whose configuration should be saved
98
- """
99
- raise NotImplementedError
100
-
101
- async def delete(self) -> bool:
102
- """Delete scenario configuration from persistent storage (async).
103
-
104
- Returns:
105
- True if configuration was deleted, False if it didn't exist
106
- """
107
- raise NotImplementedError
@@ -1,22 +0,0 @@
1
- """Scenario management module for LIFX emulator.
2
-
3
- This module contains all scenario-related functionality including:
4
- - Scenario manager (HierarchicalScenarioManager)
5
- - Scenario models (ScenarioConfig)
6
- - Scenario persistence (async file storage)
7
- - Device type classification (get_device_type)
8
- """
9
-
10
- from lifx_emulator.scenarios.manager import (
11
- HierarchicalScenarioManager,
12
- get_device_type,
13
- )
14
- from lifx_emulator.scenarios.models import ScenarioConfig
15
- from lifx_emulator.scenarios.persistence import ScenarioPersistenceAsyncFile
16
-
17
- __all__ = [
18
- "HierarchicalScenarioManager",
19
- "ScenarioConfig",
20
- "ScenarioPersistenceAsyncFile",
21
- "get_device_type",
22
- ]