lifx-emulator 1.0.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 (40) hide show
  1. lifx_emulator/__init__.py +31 -0
  2. lifx_emulator/__main__.py +607 -0
  3. lifx_emulator/api.py +1825 -0
  4. lifx_emulator/async_storage.py +308 -0
  5. lifx_emulator/constants.py +33 -0
  6. lifx_emulator/device.py +750 -0
  7. lifx_emulator/device_states.py +114 -0
  8. lifx_emulator/factories.py +380 -0
  9. lifx_emulator/handlers/__init__.py +39 -0
  10. lifx_emulator/handlers/base.py +49 -0
  11. lifx_emulator/handlers/device_handlers.py +340 -0
  12. lifx_emulator/handlers/light_handlers.py +372 -0
  13. lifx_emulator/handlers/multizone_handlers.py +249 -0
  14. lifx_emulator/handlers/registry.py +110 -0
  15. lifx_emulator/handlers/tile_handlers.py +309 -0
  16. lifx_emulator/observers.py +139 -0
  17. lifx_emulator/products/__init__.py +28 -0
  18. lifx_emulator/products/generator.py +771 -0
  19. lifx_emulator/products/registry.py +1446 -0
  20. lifx_emulator/products/specs.py +242 -0
  21. lifx_emulator/products/specs.yml +327 -0
  22. lifx_emulator/protocol/__init__.py +1 -0
  23. lifx_emulator/protocol/base.py +334 -0
  24. lifx_emulator/protocol/const.py +8 -0
  25. lifx_emulator/protocol/generator.py +1371 -0
  26. lifx_emulator/protocol/header.py +159 -0
  27. lifx_emulator/protocol/packets.py +1351 -0
  28. lifx_emulator/protocol/protocol_types.py +844 -0
  29. lifx_emulator/protocol/serializer.py +379 -0
  30. lifx_emulator/scenario_manager.py +402 -0
  31. lifx_emulator/scenario_persistence.py +206 -0
  32. lifx_emulator/server.py +482 -0
  33. lifx_emulator/state_restorer.py +259 -0
  34. lifx_emulator/state_serializer.py +130 -0
  35. lifx_emulator/storage_protocol.py +100 -0
  36. lifx_emulator-1.0.0.dist-info/METADATA +445 -0
  37. lifx_emulator-1.0.0.dist-info/RECORD +40 -0
  38. lifx_emulator-1.0.0.dist-info/WHEEL +4 -0
  39. lifx_emulator-1.0.0.dist-info/entry_points.txt +2 -0
  40. lifx_emulator-1.0.0.dist-info/licenses/LICENSE +35 -0
@@ -0,0 +1,242 @@
1
+ """Product specs and defaults for LIFX devices.
2
+
3
+ This module provides access to product-specific details that are not available
4
+ in the upstream LIFX products.json catalog, such as default zone counts and
5
+ tile configurations.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+
13
+ import yaml
14
+
15
+
16
+ @dataclass
17
+ class ProductSpecs:
18
+ """Product-specific specs and defaults.
19
+
20
+ Attributes:
21
+ product_id: Product ID this applies to
22
+ default_zone_count: Default number of zones for multizone devices
23
+ min_zone_count: Minimum zones supported
24
+ max_zone_count: Maximum zones supported
25
+ default_tile_count: Default number of tiles in chain
26
+ min_tile_count: Minimum tiles supported
27
+ max_tile_count: Maximum tiles supported
28
+ tile_width: Width of each tile in pixels
29
+ tile_height: Height of each tile in pixels
30
+ notes: Human-readable notes about this product
31
+ """
32
+
33
+ product_id: int
34
+ default_zone_count: int | None = None
35
+ min_zone_count: int | None = None
36
+ max_zone_count: int | None = None
37
+ default_tile_count: int | None = None
38
+ min_tile_count: int | None = None
39
+ max_tile_count: int | None = None
40
+ tile_width: int | None = None
41
+ tile_height: int | None = None
42
+ notes: str | None = None
43
+
44
+ @property
45
+ def has_multizone_specs(self) -> bool:
46
+ """Check if this product has multizone-specific specs."""
47
+ return self.default_zone_count is not None
48
+
49
+ @property
50
+ def has_matrix_specs(self) -> bool:
51
+ """Check if this product has matrix-specific specs."""
52
+ return self.tile_width is not None or self.tile_height is not None
53
+
54
+
55
+ class SpecsRegistry:
56
+ """Registry of product specs loaded from specs.yml."""
57
+
58
+ def __init__(self) -> None:
59
+ """Initialize empty specs registry."""
60
+ self._specs: dict[int, ProductSpecs] = {}
61
+ self._loaded = False
62
+
63
+ def load_from_file(self, specs_path: Path | None = None) -> None:
64
+ """Load specs from YAML file.
65
+
66
+ Args:
67
+ specs_path: Path to specs.yml file. If None, uses default location.
68
+ """
69
+ if specs_path is None:
70
+ specs_path = Path(__file__).parent / "specs.yml"
71
+
72
+ if not specs_path.exists():
73
+ # No specs file, that's okay - we'll use registry defaults only
74
+ self._loaded = True
75
+ return
76
+
77
+ with open(specs_path) as f:
78
+ data = yaml.safe_load(f)
79
+
80
+ if not data or "products" not in data:
81
+ self._loaded = True
82
+ return
83
+
84
+ # Parse product specs
85
+ for pid, specs_data in data["products"].items():
86
+ if isinstance(specs_data, dict):
87
+ self._specs[int(pid)] = ProductSpecs(
88
+ product_id=int(pid),
89
+ default_zone_count=specs_data.get("default_zone_count"),
90
+ min_zone_count=specs_data.get("min_zone_count"),
91
+ max_zone_count=specs_data.get("max_zone_count"),
92
+ default_tile_count=specs_data.get("default_tile_count"),
93
+ min_tile_count=specs_data.get("min_tile_count"),
94
+ max_tile_count=specs_data.get("max_tile_count"),
95
+ tile_width=specs_data.get("tile_width"),
96
+ tile_height=specs_data.get("tile_height"),
97
+ notes=specs_data.get("notes"),
98
+ )
99
+
100
+ self._loaded = True
101
+
102
+ @property
103
+ def is_loaded(self) -> bool:
104
+ """Check if specs have been loaded."""
105
+ return self._loaded
106
+
107
+ def get_specs(self, product_id: int) -> ProductSpecs | None:
108
+ """Get specs for a specific product.
109
+
110
+ Args:
111
+ product_id: Product ID to look up
112
+
113
+ Returns:
114
+ ProductSpecs if found, None otherwise
115
+ """
116
+ if not self._loaded:
117
+ self.load_from_file()
118
+
119
+ return self._specs.get(product_id)
120
+
121
+ def has_specs(self, product_id: int) -> bool:
122
+ """Check if a product has specs defined.
123
+
124
+ Args:
125
+ product_id: Product ID to check
126
+
127
+ Returns:
128
+ True if specs exist for this product
129
+ """
130
+ if not self._loaded:
131
+ self.load_from_file()
132
+
133
+ return product_id in self._specs
134
+
135
+ def get_default_zone_count(self, product_id: int) -> int | None:
136
+ """Get default zone count for a multizone product.
137
+
138
+ Args:
139
+ product_id: Product ID
140
+
141
+ Returns:
142
+ Default zone count if defined, None otherwise
143
+ """
144
+ specs = self.get_specs(product_id)
145
+ return specs.default_zone_count if specs else None
146
+
147
+ def get_default_tile_count(self, product_id: int) -> int | None:
148
+ """Get default tile count for a matrix product.
149
+
150
+ Args:
151
+ product_id: Product ID
152
+
153
+ Returns:
154
+ Default tile count if defined, None otherwise
155
+ """
156
+ specs = self.get_specs(product_id)
157
+ return specs.default_tile_count if specs else None
158
+
159
+ def get_tile_dimensions(self, product_id: int) -> tuple[int, int] | None:
160
+ """Get tile dimensions for a matrix product.
161
+
162
+ Args:
163
+ product_id: Product ID
164
+
165
+ Returns:
166
+ Tuple of (width, height) if defined, None otherwise
167
+ """
168
+ specs = self.get_specs(product_id)
169
+ if specs and specs.tile_width and specs.tile_height:
170
+ return (specs.tile_width, specs.tile_height)
171
+ return None
172
+
173
+ def __len__(self) -> int:
174
+ """Get number of products with specs."""
175
+ if not self._loaded:
176
+ self.load_from_file()
177
+ return len(self._specs)
178
+
179
+ def __contains__(self, product_id: int) -> bool:
180
+ """Check if product has specs."""
181
+ return self.has_specs(product_id)
182
+
183
+
184
+ # Global specs registry instance
185
+ _specs_registry = SpecsRegistry()
186
+
187
+
188
+ def get_specs_registry() -> SpecsRegistry:
189
+ """Get the global specs registry.
190
+
191
+ Returns:
192
+ Global SpecsRegistry instance
193
+ """
194
+ return _specs_registry
195
+
196
+
197
+ def get_specs(product_id: int) -> ProductSpecs | None:
198
+ """Get specs for a specific product.
199
+
200
+ Args:
201
+ product_id: Product ID to look up
202
+
203
+ Returns:
204
+ ProductSpecs if found, None otherwise
205
+ """
206
+ return _specs_registry.get_specs(product_id)
207
+
208
+
209
+ def get_default_zone_count(product_id: int) -> int | None:
210
+ """Get default zone count for a multizone product.
211
+
212
+ Args:
213
+ product_id: Product ID
214
+
215
+ Returns:
216
+ Default zone count if defined, None otherwise
217
+ """
218
+ return _specs_registry.get_default_zone_count(product_id)
219
+
220
+
221
+ def get_default_tile_count(product_id: int) -> int | None:
222
+ """Get default tile count for a matrix product.
223
+
224
+ Args:
225
+ product_id: Product ID
226
+
227
+ Returns:
228
+ Default tile count if defined, None otherwise
229
+ """
230
+ return _specs_registry.get_default_tile_count(product_id)
231
+
232
+
233
+ def get_tile_dimensions(product_id: int) -> tuple[int, int] | None:
234
+ """Get tile dimensions for a matrix product.
235
+
236
+ Args:
237
+ product_id: Product ID
238
+
239
+ Returns:
240
+ Tuple of (width, height) if defined, None otherwise
241
+ """
242
+ return _specs_registry.get_tile_dimensions(product_id)
@@ -0,0 +1,327 @@
1
+ # LIFX Product Specs and Defaults
2
+ # =================================
3
+ #
4
+ # This file contains product-specific details that are not available in the
5
+ # upstream LIFX products.json catalog, such as default zone counts, tile
6
+ # configurations, and other device-specific defaults.
7
+ #
8
+ # These values are used by the emulator to create realistic device
9
+ # configurations when specific parameters are not provided by the user.
10
+ #
11
+ # Format:
12
+ # -------
13
+ # products:
14
+ # <product_id>:
15
+ # # For multizone devices
16
+ # default_zone_count: <number> # Default zones (e.g., 16)
17
+ # min_zone_count: <number> # Minimum zones supported
18
+ # max_zone_count: <number> # Maximum zones supported
19
+ #
20
+ # # For matrix devices (tiles, candles, etc.)
21
+ # default_tile_count: <number> # Default number of tiles in chain
22
+ # min_tile_count: <number> # Minimum tiles supported
23
+ # max_tile_count: <number> # Maximum tiles supported
24
+ # tile_width: <number> # Width of each tile in pixels
25
+ # tile_height: <number> # Height of each tile in pixels
26
+ #
27
+ # # Other device-specific defaults
28
+ # notes: "<string>" # Notes about product
29
+
30
+ products:
31
+ # ========================================
32
+ # Multizone Products (Linear Strips)
33
+ # ========================================
34
+
35
+ 31: # Original LIFX Z strip, 2 meters, 8 zones per meter
36
+ default_zone_count: 16
37
+ min_zone_count: 8
38
+ max_zone_count: 16
39
+ notes: Original LIFX Z strip, 2 meters, 8 zones per meter
40
+
41
+ 32: # LIFX Z with extended multizone firmware support
42
+ default_zone_count: 16
43
+ min_zone_count: 8
44
+ max_zone_count: 16
45
+ notes: LIFX Z with extended multizone firmware support
46
+
47
+ 38: # LIFX Beam, 8 individual beams, each with 10 zones and up to 2 corners
48
+ default_zone_count: 80
49
+ min_zone_count: 10
50
+ max_zone_count: 82
51
+ notes: LIFX Beam, 8 individual beams, each with 10 zones and up to 2 corners
52
+
53
+ 117: # LIFX Z US variant
54
+ default_zone_count: 16
55
+ min_zone_count: 8
56
+ max_zone_count: 16
57
+ notes: LIFX Z US variant
58
+
59
+ 118: # LIFX Z International variant
60
+ default_zone_count: 16
61
+ min_zone_count: 8
62
+ max_zone_count: 16
63
+ notes: LIFX Z International variant
64
+
65
+ 119: # LIFX Beam US variant
66
+ default_zone_count: 80
67
+ min_zone_count: 10
68
+ max_zone_count: 82
69
+ notes: LIFX Beam US variant
70
+
71
+ 120: # LIFX Beam International variant
72
+ default_zone_count: 80
73
+ min_zone_count: 10
74
+ max_zone_count: 82
75
+ notes: LIFX Beam International variant
76
+
77
+ 141: # LIFX Neon flexible light strip
78
+ default_zone_count: 24
79
+ min_zone_count: 24
80
+ max_zone_count: 48
81
+ notes: LIFX Neon flexible light strip
82
+
83
+ 142: # LIFX Neon International variant
84
+ default_zone_count: 24
85
+ min_zone_count: 24
86
+ max_zone_count: 48
87
+ notes: LIFX Neon International variant
88
+
89
+ 143: # LIFX String lights
90
+ default_zone_count: 36
91
+ min_zone_count: 36
92
+ max_zone_count: 72
93
+ notes: LIFX String lights
94
+
95
+ 144: # LIFX String International variant
96
+ default_zone_count: 20
97
+ min_zone_count: 36
98
+ max_zone_count: 72
99
+ notes: LIFX String International variant
100
+
101
+ 161: # LIFX Outdoor Neon
102
+ default_zone_count: 60
103
+ min_zone_count: 60
104
+ max_zone_count: 120
105
+ notes: LIFX Outdoor Neon
106
+
107
+ 162: # LIFX Outdoor Neon International variant
108
+ default_zone_count: 60
109
+ min_zone_count: 60
110
+ max_zone_count: 120
111
+ notes: LIFX Outdoor Neon International variant
112
+
113
+ 203: # LIFX String US (newer model)
114
+ default_zone_count: 36
115
+ min_zone_count: 36
116
+ max_zone_count: 72
117
+ notes: LIFX String US (newer model)
118
+
119
+ 204: # LIFX String International (newer model)
120
+ default_zone_count: 36
121
+ min_zone_count: 36
122
+ max_zone_count: 72
123
+ notes: LIFX String International (newer model)
124
+
125
+ 205: # LIFX Indoor Neon US
126
+ default_zone_count: 24
127
+ min_zone_count: 24
128
+ max_zone_count: 48
129
+ notes: LIFX Indoor Neon US
130
+
131
+ 206: # LIFX Indoor Neon Intl
132
+ default_zone_count: 24
133
+ min_zone_count: 24
134
+ max_zone_count: 48
135
+ notes: LIFX Indoor Neon Intl
136
+
137
+ 213: # LIFX Permanent Outdoor US
138
+ default_zone_count: 30
139
+ min_zone_count: 30
140
+ max_zone_count: 120
141
+ notes: LIFX Permanent Outdoor US
142
+
143
+ 214: # LIFX Permanent Outdoor Intl
144
+ default_zone_count: 30
145
+ min_zone_count: 30
146
+ max_zone_count: 120
147
+ notes: LIFX Permanent Outdoor Intl
148
+
149
+ # ========================================
150
+ # Matrix Products (Tiles, Candles, etc.)
151
+ # ========================================
152
+
153
+ 55: # LIFX Tile, 8x8 pixel matrix, chainable up to 5
154
+ default_tile_count: 5
155
+ min_tile_count: 1
156
+ max_tile_count: 5
157
+ tile_width: 8
158
+ tile_height: 8
159
+ notes: LIFX Tile, 8x8 pixel matrix, chainable up to 5
160
+
161
+ 57: # LIFX Candle, 5x6 pixel matrix, single unit
162
+ default_tile_count: 1
163
+ min_tile_count: 1
164
+ max_tile_count: 1
165
+ tile_width: 5
166
+ tile_height: 6
167
+ notes: LIFX Candle, 5x6 pixel matrix, single unit
168
+
169
+ 68: # LIFX Candle variant, 5x6 pixel matrix
170
+ default_tile_count: 1
171
+ min_tile_count: 1
172
+ max_tile_count: 1
173
+ tile_width: 5
174
+ tile_height: 6
175
+ notes: LIFX Candle variant, 5x6 pixel matrix
176
+
177
+ 137: # LIFX Candle Color US 5x6 variant
178
+ default_tile_count: 1
179
+ min_tile_count: 1
180
+ max_tile_count: 1
181
+ tile_width: 5
182
+ tile_height: 6
183
+ notes: LIFX Candle Color US 5x6 variant
184
+
185
+ 138: # LIFX Candle Colour 5x6 International variant
186
+ default_tile_count: 1
187
+ min_tile_count: 1
188
+ max_tile_count: 1
189
+ tile_width: 5
190
+ tile_height: 6
191
+ notes: LIFX Candle Colour 5x6 International variant
192
+
193
+ 171: # LIFX Round Spot, 3x1 matrix
194
+ default_tile_count: 1
195
+ min_tile_count: 1
196
+ max_tile_count: 1
197
+ tile_width: 3
198
+ tile_height: 1
199
+ notes: LIFX Round Spot, 3x1 matrix
200
+
201
+ 173: # LIFX Round Path, 3x2 matrix
202
+ default_tile_count: 1
203
+ min_tile_count: 1
204
+ max_tile_count: 1
205
+ tile_width: 3
206
+ tile_height: 2
207
+ notes: LIFX Round Path, 3x2 matrix
208
+
209
+ 174: # LIFX Square Path, 3x2 matrix
210
+ default_tile_count: 1
211
+ min_tile_count: 1
212
+ max_tile_count: 1
213
+ tile_width: 3
214
+ tile_height: 2
215
+ notes: LIFX Square Path, 3x2 matrix
216
+
217
+ 176: # LIFX Ceiling, 8x8 matrix, zones 1-63: downlight, zone 64: uplight
218
+ default_tile_count: 1
219
+ min_tile_count: 1
220
+ max_tile_count: 1
221
+ tile_width: 8
222
+ tile_height: 8
223
+ notes: 'LIFX Ceiling, 8x8 matrix, zones 1-63: downlight, zone 64: uplight'
224
+
225
+ 177: # LIFX Tube Intl
226
+ default_tile_count: 1
227
+ min_tile_count: 1
228
+ max_tile_count: 1
229
+ tile_width: 5
230
+ tile_height: 11
231
+ notes: LIFX Tube Intl
232
+
233
+ 185: # LIFX Candle Color US
234
+ default_tile_count: 1
235
+ min_tile_count: 1
236
+ max_tile_count: 1
237
+ tile_width: 5
238
+ tile_height: 6
239
+ notes: LIFX Candle Color US
240
+
241
+ 186: # LIFX Candle Colour Intl
242
+ default_tile_count: 1
243
+ min_tile_count: 1
244
+ max_tile_count: 1
245
+ tile_width: 5
246
+ tile_height: 6
247
+ notes: LIFX Candle Colour Intl
248
+
249
+ 201: # LIFX Ceiling 13x26" US
250
+ default_tile_count: 1
251
+ min_tile_count: 1
252
+ max_tile_count: 1
253
+ tile_width: 16
254
+ tile_height: 8
255
+ notes: LIFX Ceiling 13x26" US
256
+
257
+ 202: # LIFX Ceiling 13x26" Intl
258
+ default_tile_count: 1
259
+ min_tile_count: 1
260
+ max_tile_count: 1
261
+ tile_width: 16
262
+ tile_height: 8
263
+ notes: LIFX Ceiling 13x26" Intl
264
+
265
+ 215: # LIFX Candle Color US
266
+ default_tile_count: 1
267
+ min_tile_count: 1
268
+ max_tile_count: 1
269
+ tile_width: 5
270
+ tile_height: 6
271
+ notes: LIFX Candle Color US
272
+
273
+ 216: # LIFX Candle Colour Intl
274
+ default_tile_count: 1
275
+ min_tile_count: 1
276
+ max_tile_count: 1
277
+ tile_width: 5
278
+ tile_height: 6
279
+ notes: LIFX Candle Colour Intl
280
+
281
+ 217: # LIFX Tube US
282
+ default_tile_count: 1
283
+ min_tile_count: 1
284
+ max_tile_count: 1
285
+ tile_width: 5
286
+ tile_height: 11
287
+ notes: LIFX Tube US
288
+
289
+ 218: # LIFX Tube Intl
290
+ default_tile_count: 1
291
+ min_tile_count: 1
292
+ max_tile_count: 1
293
+ tile_width: 5
294
+ tile_height: 11
295
+ notes: LIFX Tube Intl
296
+
297
+ 219: # LIFX Luna US
298
+ default_tile_count: 1
299
+ min_tile_count: 1
300
+ max_tile_count: 1
301
+ tile_width: 7
302
+ tile_height: 5
303
+ notes: LIFX Luna US
304
+
305
+ 220: # LIFX Luna Intl
306
+ default_tile_count: 1
307
+ min_tile_count: 1
308
+ max_tile_count: 1
309
+ tile_width: 7
310
+ tile_height: 5
311
+ notes: LIFX Luna Intl
312
+
313
+ 221: # LIFX Round Spot Intl
314
+ default_tile_count: 1
315
+ min_tile_count: 1
316
+ max_tile_count: 1
317
+ tile_width: 3
318
+ tile_height: 1
319
+ notes: LIFX Round Spot Intl
320
+
321
+ 222: # LIFX Round Path Intl
322
+ default_tile_count: 1
323
+ min_tile_count: 1
324
+ max_tile_count: 1
325
+ tile_width: 3
326
+ tile_height: 2
327
+ notes: LIFX Round Path Intl
@@ -0,0 +1 @@
1
+ """LIFX protocol implementation and code generation."""