lifx-emulator 2.3.1__py3-none-any.whl → 2.4.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.
- lifx_emulator/__main__.py +11 -2
- lifx_emulator/devices/device.py +56 -0
- lifx_emulator/devices/states.py +4 -0
- lifx_emulator/factories/__init__.py +2 -0
- lifx_emulator/factories/builder.py +2 -0
- lifx_emulator/factories/factory.py +31 -0
- lifx_emulator/products/generator.py +75 -33
- lifx_emulator/products/registry.py +46 -12
- lifx_emulator/products/specs.yml +38 -4
- {lifx_emulator-2.3.1.dist-info → lifx_emulator-2.4.0.dist-info}/METADATA +1 -1
- {lifx_emulator-2.3.1.dist-info → lifx_emulator-2.4.0.dist-info}/RECORD +14 -14
- {lifx_emulator-2.3.1.dist-info → lifx_emulator-2.4.0.dist-info}/WHEEL +0 -0
- {lifx_emulator-2.3.1.dist-info → lifx_emulator-2.4.0.dist-info}/entry_points.txt +0 -0
- {lifx_emulator-2.3.1.dist-info → lifx_emulator-2.4.0.dist-info}/licenses/LICENSE +0 -0
lifx_emulator/__main__.py
CHANGED
|
@@ -21,6 +21,7 @@ from lifx_emulator.factories import (
|
|
|
21
21
|
create_hev_light,
|
|
22
22
|
create_infrared_light,
|
|
23
23
|
create_multizone_light,
|
|
24
|
+
create_switch,
|
|
24
25
|
create_tile_device,
|
|
25
26
|
)
|
|
26
27
|
from lifx_emulator.products.registry import get_registry
|
|
@@ -239,6 +240,7 @@ async def run(
|
|
|
239
240
|
hev: Annotated[int, cyclopts.Parameter(group=device_group)] = 0,
|
|
240
241
|
multizone: Annotated[int, cyclopts.Parameter(group=device_group)] = 0,
|
|
241
242
|
tile: Annotated[int, cyclopts.Parameter(group=device_group)] = 0,
|
|
243
|
+
switch: Annotated[int, cyclopts.Parameter(group=device_group)] = 0,
|
|
242
244
|
# Multizone Options
|
|
243
245
|
multizone_zones: Annotated[
|
|
244
246
|
int | None, cyclopts.Parameter(group=multizone_group)
|
|
@@ -284,6 +286,7 @@ async def run(
|
|
|
284
286
|
multizone_extended: Enable extended multizone support (Beam).
|
|
285
287
|
Set --no-multizone-extended for basic multizone (Z) devices.
|
|
286
288
|
tile: Number of tile/matrix chain devices.
|
|
289
|
+
switch: Number of LIFX Switch devices (relays, no lighting).
|
|
287
290
|
tile_count: Number of tiles per device. Uses product defaults if not
|
|
288
291
|
specified (5 for Tile, 1 for Candle/Ceiling).
|
|
289
292
|
tile_width: Width of each tile in zones. Uses product defaults if not
|
|
@@ -310,7 +313,7 @@ async def run(
|
|
|
310
313
|
lifx-emulator --color 2 --multizone 1 --tile 1 --api --verbose
|
|
311
314
|
|
|
312
315
|
Create only specific device types:
|
|
313
|
-
lifx-emulator --color 0 --infrared 3 --hev 2
|
|
316
|
+
lifx-emulator --color 0 --infrared 3 --hev 2 --switch 2
|
|
314
317
|
|
|
315
318
|
Custom serial prefix:
|
|
316
319
|
lifx-emulator --serial-prefix cafe00 --color 5
|
|
@@ -410,6 +413,7 @@ async def run(
|
|
|
410
413
|
and infrared == 0
|
|
411
414
|
and hev == 0
|
|
412
415
|
and multizone == 0
|
|
416
|
+
and switch == 0
|
|
413
417
|
):
|
|
414
418
|
color = 0
|
|
415
419
|
|
|
@@ -423,6 +427,7 @@ async def run(
|
|
|
423
427
|
and hev == 0
|
|
424
428
|
and multizone == 0
|
|
425
429
|
and tile == 0
|
|
430
|
+
and switch == 0
|
|
426
431
|
):
|
|
427
432
|
color = 0
|
|
428
433
|
|
|
@@ -467,13 +472,17 @@ async def run(
|
|
|
467
472
|
)
|
|
468
473
|
)
|
|
469
474
|
|
|
475
|
+
# Create switch devices
|
|
476
|
+
for _ in range(switch):
|
|
477
|
+
devices.append(create_switch(get_serial(), storage=storage))
|
|
478
|
+
|
|
470
479
|
if not devices:
|
|
471
480
|
if persistent:
|
|
472
481
|
logger.warning("No devices configured. Server will run with no devices.")
|
|
473
482
|
logger.info("Use API (--api) or restart with device flags to add devices.")
|
|
474
483
|
else:
|
|
475
484
|
logger.error(
|
|
476
|
-
"No devices configured. Use --color, --multizone, --tile, "
|
|
485
|
+
"No devices configured. Use --color, --multizone, --tile, --switch, "
|
|
477
486
|
"etc. to add devices."
|
|
478
487
|
)
|
|
479
488
|
return
|
lifx_emulator/devices/device.py
CHANGED
|
@@ -215,6 +215,35 @@ class EmulatedLifxDevice:
|
|
|
215
215
|
header.size = LIFX_HEADER_SIZE + payload_size
|
|
216
216
|
return header
|
|
217
217
|
|
|
218
|
+
def _should_handle_packet(self, pkt_type: int) -> bool:
|
|
219
|
+
"""Check if device should handle a packet type based on capabilities.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
pkt_type: Packet type number
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
True if device should handle, False if should return StateUnhandled
|
|
226
|
+
"""
|
|
227
|
+
# Device.* packets are always handled (2-59)
|
|
228
|
+
if 2 <= pkt_type <= 59:
|
|
229
|
+
return True
|
|
230
|
+
|
|
231
|
+
# Light.* packets (101-149) require light capabilities
|
|
232
|
+
# Switches (devices with relays) don't support light operations
|
|
233
|
+
if 101 <= pkt_type <= 149:
|
|
234
|
+
return not self.state.has_relays
|
|
235
|
+
|
|
236
|
+
# MultiZone.* packets (501-512) require multizone capability
|
|
237
|
+
if 501 <= pkt_type <= 512:
|
|
238
|
+
return self.state.has_multizone
|
|
239
|
+
|
|
240
|
+
# Tile.* packets (701-720) require matrix capability
|
|
241
|
+
if 701 <= pkt_type <= 720:
|
|
242
|
+
return self.state.has_matrix
|
|
243
|
+
|
|
244
|
+
# Unknown packets - let handler decide
|
|
245
|
+
return True
|
|
246
|
+
|
|
218
247
|
def process_packet(
|
|
219
248
|
self, header: LifxHeader, packet: Any | None
|
|
220
249
|
) -> list[tuple[LifxHeader, Any]]:
|
|
@@ -229,6 +258,33 @@ class EmulatedLifxDevice:
|
|
|
229
258
|
logger.info("Dropping packet type %s per scenario", header.pkt_type)
|
|
230
259
|
return responses
|
|
231
260
|
|
|
261
|
+
# Check if device should handle this packet type (capability-based filtering)
|
|
262
|
+
if not self._should_handle_packet(header.pkt_type):
|
|
263
|
+
# Return StateUnhandled for unsupported packet types
|
|
264
|
+
state_unhandled = Device.StateUnhandled(unhandled_type=header.pkt_type)
|
|
265
|
+
unhandled_payload = state_unhandled.pack()
|
|
266
|
+
unhandled_header = self._create_response_header(
|
|
267
|
+
header.source,
|
|
268
|
+
header.sequence,
|
|
269
|
+
state_unhandled.PKT_TYPE,
|
|
270
|
+
len(unhandled_payload),
|
|
271
|
+
)
|
|
272
|
+
responses.append((unhandled_header, state_unhandled))
|
|
273
|
+
|
|
274
|
+
# Still send acknowledgment if requested
|
|
275
|
+
if header.ack_required:
|
|
276
|
+
ack_packet = Device.Acknowledgement()
|
|
277
|
+
ack_payload = ack_packet.pack()
|
|
278
|
+
ack_header = self._create_response_header(
|
|
279
|
+
header.source,
|
|
280
|
+
header.sequence,
|
|
281
|
+
ack_packet.PKT_TYPE,
|
|
282
|
+
len(ack_payload),
|
|
283
|
+
)
|
|
284
|
+
responses.append((ack_header, ack_packet))
|
|
285
|
+
|
|
286
|
+
return responses
|
|
287
|
+
|
|
232
288
|
# Update uptime
|
|
233
289
|
self.state.uptime_ns = self.get_uptime_ns()
|
|
234
290
|
|
lifx_emulator/devices/states.py
CHANGED
|
@@ -183,6 +183,8 @@ class DeviceState:
|
|
|
183
183
|
has_extended_multizone: bool = False
|
|
184
184
|
has_matrix: bool = False
|
|
185
185
|
has_hev: bool = False
|
|
186
|
+
has_relays: bool = False
|
|
187
|
+
has_buttons: bool = False
|
|
186
188
|
|
|
187
189
|
# Attribute routing map: maps attribute prefixes to state objects
|
|
188
190
|
# This eliminates ~360 lines of property boilerplate
|
|
@@ -347,6 +349,8 @@ class DeviceState:
|
|
|
347
349
|
"has_extended_multizone",
|
|
348
350
|
"has_matrix",
|
|
349
351
|
"has_hev",
|
|
352
|
+
"has_relays",
|
|
353
|
+
"has_buttons",
|
|
350
354
|
} or name.startswith("_"):
|
|
351
355
|
object.__setattr__(self, name, value)
|
|
352
356
|
return
|
|
@@ -15,6 +15,7 @@ from lifx_emulator.factories.factory import (
|
|
|
15
15
|
create_hev_light,
|
|
16
16
|
create_infrared_light,
|
|
17
17
|
create_multizone_light,
|
|
18
|
+
create_switch,
|
|
18
19
|
create_tile_device,
|
|
19
20
|
)
|
|
20
21
|
from lifx_emulator.factories.firmware_config import FirmwareConfig
|
|
@@ -32,6 +33,7 @@ __all__ = [
|
|
|
32
33
|
"create_infrared_light",
|
|
33
34
|
"create_hev_light",
|
|
34
35
|
"create_multizone_light",
|
|
36
|
+
"create_switch",
|
|
35
37
|
"create_tile_device",
|
|
36
38
|
"create_color_temperature_light",
|
|
37
39
|
]
|
|
@@ -257,6 +257,8 @@ class DeviceBuilder:
|
|
|
257
257
|
has_extended_multizone=has_extended_multizone,
|
|
258
258
|
has_matrix=self._product_info.has_matrix,
|
|
259
259
|
has_hev=self._product_info.has_hev,
|
|
260
|
+
has_relays=self._product_info.has_relays,
|
|
261
|
+
has_buttons=self._product_info.has_buttons,
|
|
260
262
|
)
|
|
261
263
|
|
|
262
264
|
# 10. Restore saved state if persistence enabled
|
|
@@ -141,6 +141,37 @@ def create_color_temperature_light(
|
|
|
141
141
|
) # LIFX Mini White to Warm
|
|
142
142
|
|
|
143
143
|
|
|
144
|
+
def create_switch(
|
|
145
|
+
serial: str | None = None,
|
|
146
|
+
product_id: int = 70,
|
|
147
|
+
firmware_version: tuple[int, int] | None = None,
|
|
148
|
+
storage: DevicePersistenceAsyncFile | None = None,
|
|
149
|
+
scenario_manager: HierarchicalScenarioManager | None = None,
|
|
150
|
+
) -> EmulatedLifxDevice:
|
|
151
|
+
"""Create a LIFX Switch device.
|
|
152
|
+
|
|
153
|
+
Switches have has_relays and has_buttons capabilities but no lighting.
|
|
154
|
+
They respond with StateUnhandled (223) to Light, MultiZone, and Tile packets.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
serial: Device serial number (auto-generated if None)
|
|
158
|
+
product_id: Switch product ID (default: 70 - LIFX Switch)
|
|
159
|
+
firmware_version: Optional firmware version (major, minor)
|
|
160
|
+
storage: Optional persistence backend
|
|
161
|
+
scenario_manager: Optional scenario manager for testing
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
EmulatedLifxDevice configured as a switch
|
|
165
|
+
"""
|
|
166
|
+
return create_device(
|
|
167
|
+
product_id,
|
|
168
|
+
serial=serial,
|
|
169
|
+
firmware_version=firmware_version,
|
|
170
|
+
storage=storage,
|
|
171
|
+
scenario_manager=scenario_manager,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
144
175
|
def create_device(
|
|
145
176
|
product_id: int,
|
|
146
177
|
serial: str | None = None,
|
|
@@ -174,7 +174,6 @@ def generate_product_definitions(
|
|
|
174
174
|
code_lines.append("PRODUCTS: dict[int, ProductInfo] = {")
|
|
175
175
|
|
|
176
176
|
product_count = 0
|
|
177
|
-
skipped_count = 0
|
|
178
177
|
for vendor_data in all_vendors:
|
|
179
178
|
vendor_id = vendor_data.get("vid", 1)
|
|
180
179
|
defaults = vendor_data.get("defaults", {})
|
|
@@ -186,11 +185,6 @@ def generate_product_definitions(
|
|
|
186
185
|
name = product["name"]
|
|
187
186
|
features = {**default_features, **product.get("features", {})}
|
|
188
187
|
|
|
189
|
-
# Skip switch products (devices with relays) - these are not lights
|
|
190
|
-
if features.get("relays"):
|
|
191
|
-
skipped_count += 1
|
|
192
|
-
continue
|
|
193
|
-
|
|
194
188
|
# Build capabilities
|
|
195
189
|
capabilities = _build_capabilities(features)
|
|
196
190
|
|
|
@@ -223,8 +217,6 @@ def generate_product_definitions(
|
|
|
223
217
|
code_lines.append("")
|
|
224
218
|
|
|
225
219
|
print(f"Generated {product_count} product definitions")
|
|
226
|
-
if skipped_count > 0:
|
|
227
|
-
print(f"Skipped {skipped_count} switch products (relays only)")
|
|
228
220
|
return "\n".join(code_lines)
|
|
229
221
|
|
|
230
222
|
|
|
@@ -373,16 +365,17 @@ class ProductInfo:
|
|
|
373
365
|
"""Format product capabilities as a human-readable string.
|
|
374
366
|
|
|
375
367
|
Returns:
|
|
376
|
-
Comma-separated capability string (e.g., "
|
|
368
|
+
Comma-separated capability string (e.g., "color, infrared, multizone")
|
|
377
369
|
"""
|
|
378
370
|
caps = []
|
|
379
371
|
|
|
380
|
-
|
|
372
|
+
if self.has_buttons:
|
|
373
|
+
caps.append("buttons")
|
|
374
|
+
|
|
381
375
|
if self.has_relays:
|
|
382
|
-
|
|
383
|
-
caps.append("switch")
|
|
376
|
+
caps.append("relays")
|
|
384
377
|
elif self.has_color:
|
|
385
|
-
caps.append("
|
|
378
|
+
caps.append("color")
|
|
386
379
|
else:
|
|
387
380
|
# Check temperature range to determine white light type
|
|
388
381
|
if self.temperature_range:
|
|
@@ -409,9 +402,7 @@ class ProductInfo:
|
|
|
409
402
|
caps.append("HEV")
|
|
410
403
|
if self.has_chain:
|
|
411
404
|
caps.append("chain")
|
|
412
|
-
|
|
413
|
-
# Only show buttons if not already identified as switch
|
|
414
|
-
caps.append("buttons")
|
|
405
|
+
|
|
415
406
|
|
|
416
407
|
return ", ".join(caps) if caps else "unknown"
|
|
417
408
|
|
|
@@ -465,10 +456,6 @@ class ProductRegistry:
|
|
|
465
456
|
prod_features = product.get("features", {})
|
|
466
457
|
features: dict[str, Any] = {**default_features, **prod_features}
|
|
467
458
|
|
|
468
|
-
# Skip switch products (devices with relays) - these are not lights
|
|
469
|
-
if features.get("relays"):
|
|
470
|
-
continue
|
|
471
|
-
|
|
472
459
|
# Build capabilities bitfield
|
|
473
460
|
capabilities = 0
|
|
474
461
|
if features.get("color"):
|
|
@@ -667,7 +654,7 @@ def _discover_new_products(
|
|
|
667
654
|
products_data: dict[str, Any] | list[dict[str, Any]],
|
|
668
655
|
existing_specs: dict[int, dict[str, Any]],
|
|
669
656
|
) -> list[dict[str, Any]]:
|
|
670
|
-
"""Find new multizone or
|
|
657
|
+
"""Find new multizone, matrix, or switch products that need specs templates.
|
|
671
658
|
|
|
672
659
|
Args:
|
|
673
660
|
products_data: Parsed products.json data
|
|
@@ -691,21 +678,19 @@ def _discover_new_products(
|
|
|
691
678
|
pid = product["pid"]
|
|
692
679
|
features = {**default_features, **product.get("features", {})}
|
|
693
680
|
|
|
694
|
-
# Skip switch products (devices with relays) - these are not lights
|
|
695
|
-
if features.get("relays"):
|
|
696
|
-
continue
|
|
697
|
-
|
|
698
681
|
# Check if this product needs specs template
|
|
699
682
|
if pid not in existing_specs:
|
|
700
683
|
is_multizone = features.get("multizone", False)
|
|
701
684
|
is_matrix = features.get("matrix", False)
|
|
685
|
+
is_switch = features.get("relays", False)
|
|
702
686
|
|
|
703
|
-
if is_multizone or is_matrix:
|
|
687
|
+
if is_multizone or is_matrix or is_switch:
|
|
704
688
|
new_product = {
|
|
705
689
|
"pid": pid,
|
|
706
690
|
"name": product["name"],
|
|
707
691
|
"multizone": is_multizone,
|
|
708
692
|
"matrix": is_matrix,
|
|
693
|
+
"switch": is_switch,
|
|
709
694
|
"extended_multizone": False,
|
|
710
695
|
}
|
|
711
696
|
|
|
@@ -732,7 +717,12 @@ def _add_product_templates(
|
|
|
732
717
|
for product in new_products:
|
|
733
718
|
product_name = product["name"].replace('"', '\\"')
|
|
734
719
|
|
|
735
|
-
if product["
|
|
720
|
+
if product["switch"]:
|
|
721
|
+
existing_specs[product["pid"]] = {
|
|
722
|
+
"relay_count": 2,
|
|
723
|
+
"notes": product_name,
|
|
724
|
+
}
|
|
725
|
+
elif product["multizone"]:
|
|
736
726
|
existing_specs[product["pid"]] = {
|
|
737
727
|
"default_zone_count": 16,
|
|
738
728
|
"min_zone_count": 1,
|
|
@@ -752,28 +742,32 @@ def _add_product_templates(
|
|
|
752
742
|
|
|
753
743
|
def _categorize_products(
|
|
754
744
|
existing_specs: dict[int, dict[str, Any]],
|
|
755
|
-
) -> tuple[list[int], list[int]]:
|
|
756
|
-
"""Categorize products into multizone and matrix.
|
|
745
|
+
) -> tuple[list[int], list[int], list[int]]:
|
|
746
|
+
"""Categorize products into switch, multizone, and matrix.
|
|
757
747
|
|
|
758
748
|
Args:
|
|
759
749
|
existing_specs: Product specs dictionary
|
|
760
750
|
|
|
761
751
|
Returns:
|
|
762
|
-
Tuple of (sorted_multizone_pids, sorted_matrix_pids)
|
|
752
|
+
Tuple of (sorted_switch_pids, sorted_multizone_pids, sorted_matrix_pids)
|
|
763
753
|
"""
|
|
754
|
+
switch_pids = []
|
|
764
755
|
multizone_pids = []
|
|
765
756
|
matrix_pids = []
|
|
766
757
|
|
|
767
758
|
for pid, specs in existing_specs.items():
|
|
768
|
-
if "
|
|
759
|
+
if "relay_count" in specs:
|
|
760
|
+
switch_pids.append(pid)
|
|
761
|
+
elif "tile_width" in specs or "tile_height" in specs:
|
|
769
762
|
matrix_pids.append(pid)
|
|
770
763
|
elif "default_zone_count" in specs:
|
|
771
764
|
multizone_pids.append(pid)
|
|
772
765
|
|
|
766
|
+
switch_pids.sort()
|
|
773
767
|
multizone_pids.sort()
|
|
774
768
|
matrix_pids.sort()
|
|
775
769
|
|
|
776
|
-
return multizone_pids, matrix_pids
|
|
770
|
+
return switch_pids, multizone_pids, matrix_pids
|
|
777
771
|
|
|
778
772
|
|
|
779
773
|
def _generate_yaml_header() -> list[str]:
|
|
@@ -833,6 +827,53 @@ def _generate_yaml_header() -> list[str]:
|
|
|
833
827
|
]
|
|
834
828
|
|
|
835
829
|
|
|
830
|
+
def _generate_switch_section(
|
|
831
|
+
switch_pids: list[int], existing_specs: dict[int, dict[str, Any]]
|
|
832
|
+
) -> list[str]:
|
|
833
|
+
"""Generate YAML lines for switch products section.
|
|
834
|
+
|
|
835
|
+
Args:
|
|
836
|
+
switch_pids: Sorted list of switch product IDs
|
|
837
|
+
existing_specs: Product specs dictionary
|
|
838
|
+
|
|
839
|
+
Returns:
|
|
840
|
+
List of YAML lines
|
|
841
|
+
"""
|
|
842
|
+
if not switch_pids:
|
|
843
|
+
return []
|
|
844
|
+
|
|
845
|
+
lines = [
|
|
846
|
+
" # ========================================",
|
|
847
|
+
" # Switch Products (Relays)",
|
|
848
|
+
" # ========================================",
|
|
849
|
+
"",
|
|
850
|
+
]
|
|
851
|
+
|
|
852
|
+
for pid in switch_pids:
|
|
853
|
+
specs = existing_specs[pid]
|
|
854
|
+
name = specs.get("notes", f"Product {pid}").split(" - ")[0]
|
|
855
|
+
|
|
856
|
+
lines.append(f" {pid}: # {name}")
|
|
857
|
+
lines.append(f" relay_count: {specs['relay_count']}")
|
|
858
|
+
|
|
859
|
+
# Add firmware version if present
|
|
860
|
+
if "default_firmware_major" in specs and "default_firmware_minor" in specs:
|
|
861
|
+
lines.append(
|
|
862
|
+
f" default_firmware_major: {specs['default_firmware_major']}"
|
|
863
|
+
)
|
|
864
|
+
lines.append(
|
|
865
|
+
f" default_firmware_minor: {specs['default_firmware_minor']}"
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
notes = specs.get("notes", "")
|
|
869
|
+
if notes:
|
|
870
|
+
notes_escaped = notes.replace('"', '\\"')
|
|
871
|
+
lines.append(f' notes: "{notes_escaped}"')
|
|
872
|
+
lines.append("")
|
|
873
|
+
|
|
874
|
+
return lines
|
|
875
|
+
|
|
876
|
+
|
|
836
877
|
def _generate_multizone_section(
|
|
837
878
|
multizone_pids: list[int], existing_specs: dict[int, dict[str, Any]]
|
|
838
879
|
) -> list[str]:
|
|
@@ -964,12 +1005,13 @@ def update_specs_file(
|
|
|
964
1005
|
_add_product_templates(new_products, existing_specs)
|
|
965
1006
|
|
|
966
1007
|
# Categorize products and sort
|
|
967
|
-
multizone_pids, matrix_pids = _categorize_products(existing_specs)
|
|
1008
|
+
switch_pids, multizone_pids, matrix_pids = _categorize_products(existing_specs)
|
|
968
1009
|
|
|
969
1010
|
# Build YAML content
|
|
970
1011
|
lines = _generate_yaml_header()
|
|
971
1012
|
lines.extend(_generate_multizone_section(multizone_pids, existing_specs))
|
|
972
1013
|
lines.extend(_generate_matrix_section(matrix_pids, existing_specs))
|
|
1014
|
+
lines.extend(_generate_switch_section(switch_pids, existing_specs))
|
|
973
1015
|
|
|
974
1016
|
# Write the new file
|
|
975
1017
|
with open(specs_path, "w") as f:
|
|
@@ -134,16 +134,17 @@ class ProductInfo:
|
|
|
134
134
|
"""Format product capabilities as a human-readable string.
|
|
135
135
|
|
|
136
136
|
Returns:
|
|
137
|
-
Comma-separated capability string (e.g., "
|
|
137
|
+
Comma-separated capability string (e.g., "color, infrared, multizone")
|
|
138
138
|
"""
|
|
139
139
|
caps = []
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
if self.has_buttons:
|
|
142
|
+
caps.append("buttons")
|
|
143
|
+
|
|
142
144
|
if self.has_relays:
|
|
143
|
-
|
|
144
|
-
caps.append("switch")
|
|
145
|
+
caps.append("relays")
|
|
145
146
|
elif self.has_color:
|
|
146
|
-
caps.append("
|
|
147
|
+
caps.append("color")
|
|
147
148
|
else:
|
|
148
149
|
# Check temperature range to determine white light type
|
|
149
150
|
if self.temperature_range:
|
|
@@ -170,9 +171,6 @@ class ProductInfo:
|
|
|
170
171
|
caps.append("HEV")
|
|
171
172
|
if self.has_chain:
|
|
172
173
|
caps.append("chain")
|
|
173
|
-
if self.has_buttons and not self.has_relays:
|
|
174
|
-
# Only show buttons if not already identified as switch
|
|
175
|
-
caps.append("buttons")
|
|
176
174
|
|
|
177
175
|
return ", ".join(caps) if caps else "unknown"
|
|
178
176
|
|
|
@@ -505,6 +503,22 @@ PRODUCTS: dict[int, ProductInfo] = {
|
|
|
505
503
|
temperature_range=TemperatureRange(min=1500, max=9000),
|
|
506
504
|
min_ext_mz_firmware=None,
|
|
507
505
|
),
|
|
506
|
+
70: ProductInfo(
|
|
507
|
+
pid=70,
|
|
508
|
+
name="LIFX Switch",
|
|
509
|
+
vendor=1,
|
|
510
|
+
capabilities=ProductCapability.RELAYS | ProductCapability.BUTTONS,
|
|
511
|
+
temperature_range=None,
|
|
512
|
+
min_ext_mz_firmware=None,
|
|
513
|
+
),
|
|
514
|
+
71: ProductInfo(
|
|
515
|
+
pid=71,
|
|
516
|
+
name="LIFX Switch",
|
|
517
|
+
vendor=1,
|
|
518
|
+
capabilities=ProductCapability.RELAYS | ProductCapability.BUTTONS,
|
|
519
|
+
temperature_range=None,
|
|
520
|
+
min_ext_mz_firmware=None,
|
|
521
|
+
),
|
|
508
522
|
81: ProductInfo(
|
|
509
523
|
pid=81,
|
|
510
524
|
name="LIFX Candle White to Warm",
|
|
@@ -545,6 +559,14 @@ PRODUCTS: dict[int, ProductInfo] = {
|
|
|
545
559
|
temperature_range=TemperatureRange(min=2700, max=2700),
|
|
546
560
|
min_ext_mz_firmware=None,
|
|
547
561
|
),
|
|
562
|
+
89: ProductInfo(
|
|
563
|
+
pid=89,
|
|
564
|
+
name="LIFX Switch",
|
|
565
|
+
vendor=1,
|
|
566
|
+
capabilities=ProductCapability.RELAYS | ProductCapability.BUTTONS,
|
|
567
|
+
temperature_range=None,
|
|
568
|
+
min_ext_mz_firmware=None,
|
|
569
|
+
),
|
|
548
570
|
90: ProductInfo(
|
|
549
571
|
pid=90,
|
|
550
572
|
name="LIFX Clean",
|
|
@@ -681,6 +703,22 @@ PRODUCTS: dict[int, ProductInfo] = {
|
|
|
681
703
|
temperature_range=TemperatureRange(min=1500, max=9000),
|
|
682
704
|
min_ext_mz_firmware=None,
|
|
683
705
|
),
|
|
706
|
+
115: ProductInfo(
|
|
707
|
+
pid=115,
|
|
708
|
+
name="LIFX Switch",
|
|
709
|
+
vendor=1,
|
|
710
|
+
capabilities=ProductCapability.RELAYS | ProductCapability.BUTTONS,
|
|
711
|
+
temperature_range=None,
|
|
712
|
+
min_ext_mz_firmware=None,
|
|
713
|
+
),
|
|
714
|
+
116: ProductInfo(
|
|
715
|
+
pid=116,
|
|
716
|
+
name="LIFX Switch",
|
|
717
|
+
vendor=1,
|
|
718
|
+
capabilities=ProductCapability.RELAYS | ProductCapability.BUTTONS,
|
|
719
|
+
temperature_range=None,
|
|
720
|
+
min_ext_mz_firmware=None,
|
|
721
|
+
),
|
|
684
722
|
117: ProductInfo(
|
|
685
723
|
pid=117,
|
|
686
724
|
name="LIFX Z US",
|
|
@@ -1321,10 +1359,6 @@ class ProductRegistry:
|
|
|
1321
1359
|
prod_features = product.get("features", {})
|
|
1322
1360
|
features: dict[str, Any] = {**default_features, **prod_features}
|
|
1323
1361
|
|
|
1324
|
-
# Skip switch products (devices with relays) - these are not lights
|
|
1325
|
-
if features.get("relays"):
|
|
1326
|
-
continue
|
|
1327
|
-
|
|
1328
1362
|
# Build capabilities bitfield
|
|
1329
1363
|
capabilities = 0
|
|
1330
1364
|
if features.get("color"):
|
lifx_emulator/products/specs.yml
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
# tile_width: <number> # Width of each tile in zones
|
|
25
25
|
# tile_height: <number> # Height of each tile in zones
|
|
26
26
|
#
|
|
27
|
-
# # Host firmware version (optional, overrides
|
|
27
|
+
# # Host firmware version (optional, overrides auto firmware selection)
|
|
28
28
|
# default_firmware_major: <number> # Firmware major version (e.g., 3)
|
|
29
29
|
# default_firmware_minor: <number> # Firmware minor version (e.g., 70)
|
|
30
30
|
#
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
#
|
|
34
34
|
# Firmware Version Notes:
|
|
35
35
|
# ----------------------
|
|
36
|
-
# If default_firmware_major and default_firmware_minor are both specified
|
|
37
|
-
#
|
|
38
|
-
# of that
|
|
36
|
+
# If default_firmware_major and default_firmware_minor are both specified
|
|
37
|
+
# they will be used as the default firmware version when creating devices
|
|
38
|
+
# of that type. This overrides the automatic firmware version selection
|
|
39
39
|
# based on extended_multizone capability (which defaults to 3.70 for extended
|
|
40
40
|
# multizone or 2.60 for non-extended).
|
|
41
41
|
#
|
|
@@ -350,3 +350,37 @@ products:
|
|
|
350
350
|
tile_width: 3
|
|
351
351
|
tile_height: 2
|
|
352
352
|
notes: LIFX Round Path Intl
|
|
353
|
+
|
|
354
|
+
# ========================================
|
|
355
|
+
# Switch Products (Relays)
|
|
356
|
+
# ========================================
|
|
357
|
+
|
|
358
|
+
70: # LIFX Switch
|
|
359
|
+
relay_count: 2
|
|
360
|
+
default_firmware_major: 4
|
|
361
|
+
default_firmware_minor: 100
|
|
362
|
+
notes: LIFX Switch
|
|
363
|
+
|
|
364
|
+
71: # LIFX Switch
|
|
365
|
+
relay_count: 2
|
|
366
|
+
default_firmware_major: 4
|
|
367
|
+
default_firmware_minor: 100
|
|
368
|
+
notes: LIFX Switch
|
|
369
|
+
|
|
370
|
+
89: # LIFX Switch
|
|
371
|
+
relay_count: 2
|
|
372
|
+
default_firmware_major: 4
|
|
373
|
+
default_firmware_minor: 100
|
|
374
|
+
notes: LIFX Switch
|
|
375
|
+
|
|
376
|
+
115: # LIFX Switch
|
|
377
|
+
relay_count: 2
|
|
378
|
+
default_firmware_major: 4
|
|
379
|
+
default_firmware_minor: 100
|
|
380
|
+
notes: LIFX Switch
|
|
381
|
+
|
|
382
|
+
116: # LIFX Switch
|
|
383
|
+
relay_count: 2
|
|
384
|
+
default_firmware_major: 4
|
|
385
|
+
default_firmware_minor: 100
|
|
386
|
+
notes: LIFX Switch
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
lifx_emulator/__init__.py,sha256=vjhtpAQRSsUZtaUGCQKbmPALvwZ_BF8Mko8w6jzVqBw,819
|
|
2
|
-
lifx_emulator/__main__.py,sha256=
|
|
2
|
+
lifx_emulator/__main__.py,sha256=0rNT1ua1uckq9f2l7g6wIC5VDj9kmWzuL-UZBwm5_tE,22057
|
|
3
3
|
lifx_emulator/constants.py,sha256=DFZkUsdewE-x_3MgO28tMGkjUCWPeYc3xLj_EXViGOw,1032
|
|
4
4
|
lifx_emulator/server.py,sha256=r2JYFcpZIqqhue-Nfq7FbN0KfC3XDf3XDb6b43DsiCk,16438
|
|
5
5
|
lifx_emulator/api/__init__.py,sha256=FoEPw_In5-H_BDQ-XIIONvgj-UqIDVtejIEVRv9qmV8,647
|
|
@@ -15,17 +15,17 @@ lifx_emulator/api/services/__init__.py,sha256=ttjjZfAxbDQC_Ep0LkXjopNiVZOFPsFDSO
|
|
|
15
15
|
lifx_emulator/api/services/device_service.py,sha256=r3uFWApC8sVQMCuuzkyjm27K4LDpZnnHmQNgXWX40ok,6294
|
|
16
16
|
lifx_emulator/api/templates/dashboard.html,sha256=h-PeOH_La5bVOUBcXmTY2leRlMdL8D8yJ-NCx3S16-A,33792
|
|
17
17
|
lifx_emulator/devices/__init__.py,sha256=QlBTPnFErJcSKLvGyeDwemh7xcpjYvB_L5siKsjr3s8,1089
|
|
18
|
-
lifx_emulator/devices/device.py,sha256=
|
|
18
|
+
lifx_emulator/devices/device.py,sha256=yEOXc_xr1X45bJzG2qB-A-oIHwnA8qqYlIsFialobGc,15780
|
|
19
19
|
lifx_emulator/devices/manager.py,sha256=XDrT82um5sgNpNihLj5RsNvHqdVI1bK9YY2eBzWIcf0,8162
|
|
20
20
|
lifx_emulator/devices/observers.py,sha256=-KnUgFcKdhlNo7CNVstP-u0wU2W0JAGg055ZPV15Sj0,3874
|
|
21
21
|
lifx_emulator/devices/persistence.py,sha256=9Mhj46-xrweOmyzjORCi2jKIwa8XJWpQ5CgaKcw6U98,10513
|
|
22
22
|
lifx_emulator/devices/state_restorer.py,sha256=eDsRSW-2RviP_0Qlk2DHqMaB-zhV0X1cNQECv2lD1qc,9809
|
|
23
23
|
lifx_emulator/devices/state_serializer.py,sha256=aws4LUmXBJS8oBrQziJtlV0XMvCTm5X4dGkGlO_QHcM,6281
|
|
24
|
-
lifx_emulator/devices/states.py,sha256=
|
|
25
|
-
lifx_emulator/factories/__init__.py,sha256=
|
|
26
|
-
lifx_emulator/factories/builder.py,sha256=
|
|
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
27
|
lifx_emulator/factories/default_config.py,sha256=FTcxKDfeTmO49GTSki8nxnEIZQzR0Lg0hL_PwHUrkVQ,4828
|
|
28
|
-
lifx_emulator/factories/factory.py,sha256=
|
|
28
|
+
lifx_emulator/factories/factory.py,sha256=MyGG-pW7EV2BFP5ZzgMuFF5TfNFvfyFDoE5dmd3LC8w,8623
|
|
29
29
|
lifx_emulator/factories/firmware_config.py,sha256=tPN5Hq-uNb1xzW9Q0A9jD-G0-NaGfINcD0i1XZRUMoE,2711
|
|
30
30
|
lifx_emulator/factories/serial_generator.py,sha256=MbaXoommsj76ho8_ZoKuUDnffDf98YvwQiXZSWsUsEs,2507
|
|
31
31
|
lifx_emulator/handlers/__init__.py,sha256=3Hj1hRo3yL3E7GKwG9TaYh33ymk_N3bRiQ8nvqSQULA,1306
|
|
@@ -36,10 +36,10 @@ lifx_emulator/handlers/multizone_handlers.py,sha256=2dYsitq0KzEaxEAJmz7ixtir1tvF
|
|
|
36
36
|
lifx_emulator/handlers/registry.py,sha256=s1ht4PmPhXhAcwu1hoY4yW39wy3SPJBMY-9Uxd0FWuE,3292
|
|
37
37
|
lifx_emulator/handlers/tile_handlers.py,sha256=L4fNKGTSSIxRuqKrfDrMSrNPvDJr3aIuaEqbhRCOt04,17176
|
|
38
38
|
lifx_emulator/products/__init__.py,sha256=qcNop_kRYFF3zSjNemzQEgu3jPrIxfyQyLv9GsnaLEI,627
|
|
39
|
-
lifx_emulator/products/generator.py,sha256=
|
|
40
|
-
lifx_emulator/products/registry.py,sha256=
|
|
39
|
+
lifx_emulator/products/generator.py,sha256=fvrhw_b7shLCtEtUFxWF5VBEQAeSrsaiXxoGIP5Vn4g,34675
|
|
40
|
+
lifx_emulator/products/registry.py,sha256=1SZ3fXVFFL8jhKYIZBqwtIQDN3qL1Lvf86P3N1_Kdx8,47323
|
|
41
41
|
lifx_emulator/products/specs.py,sha256=epqz2DPyNOOOFHhmI_wlk7iEbgN0vCugHz-hWx9FlAI,8728
|
|
42
|
-
lifx_emulator/products/specs.yml,sha256
|
|
42
|
+
lifx_emulator/products/specs.yml,sha256=6hh7V-953uN4t3WD2rY9Nn8zKFZuQDHgYVo7LgZcGEA,10399
|
|
43
43
|
lifx_emulator/protocol/__init__.py,sha256=-wjC-wBcb7fxi5I-mJr2Ad8K2YRflJFdLLdobfD-W1Q,56
|
|
44
44
|
lifx_emulator/protocol/base.py,sha256=V6t0baSgIXjrsz2dBuUn_V9xwradSqMxBFJHAUtnfCs,15368
|
|
45
45
|
lifx_emulator/protocol/const.py,sha256=ilhv-KcQpHtKh2MDCaIbMLQAsxKO_uTaxyR63v1W8cc,226
|
|
@@ -55,8 +55,8 @@ lifx_emulator/scenarios/__init__.py,sha256=CGjudoWvyysvFj2xej11N2cr3mYROGtRb9zVH
|
|
|
55
55
|
lifx_emulator/scenarios/manager.py,sha256=1esxRdz74UynNk1wb86MGZ2ZFAuMzByuu74nRe3D-Og,11163
|
|
56
56
|
lifx_emulator/scenarios/models.py,sha256=BKS_fGvrbkGe-vK3arZ0w2f9adS1UZhiOoKpu7GENnc,4099
|
|
57
57
|
lifx_emulator/scenarios/persistence.py,sha256=3vjtPNFYfag38tUxuqxkGpWhQ7uBitc1rLroSAuw9N8,8881
|
|
58
|
-
lifx_emulator-2.
|
|
59
|
-
lifx_emulator-2.
|
|
60
|
-
lifx_emulator-2.
|
|
61
|
-
lifx_emulator-2.
|
|
62
|
-
lifx_emulator-2.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|