lifx-emulator 1.0.2__py3-none-any.whl → 2.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.
- lifx_emulator/__init__.py +1 -1
- lifx_emulator/__main__.py +26 -51
- lifx_emulator/api/__init__.py +18 -0
- lifx_emulator/api/app.py +154 -0
- lifx_emulator/api/mappers/__init__.py +5 -0
- lifx_emulator/api/mappers/device_mapper.py +114 -0
- lifx_emulator/api/models.py +133 -0
- lifx_emulator/api/routers/__init__.py +11 -0
- lifx_emulator/api/routers/devices.py +130 -0
- lifx_emulator/api/routers/monitoring.py +52 -0
- lifx_emulator/api/routers/scenarios.py +247 -0
- lifx_emulator/api/services/__init__.py +8 -0
- lifx_emulator/api/services/device_service.py +198 -0
- lifx_emulator/{api.py → api/templates/dashboard.html} +0 -942
- lifx_emulator/devices/__init__.py +37 -0
- lifx_emulator/devices/device.py +333 -0
- lifx_emulator/devices/manager.py +256 -0
- lifx_emulator/{async_storage.py → devices/persistence.py} +3 -3
- lifx_emulator/{state_restorer.py → devices/state_restorer.py} +2 -2
- lifx_emulator/devices/states.py +333 -0
- lifx_emulator/factories/__init__.py +37 -0
- lifx_emulator/factories/builder.py +371 -0
- lifx_emulator/factories/default_config.py +158 -0
- lifx_emulator/factories/factory.py +221 -0
- lifx_emulator/factories/firmware_config.py +59 -0
- lifx_emulator/factories/serial_generator.py +82 -0
- lifx_emulator/handlers/base.py +1 -1
- lifx_emulator/handlers/device_handlers.py +10 -28
- lifx_emulator/handlers/light_handlers.py +5 -9
- lifx_emulator/handlers/multizone_handlers.py +1 -1
- lifx_emulator/handlers/tile_handlers.py +1 -1
- lifx_emulator/products/generator.py +389 -170
- lifx_emulator/products/registry.py +52 -40
- lifx_emulator/products/specs.py +12 -13
- lifx_emulator/protocol/base.py +115 -61
- lifx_emulator/protocol/generator.py +18 -5
- lifx_emulator/protocol/packets.py +7 -7
- lifx_emulator/repositories/__init__.py +22 -0
- lifx_emulator/repositories/device_repository.py +155 -0
- lifx_emulator/repositories/storage_backend.py +107 -0
- lifx_emulator/scenarios/__init__.py +22 -0
- lifx_emulator/{scenario_manager.py → scenarios/manager.py} +11 -91
- lifx_emulator/scenarios/models.py +112 -0
- lifx_emulator/{scenario_persistence.py → scenarios/persistence.py} +82 -47
- lifx_emulator/server.py +38 -64
- {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.0.0.dist-info}/METADATA +1 -1
- lifx_emulator-2.0.0.dist-info/RECORD +62 -0
- lifx_emulator/device.py +0 -750
- lifx_emulator/device_states.py +0 -114
- lifx_emulator/factories.py +0 -380
- lifx_emulator/storage_protocol.py +0 -100
- lifx_emulator-1.0.2.dist-info/RECORD +0 -40
- /lifx_emulator/{observers.py → devices/observers.py} +0 -0
- /lifx_emulator/{state_serializer.py → devices/state_serializer.py} +0 -0
- {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.0.0.dist-info}/WHEEL +0 -0
- {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.0.0.dist-info}/entry_points.txt +0 -0
- {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.0.0.dist-info}/licenses/LICENSE +0 -0
lifx_emulator/device_states.py
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
"""Focused state dataclasses following Single Responsibility Principle."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import time
|
|
6
|
-
import uuid
|
|
7
|
-
from dataclasses import dataclass, field
|
|
8
|
-
from typing import Any
|
|
9
|
-
|
|
10
|
-
from lifx_emulator.constants import LIFX_UDP_PORT
|
|
11
|
-
from lifx_emulator.protocol.protocol_types import LightHsbk
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@dataclass
|
|
15
|
-
class CoreDeviceState:
|
|
16
|
-
"""Core device identification and basic state."""
|
|
17
|
-
|
|
18
|
-
serial: str
|
|
19
|
-
label: str
|
|
20
|
-
power_level: int
|
|
21
|
-
color: LightHsbk
|
|
22
|
-
vendor: int
|
|
23
|
-
product: int
|
|
24
|
-
version_major: int
|
|
25
|
-
version_minor: int
|
|
26
|
-
build_timestamp: int
|
|
27
|
-
uptime_ns: int = 0
|
|
28
|
-
mac_address: bytes = field(default_factory=lambda: bytes.fromhex("d073d5123456"))
|
|
29
|
-
port: int = LIFX_UDP_PORT
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@dataclass
|
|
33
|
-
class NetworkState:
|
|
34
|
-
"""Network and connectivity state."""
|
|
35
|
-
|
|
36
|
-
wifi_signal: float = -45.0
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@dataclass
|
|
40
|
-
class LocationState:
|
|
41
|
-
"""Device location metadata."""
|
|
42
|
-
|
|
43
|
-
location_id: bytes = field(default_factory=lambda: uuid.uuid4().bytes)
|
|
44
|
-
location_label: str = "Test Location"
|
|
45
|
-
location_updated_at: int = field(default_factory=lambda: int(time.time() * 1e9))
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
@dataclass
|
|
49
|
-
class GroupState:
|
|
50
|
-
"""Device group metadata."""
|
|
51
|
-
|
|
52
|
-
group_id: bytes = field(default_factory=lambda: uuid.uuid4().bytes)
|
|
53
|
-
group_label: str = "Test Group"
|
|
54
|
-
group_updated_at: int = field(default_factory=lambda: int(time.time() * 1e9))
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
@dataclass
|
|
58
|
-
class InfraredState:
|
|
59
|
-
"""Infrared capability state."""
|
|
60
|
-
|
|
61
|
-
infrared_brightness: int = 0 # 0-65535
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
@dataclass
|
|
65
|
-
class HevState:
|
|
66
|
-
"""HEV (germicidal UV) capability state."""
|
|
67
|
-
|
|
68
|
-
hev_cycle_duration_s: int = 7200 # 2 hours default
|
|
69
|
-
hev_cycle_remaining_s: int = 0
|
|
70
|
-
hev_cycle_last_power: bool = False
|
|
71
|
-
hev_indication: bool = True
|
|
72
|
-
hev_last_result: int = 0 # 0=success
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
@dataclass
|
|
76
|
-
class MultiZoneState:
|
|
77
|
-
"""Multizone (strip/beam) capability state."""
|
|
78
|
-
|
|
79
|
-
zone_count: int
|
|
80
|
-
zone_colors: list[LightHsbk]
|
|
81
|
-
effect_type: int = 0 # 0=OFF, 1=MOVE, 2=RESERVED
|
|
82
|
-
effect_speed: int = 5 # Duration of one cycle in seconds
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
@dataclass
|
|
86
|
-
class MatrixState:
|
|
87
|
-
"""Matrix (tile/candle) capability state."""
|
|
88
|
-
|
|
89
|
-
tile_count: int
|
|
90
|
-
tile_devices: list[dict[str, Any]]
|
|
91
|
-
tile_width: int
|
|
92
|
-
tile_height: int
|
|
93
|
-
effect_type: int = 0 # 0=OFF, 2=MORPH, 3=FLAME
|
|
94
|
-
effect_speed: int = 5 # Duration of one cycle in seconds
|
|
95
|
-
effect_palette_count: int = 0
|
|
96
|
-
effect_palette: list[LightHsbk] = field(default_factory=list)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
@dataclass
|
|
100
|
-
class WaveformState:
|
|
101
|
-
"""Waveform effect state."""
|
|
102
|
-
|
|
103
|
-
waveform_active: bool = False
|
|
104
|
-
waveform_type: int = 0
|
|
105
|
-
waveform_transient: bool = False
|
|
106
|
-
waveform_color: LightHsbk = field(
|
|
107
|
-
default_factory=lambda: LightHsbk(
|
|
108
|
-
hue=0, saturation=0, brightness=0, kelvin=3500
|
|
109
|
-
)
|
|
110
|
-
)
|
|
111
|
-
waveform_period_ms: int = 0
|
|
112
|
-
waveform_cycles: float = 0
|
|
113
|
-
waveform_duty_cycle: int = 0
|
|
114
|
-
waveform_skew_ratio: int = 0
|
lifx_emulator/factories.py
DELETED
|
@@ -1,380 +0,0 @@
|
|
|
1
|
-
"""Factory functions for creating emulated LIFX devices."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import random
|
|
6
|
-
import time
|
|
7
|
-
from typing import TYPE_CHECKING
|
|
8
|
-
|
|
9
|
-
from lifx_emulator.device import DeviceState, EmulatedLifxDevice
|
|
10
|
-
from lifx_emulator.device_states import (
|
|
11
|
-
CoreDeviceState,
|
|
12
|
-
GroupState,
|
|
13
|
-
HevState,
|
|
14
|
-
InfraredState,
|
|
15
|
-
LocationState,
|
|
16
|
-
MatrixState,
|
|
17
|
-
MultiZoneState,
|
|
18
|
-
NetworkState,
|
|
19
|
-
WaveformState,
|
|
20
|
-
)
|
|
21
|
-
from lifx_emulator.products.registry import ProductInfo, get_product
|
|
22
|
-
from lifx_emulator.products.specs import (
|
|
23
|
-
get_default_tile_count,
|
|
24
|
-
get_default_zone_count,
|
|
25
|
-
get_tile_dimensions,
|
|
26
|
-
)
|
|
27
|
-
from lifx_emulator.protocol.protocol_types import LightHsbk
|
|
28
|
-
from lifx_emulator.state_restorer import StateRestorer
|
|
29
|
-
|
|
30
|
-
if TYPE_CHECKING:
|
|
31
|
-
from lifx_emulator.async_storage import AsyncDeviceStorage
|
|
32
|
-
from lifx_emulator.scenario_manager import HierarchicalScenarioManager
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def create_color_light(
|
|
36
|
-
serial: str | None = None,
|
|
37
|
-
firmware_version: tuple[int, int] | None = None,
|
|
38
|
-
storage: AsyncDeviceStorage | None = None,
|
|
39
|
-
scenario_manager: HierarchicalScenarioManager | None = None,
|
|
40
|
-
) -> EmulatedLifxDevice:
|
|
41
|
-
"""Create a regular color light (LIFX Color)"""
|
|
42
|
-
return create_device(
|
|
43
|
-
91,
|
|
44
|
-
serial=serial,
|
|
45
|
-
firmware_version=firmware_version,
|
|
46
|
-
storage=storage,
|
|
47
|
-
scenario_manager=scenario_manager,
|
|
48
|
-
) # LIFX Color
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def create_infrared_light(
|
|
52
|
-
serial: str | None = None,
|
|
53
|
-
firmware_version: tuple[int, int] | None = None,
|
|
54
|
-
storage: AsyncDeviceStorage | None = None,
|
|
55
|
-
scenario_manager: HierarchicalScenarioManager | None = None,
|
|
56
|
-
) -> EmulatedLifxDevice:
|
|
57
|
-
"""Create an infrared-enabled light (LIFX A19 Night Vision)"""
|
|
58
|
-
return create_device(
|
|
59
|
-
29,
|
|
60
|
-
serial=serial,
|
|
61
|
-
firmware_version=firmware_version,
|
|
62
|
-
storage=storage,
|
|
63
|
-
scenario_manager=scenario_manager,
|
|
64
|
-
) # LIFX A19 Night Vision
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def create_hev_light(
|
|
68
|
-
serial: str | None = None,
|
|
69
|
-
firmware_version: tuple[int, int] | None = None,
|
|
70
|
-
storage: AsyncDeviceStorage | None = None,
|
|
71
|
-
scenario_manager: HierarchicalScenarioManager | None = None,
|
|
72
|
-
) -> EmulatedLifxDevice:
|
|
73
|
-
"""Create an HEV-enabled light (LIFX Clean)"""
|
|
74
|
-
return create_device(
|
|
75
|
-
90,
|
|
76
|
-
serial=serial,
|
|
77
|
-
firmware_version=firmware_version,
|
|
78
|
-
storage=storage,
|
|
79
|
-
scenario_manager=scenario_manager,
|
|
80
|
-
) # LIFX Clean
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def create_multizone_light(
|
|
84
|
-
serial: str | None = None,
|
|
85
|
-
zone_count: int | None = None,
|
|
86
|
-
extended_multizone: bool = True,
|
|
87
|
-
firmware_version: tuple[int, int] | None = None,
|
|
88
|
-
storage: AsyncDeviceStorage | None = None,
|
|
89
|
-
scenario_manager: HierarchicalScenarioManager | None = None,
|
|
90
|
-
) -> EmulatedLifxDevice:
|
|
91
|
-
"""Create a multizone light (LIFX Beam)
|
|
92
|
-
|
|
93
|
-
Args:
|
|
94
|
-
serial: Optional serial
|
|
95
|
-
zone_count: Optional zone count (uses product default if not specified)
|
|
96
|
-
extended_multizone: enables support for extended multizone requests
|
|
97
|
-
firmware_version: Optional firmware version tuple (major, minor)
|
|
98
|
-
storage: Optional storage for persistence
|
|
99
|
-
scenario_manager: Optional scenario manager
|
|
100
|
-
"""
|
|
101
|
-
return create_device(
|
|
102
|
-
38,
|
|
103
|
-
serial=serial,
|
|
104
|
-
zone_count=zone_count,
|
|
105
|
-
extended_multizone=extended_multizone,
|
|
106
|
-
firmware_version=firmware_version,
|
|
107
|
-
storage=storage,
|
|
108
|
-
scenario_manager=scenario_manager,
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def create_tile_device(
|
|
113
|
-
serial: str | None = None,
|
|
114
|
-
tile_count: int | None = None,
|
|
115
|
-
tile_width: int | None = None,
|
|
116
|
-
tile_height: int | None = None,
|
|
117
|
-
firmware_version: tuple[int, int] | None = None,
|
|
118
|
-
storage: AsyncDeviceStorage | None = None,
|
|
119
|
-
scenario_manager: HierarchicalScenarioManager | None = None,
|
|
120
|
-
) -> EmulatedLifxDevice:
|
|
121
|
-
"""Create a tile device (LIFX Tile)
|
|
122
|
-
|
|
123
|
-
Args:
|
|
124
|
-
serial: Optional serial
|
|
125
|
-
tile_count: Optional tile count (uses product default)
|
|
126
|
-
tile_width: Optional tile width in pixels (uses product default)
|
|
127
|
-
tile_height: Optional tile height in pixels (uses product default)
|
|
128
|
-
firmware_version: Optional firmware version tuple (major, minor)
|
|
129
|
-
storage: Optional storage for persistence
|
|
130
|
-
scenario_manager: Optional scenario manager
|
|
131
|
-
"""
|
|
132
|
-
return create_device(
|
|
133
|
-
55,
|
|
134
|
-
serial=serial,
|
|
135
|
-
tile_count=tile_count,
|
|
136
|
-
tile_width=tile_width,
|
|
137
|
-
tile_height=tile_height,
|
|
138
|
-
firmware_version=firmware_version,
|
|
139
|
-
storage=storage,
|
|
140
|
-
scenario_manager=scenario_manager,
|
|
141
|
-
) # LIFX Tile
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def create_color_temperature_light(
|
|
145
|
-
serial: str | None = None,
|
|
146
|
-
firmware_version: tuple[int, int] | None = None,
|
|
147
|
-
storage: AsyncDeviceStorage | None = None,
|
|
148
|
-
scenario_manager: HierarchicalScenarioManager | None = None,
|
|
149
|
-
) -> EmulatedLifxDevice:
|
|
150
|
-
"""Create a color temperature light (LIFX Mini White to Warm).
|
|
151
|
-
|
|
152
|
-
Variable color temperature, no RGB.
|
|
153
|
-
"""
|
|
154
|
-
return create_device(
|
|
155
|
-
50,
|
|
156
|
-
serial=serial,
|
|
157
|
-
firmware_version=firmware_version,
|
|
158
|
-
storage=storage,
|
|
159
|
-
scenario_manager=scenario_manager,
|
|
160
|
-
) # LIFX Mini White to Warm
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def create_device(
|
|
164
|
-
product_id: int,
|
|
165
|
-
serial: str | None = None,
|
|
166
|
-
zone_count: int | None = None,
|
|
167
|
-
extended_multizone: bool | None = None,
|
|
168
|
-
tile_count: int | None = None,
|
|
169
|
-
tile_width: int | None = None,
|
|
170
|
-
tile_height: int | None = None,
|
|
171
|
-
firmware_version: tuple[int, int] | None = None,
|
|
172
|
-
storage: AsyncDeviceStorage | None = None,
|
|
173
|
-
scenario_manager: HierarchicalScenarioManager | None = None,
|
|
174
|
-
) -> EmulatedLifxDevice:
|
|
175
|
-
"""Create a device for any LIFX product using the product registry.
|
|
176
|
-
|
|
177
|
-
Args:
|
|
178
|
-
product_id: Product ID from the LIFX product registry
|
|
179
|
-
serial: Optional serial (auto-generated if not provided)
|
|
180
|
-
zone_count: Number of zones for multizone devices (auto-determined)
|
|
181
|
-
extended_multizone: Enable extended multizone requests
|
|
182
|
-
tile_count: Number of tiles for matrix devices (default: 5)
|
|
183
|
-
tile_width: Width of each tile in pixels (default: 8)
|
|
184
|
-
tile_height: Height of each tile in pixels (default: 8)
|
|
185
|
-
firmware_version: Optional firmware version tuple (major, minor).
|
|
186
|
-
If not specified, uses 3.70 for extended_multizone
|
|
187
|
-
or 2.60 otherwise
|
|
188
|
-
storage: Optional storage for persistence
|
|
189
|
-
|
|
190
|
-
Returns:
|
|
191
|
-
EmulatedLifxDevice configured for the specified product
|
|
192
|
-
|
|
193
|
-
Raises:
|
|
194
|
-
ValueError: If product_id is not found in registry
|
|
195
|
-
|
|
196
|
-
Examples:
|
|
197
|
-
>>> # Create LIFX A19 (PID 27)
|
|
198
|
-
>>> device = create_device(27)
|
|
199
|
-
>>> # Create LIFX Z strip (PID 32) with 24 zones
|
|
200
|
-
>>> strip = create_device(32, zone_count=24)
|
|
201
|
-
>>> # Create LIFX Tile (PID 55) with 10 tiles
|
|
202
|
-
>>> tiles = create_device(55, tile_count=10)
|
|
203
|
-
"""
|
|
204
|
-
# Get product info from registry
|
|
205
|
-
product_info: ProductInfo | None = get_product(product_id)
|
|
206
|
-
if product_info is None:
|
|
207
|
-
raise ValueError(f"Unknown product ID: {product_id}")
|
|
208
|
-
|
|
209
|
-
# Generate serial if not provided
|
|
210
|
-
if not serial:
|
|
211
|
-
# Use different prefixes for product types for easier identification
|
|
212
|
-
if product_info.has_matrix:
|
|
213
|
-
prefix = "d073d9" # Tiles
|
|
214
|
-
elif product_info.has_multizone:
|
|
215
|
-
prefix = "d073d8" # Strips/Beams
|
|
216
|
-
elif product_info.has_hev:
|
|
217
|
-
prefix = "d073d7" # HEV
|
|
218
|
-
elif product_info.has_infrared:
|
|
219
|
-
prefix = "d073d6" # Infrared
|
|
220
|
-
else:
|
|
221
|
-
prefix = "d073d5" # Regular lights
|
|
222
|
-
serial = f"{prefix}{random.randint(100000, 999999):06x}" # nosec
|
|
223
|
-
|
|
224
|
-
# Determine zone count for multizone devices
|
|
225
|
-
if product_info.has_multizone and zone_count is None:
|
|
226
|
-
# Try to get default from specs first
|
|
227
|
-
zone_count = get_default_zone_count(product_id) or 16
|
|
228
|
-
|
|
229
|
-
# Determine tile configuration for matrix devices
|
|
230
|
-
if product_info.has_matrix:
|
|
231
|
-
# Get tile dimensions from specs (always use specs for dimensions)
|
|
232
|
-
tile_dims = get_tile_dimensions(product_id)
|
|
233
|
-
if tile_dims:
|
|
234
|
-
tile_width, tile_height = tile_dims
|
|
235
|
-
else:
|
|
236
|
-
# Fallback to standard 8x8 tiles
|
|
237
|
-
if tile_width is None:
|
|
238
|
-
tile_width = 8
|
|
239
|
-
if tile_height is None:
|
|
240
|
-
tile_height = 8
|
|
241
|
-
|
|
242
|
-
# Get default tile count from specs
|
|
243
|
-
if tile_count is None:
|
|
244
|
-
specs_tile_count = get_default_tile_count(product_id)
|
|
245
|
-
if specs_tile_count is not None:
|
|
246
|
-
tile_count = specs_tile_count
|
|
247
|
-
else:
|
|
248
|
-
tile_count = 5 # Generic default
|
|
249
|
-
|
|
250
|
-
# Create default color based on product type
|
|
251
|
-
if (
|
|
252
|
-
not product_info.has_color
|
|
253
|
-
and product_info.temperature_range is not None
|
|
254
|
-
and product_info.temperature_range.min == product_info.temperature_range.max
|
|
255
|
-
):
|
|
256
|
-
# Brightness only light
|
|
257
|
-
default_color = LightHsbk(hue=0, saturation=0, brightness=32768, kelvin=2700)
|
|
258
|
-
elif (
|
|
259
|
-
not product_info.has_color
|
|
260
|
-
and product_info.temperature_range is not None
|
|
261
|
-
and product_info.temperature_range.min != product_info.temperature_range.max
|
|
262
|
-
):
|
|
263
|
-
# Color temperature adjustable light
|
|
264
|
-
default_color = LightHsbk(hue=0, saturation=0, brightness=32768, kelvin=3500)
|
|
265
|
-
else:
|
|
266
|
-
# Color devices - use a unique hue per device type
|
|
267
|
-
hue_map = {
|
|
268
|
-
"matrix": 43690, # Cyan
|
|
269
|
-
"multizone": 0, # Red
|
|
270
|
-
"hev": 32768, # Green
|
|
271
|
-
"infrared": 0, # Red
|
|
272
|
-
"color": 21845, # Orange
|
|
273
|
-
}
|
|
274
|
-
if product_info.has_matrix:
|
|
275
|
-
hue = hue_map["matrix"]
|
|
276
|
-
elif product_info.has_multizone:
|
|
277
|
-
hue = hue_map["multizone"]
|
|
278
|
-
elif product_info.has_hev:
|
|
279
|
-
hue = hue_map["hev"]
|
|
280
|
-
elif product_info.has_infrared:
|
|
281
|
-
hue = hue_map["infrared"]
|
|
282
|
-
else:
|
|
283
|
-
hue = hue_map["color"]
|
|
284
|
-
default_color = LightHsbk(
|
|
285
|
-
hue=hue, saturation=65535, brightness=32768, kelvin=3500
|
|
286
|
-
)
|
|
287
|
-
|
|
288
|
-
# Get a simplified label from product name
|
|
289
|
-
label = f"{product_info.name} {serial[-6:]}"
|
|
290
|
-
|
|
291
|
-
# Determine firmware version: use extended_multizone to set default,
|
|
292
|
-
# then override with explicit firmware_version if provided
|
|
293
|
-
# None defaults to True (3.70), only explicit False gives 2.60
|
|
294
|
-
if extended_multizone is False:
|
|
295
|
-
version_major = 2
|
|
296
|
-
version_minor = 60
|
|
297
|
-
else:
|
|
298
|
-
version_major = 3
|
|
299
|
-
version_minor = 70
|
|
300
|
-
|
|
301
|
-
# Override with explicit firmware_version if provided
|
|
302
|
-
if firmware_version is not None:
|
|
303
|
-
version_major, version_minor = firmware_version
|
|
304
|
-
|
|
305
|
-
core = CoreDeviceState(
|
|
306
|
-
serial=serial,
|
|
307
|
-
label=label,
|
|
308
|
-
power_level=65535, # Default to on
|
|
309
|
-
color=default_color,
|
|
310
|
-
vendor=product_info.vendor,
|
|
311
|
-
product=product_info.pid,
|
|
312
|
-
version_major=version_major,
|
|
313
|
-
version_minor=version_minor,
|
|
314
|
-
build_timestamp=int(time.time()),
|
|
315
|
-
mac_address=bytes.fromhex(serial[:12]),
|
|
316
|
-
)
|
|
317
|
-
|
|
318
|
-
# Create network, location, group, and waveform state
|
|
319
|
-
network = NetworkState()
|
|
320
|
-
location = LocationState()
|
|
321
|
-
group = GroupState()
|
|
322
|
-
waveform = WaveformState()
|
|
323
|
-
|
|
324
|
-
# Create capability-specific state objects
|
|
325
|
-
infrared_state = (
|
|
326
|
-
InfraredState(infrared_brightness=16384) if product_info.has_infrared else None
|
|
327
|
-
)
|
|
328
|
-
hev_state = HevState() if product_info.has_hev else None
|
|
329
|
-
|
|
330
|
-
multizone_state = None
|
|
331
|
-
if product_info.has_multizone and zone_count:
|
|
332
|
-
multizone_state = MultiZoneState(
|
|
333
|
-
zone_count=zone_count,
|
|
334
|
-
zone_colors=[], # Will be initialized by EmulatedLifxDevice
|
|
335
|
-
)
|
|
336
|
-
|
|
337
|
-
matrix_state = None
|
|
338
|
-
if product_info.has_matrix and tile_count:
|
|
339
|
-
matrix_state = MatrixState(
|
|
340
|
-
tile_count=tile_count,
|
|
341
|
-
tile_devices=[], # Will be initialized by EmulatedLifxDevice
|
|
342
|
-
tile_width=tile_width or 8,
|
|
343
|
-
tile_height=tile_height or 8,
|
|
344
|
-
)
|
|
345
|
-
|
|
346
|
-
# Determine if device supports extended multizone
|
|
347
|
-
firmware_version_int = (version_major << 16) | version_minor
|
|
348
|
-
has_extended_multizone = product_info.supports_extended_multizone(
|
|
349
|
-
firmware_version_int
|
|
350
|
-
)
|
|
351
|
-
|
|
352
|
-
# Create composed device state
|
|
353
|
-
state = DeviceState(
|
|
354
|
-
core=core,
|
|
355
|
-
network=network,
|
|
356
|
-
location=location,
|
|
357
|
-
group=group,
|
|
358
|
-
waveform=waveform,
|
|
359
|
-
infrared=infrared_state,
|
|
360
|
-
hev=hev_state,
|
|
361
|
-
multizone=multizone_state,
|
|
362
|
-
matrix=matrix_state,
|
|
363
|
-
has_color=product_info.has_color,
|
|
364
|
-
has_infrared=product_info.has_infrared,
|
|
365
|
-
has_multizone=product_info.has_multizone,
|
|
366
|
-
has_extended_multizone=has_extended_multizone,
|
|
367
|
-
has_matrix=product_info.has_matrix,
|
|
368
|
-
has_hev=product_info.has_hev,
|
|
369
|
-
)
|
|
370
|
-
|
|
371
|
-
# Restore saved state if persistence is enabled
|
|
372
|
-
if storage:
|
|
373
|
-
restorer = StateRestorer(storage)
|
|
374
|
-
restorer.restore_if_available(state)
|
|
375
|
-
|
|
376
|
-
return EmulatedLifxDevice(
|
|
377
|
-
state,
|
|
378
|
-
storage=storage,
|
|
379
|
-
scenario_manager=scenario_manager,
|
|
380
|
-
)
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
"""Storage protocol definition for device state persistence.
|
|
2
|
-
|
|
3
|
-
This module defines the common interface that all storage implementations
|
|
4
|
-
must follow, enabling polymorphic use and easier testing.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
from typing import Any, Protocol, runtime_checkable
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@runtime_checkable
|
|
13
|
-
class StorageProtocol(Protocol):
|
|
14
|
-
"""Protocol defining the interface for device state storage.
|
|
15
|
-
|
|
16
|
-
Both synchronous (DeviceStorage) and asynchronous (AsyncDeviceStorage)
|
|
17
|
-
implementations must provide these methods.
|
|
18
|
-
|
|
19
|
-
The protocol allows for polymorphic usage and dependency injection,
|
|
20
|
-
improving testability and adherence to SOLID principles.
|
|
21
|
-
"""
|
|
22
|
-
|
|
23
|
-
def load_device_state(self, serial: str) -> dict[str, Any] | None:
|
|
24
|
-
"""Load device state from persistent storage.
|
|
25
|
-
|
|
26
|
-
This method is synchronous in both implementations because loading
|
|
27
|
-
primarily happens at device initialization where blocking is acceptable.
|
|
28
|
-
|
|
29
|
-
Args:
|
|
30
|
-
serial: Device serial number
|
|
31
|
-
|
|
32
|
-
Returns:
|
|
33
|
-
Dictionary with device state, or None if not found
|
|
34
|
-
"""
|
|
35
|
-
...
|
|
36
|
-
|
|
37
|
-
def delete_device_state(self, serial: str) -> None:
|
|
38
|
-
"""Delete device state from persistent storage.
|
|
39
|
-
|
|
40
|
-
This method is synchronous because deletion is rare and blocking
|
|
41
|
-
is acceptable for this operation.
|
|
42
|
-
|
|
43
|
-
Args:
|
|
44
|
-
serial: Device serial number
|
|
45
|
-
"""
|
|
46
|
-
...
|
|
47
|
-
|
|
48
|
-
def delete_all_device_states(self) -> int:
|
|
49
|
-
"""Delete all device states from persistent storage.
|
|
50
|
-
|
|
51
|
-
This method is synchronous because it's typically used for cleanup
|
|
52
|
-
operations where blocking is acceptable.
|
|
53
|
-
|
|
54
|
-
Returns:
|
|
55
|
-
Number of devices deleted
|
|
56
|
-
"""
|
|
57
|
-
...
|
|
58
|
-
|
|
59
|
-
def list_devices(self) -> list[str]:
|
|
60
|
-
"""List all devices with saved state.
|
|
61
|
-
|
|
62
|
-
This method is synchronous because listing is typically used for
|
|
63
|
-
administrative/query operations where blocking is acceptable.
|
|
64
|
-
|
|
65
|
-
Returns:
|
|
66
|
-
List of device serial numbers
|
|
67
|
-
"""
|
|
68
|
-
...
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
@runtime_checkable
|
|
72
|
-
class AsyncStorageProtocol(StorageProtocol, Protocol):
|
|
73
|
-
"""Extended protocol for asynchronous storage implementations.
|
|
74
|
-
|
|
75
|
-
Adds async save method for high-performance non-blocking writes.
|
|
76
|
-
"""
|
|
77
|
-
|
|
78
|
-
async def save_device_state(self, device_state: Any) -> None:
|
|
79
|
-
"""Queue device state for saving (non-blocking).
|
|
80
|
-
|
|
81
|
-
Args:
|
|
82
|
-
device_state: DeviceState instance to persist
|
|
83
|
-
"""
|
|
84
|
-
...
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
@runtime_checkable
|
|
88
|
-
class SyncStorageProtocol(StorageProtocol, Protocol):
|
|
89
|
-
"""Extended protocol for synchronous storage implementations.
|
|
90
|
-
|
|
91
|
-
Adds synchronous save method for simple blocking writes.
|
|
92
|
-
"""
|
|
93
|
-
|
|
94
|
-
def save_device_state(self, device_state: Any) -> None:
|
|
95
|
-
"""Save device state to persistent storage (blocking).
|
|
96
|
-
|
|
97
|
-
Args:
|
|
98
|
-
device_state: DeviceState instance to persist
|
|
99
|
-
"""
|
|
100
|
-
...
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
lifx_emulator/__init__.py,sha256=WzD3GRp_QFlYF9IexitPxuTVyOUDNv2ZApKhfRP0UGA,818
|
|
2
|
-
lifx_emulator/__main__.py,sha256=Irr5g-OBIoWn6shPzfWIPjJSVM2Sq5Y1R1ETxdhCjx8,22570
|
|
3
|
-
lifx_emulator/api.py,sha256=ATVLtPNh-9TySn_orFV03vJvZ20D5VZyuVQPgvzUuFg,67416
|
|
4
|
-
lifx_emulator/async_storage.py,sha256=-J4s_4Cxika_EDlFjBegKAaM8sY78wbTDWnttWH4Eik,10504
|
|
5
|
-
lifx_emulator/constants.py,sha256=DFZkUsdewE-x_3MgO28tMGkjUCWPeYc3xLj_EXViGOw,1032
|
|
6
|
-
lifx_emulator/device.py,sha256=qaTNgeyvg1S-g1rC91NnQpvDe4p1razMNVDTlv0vKMI,24109
|
|
7
|
-
lifx_emulator/device_states.py,sha256=3juEk1PpBySOwB3XpvH4UqKAPek5ZNCbvepJ3Y97Sck,2855
|
|
8
|
-
lifx_emulator/factories.py,sha256=vLsGFuP8E_9bbdo2Sry3G6Ax6qSl5QfpXsRfXE3SfBU,13017
|
|
9
|
-
lifx_emulator/observers.py,sha256=-KnUgFcKdhlNo7CNVstP-u0wU2W0JAGg055ZPV15Sj0,3874
|
|
10
|
-
lifx_emulator/scenario_manager.py,sha256=xv2NwdZjDcF83MTt7JjKgDvua6zx6SkGQT1P4156-RU,14154
|
|
11
|
-
lifx_emulator/scenario_persistence.py,sha256=oytK5W2si1N-O31jBACHhGQyo5IrINlqfWE0K2bZq_8,7394
|
|
12
|
-
lifx_emulator/server.py,sha256=mWkssl42Z_5m78DAn5_-DRACLGvymSbouTE4j8dVAXA,17239
|
|
13
|
-
lifx_emulator/state_restorer.py,sha256=isgCsmcjxc4aQrTXoIYr_FoIU4H5Swg7GNaoh_-X7zU,9793
|
|
14
|
-
lifx_emulator/state_serializer.py,sha256=O4Cp3bbGkd4eZf5jzb0MKzWDTgiNhrSGgypmMWaB4dg,5097
|
|
15
|
-
lifx_emulator/storage_protocol.py,sha256=j5wC4gI-2DtPgmY4kM3mKDI6OYpmiNpGulzdJnZnqYY,2929
|
|
16
|
-
lifx_emulator/handlers/__init__.py,sha256=3Hj1hRo3yL3E7GKwG9TaYh33ymk_N3bRiQ8nvqSQULA,1306
|
|
17
|
-
lifx_emulator/handlers/base.py,sha256=Kn_OoVOQPlJOkl-xiHRYsoNKsS054sxk7sZ8rI9NvXQ,1653
|
|
18
|
-
lifx_emulator/handlers/device_handlers.py,sha256=MDUanFFDT9RWBcWtEZ3sXeGmoYjFW50T4zVIkkK2iB4,10239
|
|
19
|
-
lifx_emulator/handlers/light_handlers.py,sha256=ycwOAD6skmBLVUlyeg5kBprHDlrllP-_0JRzQrDGFos,11778
|
|
20
|
-
lifx_emulator/handlers/multizone_handlers.py,sha256=949FEt1Ilhp-LCxxY0xfHxYJccGNy_xnNhO14ePENaw,7913
|
|
21
|
-
lifx_emulator/handlers/registry.py,sha256=s1ht4PmPhXhAcwu1hoY4yW39wy3SPJBMY-9Uxd0FWuE,3292
|
|
22
|
-
lifx_emulator/handlers/tile_handlers.py,sha256=jzguqNr_aZheuqd4mikfQlkRcVeqs3HAEAseFSlkBlE,10038
|
|
23
|
-
lifx_emulator/products/__init__.py,sha256=qcNop_kRYFF3zSjNemzQEgu3jPrIxfyQyLv9GsnaLEI,627
|
|
24
|
-
lifx_emulator/products/generator.py,sha256=ovXSwh-3bFNeNJJntYDSURUrzM-WAtXeGKI0-sP1miI,26431
|
|
25
|
-
lifx_emulator/products/registry.py,sha256=oKO7Nb4KsWMMc3bkr5FWKgdXsi4xbrRCD9xP27rQPPQ,45757
|
|
26
|
-
lifx_emulator/products/specs.py,sha256=2bpgr2z2Ugb_0IBl9LY8LGotB031Bim8zizwvtviFZ0,7186
|
|
27
|
-
lifx_emulator/products/specs.yml,sha256=uxzdKFREAHphk8XSPiCHvQE2vwoPfT2m1xy-zC4ZIl4,8552
|
|
28
|
-
lifx_emulator/protocol/__init__.py,sha256=-wjC-wBcb7fxi5I-mJr2Ad8K2YRflJFdLLdobfD-W1Q,56
|
|
29
|
-
lifx_emulator/protocol/base.py,sha256=OH2fLGJCAFYCgaxkKB3-pLclRaCIMIQtLtlgS_Qe4Ko,12090
|
|
30
|
-
lifx_emulator/protocol/const.py,sha256=ilhv-KcQpHtKh2MDCaIbMLQAsxKO_uTaxyR63v1W8cc,226
|
|
31
|
-
lifx_emulator/protocol/generator.py,sha256=KuNuWUUNikBRudcnBNIFcJ_2h2zliceVmyxfyyHIpXc,48980
|
|
32
|
-
lifx_emulator/protocol/header.py,sha256=RXMJ5YZG1jyxl4Mz46ZGJBYX41Jdp7J95BHuY-scYC0,5499
|
|
33
|
-
lifx_emulator/protocol/packets.py,sha256=_xYYPuu6WfBpg1LwYVdzQdytFwTyMqpZRadmDdwZXWk,41567
|
|
34
|
-
lifx_emulator/protocol/protocol_types.py,sha256=2Mccm9717EuTXQYaW44W_yReI4EtnlPp3-WEVASgdGY,24820
|
|
35
|
-
lifx_emulator/protocol/serializer.py,sha256=2bZz7TddxaMRO4_6LujRGCS1w7GxD4E3rRk3r-hpEIE,10738
|
|
36
|
-
lifx_emulator-1.0.2.dist-info/METADATA,sha256=gASUaAnvN-dLyu3rBoyYb4lYq_guVFwTXznmTOzAAvM,4549
|
|
37
|
-
lifx_emulator-1.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
38
|
-
lifx_emulator-1.0.2.dist-info/entry_points.txt,sha256=R9C_K_tTgt6yXEmhzH4r2Yx2Tu1rLlnYzeG4RFUVzSc,62
|
|
39
|
-
lifx_emulator-1.0.2.dist-info/licenses/LICENSE,sha256=eBz48GRA3gSiWn3rYZAz2Ewp35snnhV9cSqkVBq7g3k,1832
|
|
40
|
-
lifx_emulator-1.0.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|