lifx-emulator 1.0.2__py3-none-any.whl → 2.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.
- 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 +346 -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 +31 -11
- 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 +175 -63
- lifx_emulator/protocol/generator.py +18 -5
- lifx_emulator/protocol/packets.py +7 -7
- lifx_emulator/protocol/protocol_types.py +35 -62
- 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 +42 -66
- {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.1.0.dist-info}/METADATA +1 -1
- lifx_emulator-2.1.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.1.0.dist-info}/WHEEL +0 -0
- {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.1.0.dist-info}/entry_points.txt +0 -0
- {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.1.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Factory functions for creating emulated LIFX devices."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from lifx_emulator.devices import EmulatedLifxDevice
|
|
8
|
+
from lifx_emulator.factories.builder import DeviceBuilder
|
|
9
|
+
from lifx_emulator.products.registry import get_product
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from lifx_emulator.devices import DevicePersistenceAsyncFile
|
|
13
|
+
from lifx_emulator.scenarios import HierarchicalScenarioManager
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_color_light(
|
|
17
|
+
serial: str | None = None,
|
|
18
|
+
firmware_version: tuple[int, int] | None = None,
|
|
19
|
+
storage: DevicePersistenceAsyncFile | None = None,
|
|
20
|
+
scenario_manager: HierarchicalScenarioManager | None = None,
|
|
21
|
+
) -> EmulatedLifxDevice:
|
|
22
|
+
"""Create a regular color light (LIFX Color)"""
|
|
23
|
+
return create_device(
|
|
24
|
+
91,
|
|
25
|
+
serial=serial,
|
|
26
|
+
firmware_version=firmware_version,
|
|
27
|
+
storage=storage,
|
|
28
|
+
scenario_manager=scenario_manager,
|
|
29
|
+
) # LIFX Color
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def create_infrared_light(
|
|
33
|
+
serial: str | None = None,
|
|
34
|
+
firmware_version: tuple[int, int] | None = None,
|
|
35
|
+
storage: DevicePersistenceAsyncFile | None = None,
|
|
36
|
+
scenario_manager: HierarchicalScenarioManager | None = None,
|
|
37
|
+
) -> EmulatedLifxDevice:
|
|
38
|
+
"""Create an infrared-enabled light (LIFX A19 Night Vision)"""
|
|
39
|
+
return create_device(
|
|
40
|
+
29,
|
|
41
|
+
serial=serial,
|
|
42
|
+
firmware_version=firmware_version,
|
|
43
|
+
storage=storage,
|
|
44
|
+
scenario_manager=scenario_manager,
|
|
45
|
+
) # LIFX A19 Night Vision
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def create_hev_light(
|
|
49
|
+
serial: str | None = None,
|
|
50
|
+
firmware_version: tuple[int, int] | None = None,
|
|
51
|
+
storage: DevicePersistenceAsyncFile | None = None,
|
|
52
|
+
scenario_manager: HierarchicalScenarioManager | None = None,
|
|
53
|
+
) -> EmulatedLifxDevice:
|
|
54
|
+
"""Create an HEV-enabled light (LIFX Clean)"""
|
|
55
|
+
return create_device(
|
|
56
|
+
90,
|
|
57
|
+
serial=serial,
|
|
58
|
+
firmware_version=firmware_version,
|
|
59
|
+
storage=storage,
|
|
60
|
+
scenario_manager=scenario_manager,
|
|
61
|
+
) # LIFX Clean
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def create_multizone_light(
|
|
65
|
+
serial: str | None = None,
|
|
66
|
+
zone_count: int | None = None,
|
|
67
|
+
extended_multizone: bool = True,
|
|
68
|
+
firmware_version: tuple[int, int] | None = None,
|
|
69
|
+
storage: DevicePersistenceAsyncFile | None = None,
|
|
70
|
+
scenario_manager: HierarchicalScenarioManager | None = None,
|
|
71
|
+
) -> EmulatedLifxDevice:
|
|
72
|
+
"""Create a multizone light (LIFX Beam)
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
serial: Optional serial
|
|
76
|
+
zone_count: Optional zone count (uses product default if not specified)
|
|
77
|
+
extended_multizone: enables support for extended multizone requests
|
|
78
|
+
firmware_version: Optional firmware version tuple (major, minor)
|
|
79
|
+
storage: Optional storage for persistence
|
|
80
|
+
scenario_manager: Optional scenario manager
|
|
81
|
+
"""
|
|
82
|
+
return create_device(
|
|
83
|
+
38,
|
|
84
|
+
serial=serial,
|
|
85
|
+
zone_count=zone_count,
|
|
86
|
+
extended_multizone=extended_multizone,
|
|
87
|
+
firmware_version=firmware_version,
|
|
88
|
+
storage=storage,
|
|
89
|
+
scenario_manager=scenario_manager,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def create_tile_device(
|
|
94
|
+
serial: str | None = None,
|
|
95
|
+
tile_count: int | None = None,
|
|
96
|
+
tile_width: int | None = None,
|
|
97
|
+
tile_height: int | None = None,
|
|
98
|
+
firmware_version: tuple[int, int] | None = None,
|
|
99
|
+
storage: DevicePersistenceAsyncFile | None = None,
|
|
100
|
+
scenario_manager: HierarchicalScenarioManager | None = None,
|
|
101
|
+
) -> EmulatedLifxDevice:
|
|
102
|
+
"""Create a tile device (LIFX Tile)
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
serial: Optional serial
|
|
106
|
+
tile_count: Optional tile count (uses product default)
|
|
107
|
+
tile_width: Optional tile width in pixels (uses product default)
|
|
108
|
+
tile_height: Optional tile height in pixels (uses product default)
|
|
109
|
+
firmware_version: Optional firmware version tuple (major, minor)
|
|
110
|
+
storage: Optional storage for persistence
|
|
111
|
+
scenario_manager: Optional scenario manager
|
|
112
|
+
"""
|
|
113
|
+
return create_device(
|
|
114
|
+
55,
|
|
115
|
+
serial=serial,
|
|
116
|
+
tile_count=tile_count,
|
|
117
|
+
tile_width=tile_width,
|
|
118
|
+
tile_height=tile_height,
|
|
119
|
+
firmware_version=firmware_version,
|
|
120
|
+
storage=storage,
|
|
121
|
+
scenario_manager=scenario_manager,
|
|
122
|
+
) # LIFX Tile
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def create_color_temperature_light(
|
|
126
|
+
serial: str | None = None,
|
|
127
|
+
firmware_version: tuple[int, int] | None = None,
|
|
128
|
+
storage: DevicePersistenceAsyncFile | None = None,
|
|
129
|
+
scenario_manager: HierarchicalScenarioManager | None = None,
|
|
130
|
+
) -> EmulatedLifxDevice:
|
|
131
|
+
"""Create a color temperature light (LIFX Mini White to Warm).
|
|
132
|
+
|
|
133
|
+
Variable color temperature, no RGB.
|
|
134
|
+
"""
|
|
135
|
+
return create_device(
|
|
136
|
+
50,
|
|
137
|
+
serial=serial,
|
|
138
|
+
firmware_version=firmware_version,
|
|
139
|
+
storage=storage,
|
|
140
|
+
scenario_manager=scenario_manager,
|
|
141
|
+
) # LIFX Mini White to Warm
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def create_device(
|
|
145
|
+
product_id: int,
|
|
146
|
+
serial: str | None = None,
|
|
147
|
+
zone_count: int | None = None,
|
|
148
|
+
extended_multizone: bool | None = None,
|
|
149
|
+
tile_count: int | None = None,
|
|
150
|
+
tile_width: int | None = None,
|
|
151
|
+
tile_height: int | None = None,
|
|
152
|
+
firmware_version: tuple[int, int] | None = None,
|
|
153
|
+
storage: DevicePersistenceAsyncFile | None = None,
|
|
154
|
+
scenario_manager: HierarchicalScenarioManager | None = None,
|
|
155
|
+
) -> EmulatedLifxDevice:
|
|
156
|
+
"""Create a device for any LIFX product using the product registry.
|
|
157
|
+
|
|
158
|
+
This function uses the DeviceBuilder pattern to construct devices with
|
|
159
|
+
clean separation of concerns and testable components.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
product_id: Product ID from the LIFX product registry
|
|
163
|
+
serial: Optional serial (auto-generated if not provided)
|
|
164
|
+
zone_count: Number of zones for multizone devices (auto-determined)
|
|
165
|
+
extended_multizone: Enable extended multizone requests
|
|
166
|
+
tile_count: Number of tiles for matrix devices (default: 5)
|
|
167
|
+
tile_width: Width of each tile in pixels (default: 8)
|
|
168
|
+
tile_height: Height of each tile in pixels (default: 8)
|
|
169
|
+
firmware_version: Optional firmware version tuple (major, minor).
|
|
170
|
+
If not specified, uses 3.70 for extended_multizone
|
|
171
|
+
or 2.60 otherwise
|
|
172
|
+
storage: Optional storage for persistence
|
|
173
|
+
scenario_manager: Optional scenario manager for testing
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
EmulatedLifxDevice configured for the specified product
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
ValueError: If product_id is not found in registry
|
|
180
|
+
|
|
181
|
+
Examples:
|
|
182
|
+
>>> # Create LIFX A19 (PID 27)
|
|
183
|
+
>>> device = create_device(27)
|
|
184
|
+
>>> # Create LIFX Z strip (PID 32) with 24 zones
|
|
185
|
+
>>> strip = create_device(32, zone_count=24)
|
|
186
|
+
>>> # Create LIFX Tile (PID 55) with 10 tiles
|
|
187
|
+
>>> tiles = create_device(55, tile_count=10)
|
|
188
|
+
"""
|
|
189
|
+
# Get product info from registry
|
|
190
|
+
product_info = get_product(product_id)
|
|
191
|
+
if product_info is None:
|
|
192
|
+
raise ValueError(f"Unknown product ID: {product_id}")
|
|
193
|
+
|
|
194
|
+
# Build device using builder pattern
|
|
195
|
+
builder = DeviceBuilder(product_info)
|
|
196
|
+
|
|
197
|
+
if serial is not None:
|
|
198
|
+
builder.with_serial(serial)
|
|
199
|
+
|
|
200
|
+
if zone_count is not None:
|
|
201
|
+
builder.with_zone_count(zone_count)
|
|
202
|
+
|
|
203
|
+
if extended_multizone is not None:
|
|
204
|
+
builder.with_extended_multizone(extended_multizone)
|
|
205
|
+
|
|
206
|
+
if tile_count is not None:
|
|
207
|
+
builder.with_tile_count(tile_count)
|
|
208
|
+
|
|
209
|
+
if tile_width is not None and tile_height is not None:
|
|
210
|
+
builder.with_tile_dimensions(tile_width, tile_height)
|
|
211
|
+
|
|
212
|
+
if firmware_version is not None:
|
|
213
|
+
builder.with_firmware_version(*firmware_version)
|
|
214
|
+
|
|
215
|
+
if storage is not None:
|
|
216
|
+
builder.with_storage(storage)
|
|
217
|
+
|
|
218
|
+
if scenario_manager is not None:
|
|
219
|
+
builder.with_scenario_manager(scenario_manager)
|
|
220
|
+
|
|
221
|
+
return builder.build()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Firmware version configuration for devices."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FirmwareConfig:
|
|
7
|
+
"""Determines firmware versions for devices.
|
|
8
|
+
|
|
9
|
+
Extended multizone support requires firmware 3.70+.
|
|
10
|
+
Devices without extended multizone use firmware 2.60.
|
|
11
|
+
|
|
12
|
+
Examples:
|
|
13
|
+
>>> config = FirmwareConfig()
|
|
14
|
+
>>> major, minor = config.get_firmware_version(extended_multizone=True)
|
|
15
|
+
>>> (major, minor)
|
|
16
|
+
(3, 70)
|
|
17
|
+
>>> major, minor = config.get_firmware_version(extended_multizone=False)
|
|
18
|
+
>>> (major, minor)
|
|
19
|
+
(2, 60)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# Firmware versions
|
|
23
|
+
VERSION_EXTENDED = (3, 70) # Extended multizone support
|
|
24
|
+
VERSION_LEGACY = (2, 60) # Legacy firmware
|
|
25
|
+
|
|
26
|
+
def get_firmware_version(
|
|
27
|
+
self,
|
|
28
|
+
extended_multizone: bool | None = None,
|
|
29
|
+
override: tuple[int, int] | None = None,
|
|
30
|
+
) -> tuple[int, int]:
|
|
31
|
+
"""Get firmware version based on extended multizone support.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
extended_multizone: Whether device supports extended multizone.
|
|
35
|
+
None or True defaults to 3.70, False gives 2.60
|
|
36
|
+
override: Optional explicit firmware version to use
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Tuple of (major, minor) firmware version
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
>>> config = FirmwareConfig()
|
|
43
|
+
>>> config.get_firmware_version(extended_multizone=True)
|
|
44
|
+
(3, 70)
|
|
45
|
+
>>> config.get_firmware_version(extended_multizone=False)
|
|
46
|
+
(2, 60)
|
|
47
|
+
>>> config.get_firmware_version(override=(4, 0))
|
|
48
|
+
(4, 0)
|
|
49
|
+
"""
|
|
50
|
+
# Explicit override takes precedence
|
|
51
|
+
if override is not None:
|
|
52
|
+
return override
|
|
53
|
+
|
|
54
|
+
# None or True defaults to extended (3.70)
|
|
55
|
+
# Only explicit False gives legacy (2.60)
|
|
56
|
+
if extended_multizone is False:
|
|
57
|
+
return self.VERSION_LEGACY
|
|
58
|
+
else:
|
|
59
|
+
return self.VERSION_EXTENDED
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Serial number generation service for LIFX devices."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import random
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from lifx_emulator.products.registry import ProductInfo
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SerialGenerator:
|
|
13
|
+
"""Generates serial numbers for emulated LIFX devices.
|
|
14
|
+
|
|
15
|
+
Serial numbers are 12-character hex strings with different prefixes
|
|
16
|
+
based on device capabilities for easier identification.
|
|
17
|
+
|
|
18
|
+
Prefixes:
|
|
19
|
+
- d073d9: Matrix/Tile devices
|
|
20
|
+
- d073d8: Multizone devices (strips/beams)
|
|
21
|
+
- d073d7: HEV devices
|
|
22
|
+
- d073d6: Infrared devices
|
|
23
|
+
- d073d5: Regular color/temperature lights
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
>>> generator = SerialGenerator()
|
|
27
|
+
>>> serial = generator.generate(product_info)
|
|
28
|
+
>>> len(serial)
|
|
29
|
+
12
|
|
30
|
+
>>> serial.startswith("d073d")
|
|
31
|
+
True
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# Device type prefixes for easy identification
|
|
35
|
+
PREFIX_MATRIX = "d073d9"
|
|
36
|
+
PREFIX_MULTIZONE = "d073d8"
|
|
37
|
+
PREFIX_HEV = "d073d7"
|
|
38
|
+
PREFIX_INFRARED = "d073d6"
|
|
39
|
+
PREFIX_DEFAULT = "d073d5"
|
|
40
|
+
|
|
41
|
+
def generate(self, product_info: ProductInfo) -> str:
|
|
42
|
+
"""Generate a serial number based on product capabilities.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
product_info: Product information from registry
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
12-character hex serial number
|
|
49
|
+
|
|
50
|
+
Examples:
|
|
51
|
+
>>> from lifx_emulator.products.registry import get_product
|
|
52
|
+
>>> generator = SerialGenerator()
|
|
53
|
+
>>> product = get_product(55) # LIFX Tile
|
|
54
|
+
>>> serial = generator.generate(product)
|
|
55
|
+
>>> serial.startswith("d073d9") # Matrix prefix
|
|
56
|
+
True
|
|
57
|
+
"""
|
|
58
|
+
prefix = self._determine_prefix(product_info)
|
|
59
|
+
suffix = random.randint(100000, 999999) # nosec
|
|
60
|
+
return f"{prefix}{suffix:06x}"
|
|
61
|
+
|
|
62
|
+
def _determine_prefix(self, product_info: ProductInfo) -> str:
|
|
63
|
+
"""Determine the prefix based on product capabilities.
|
|
64
|
+
|
|
65
|
+
Precedence: matrix > multizone > hev > infrared > default
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
product_info: Product information from registry
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
6-character hex prefix
|
|
72
|
+
"""
|
|
73
|
+
if product_info.has_matrix:
|
|
74
|
+
return self.PREFIX_MATRIX
|
|
75
|
+
elif product_info.has_multizone:
|
|
76
|
+
return self.PREFIX_MULTIZONE
|
|
77
|
+
elif product_info.has_hev:
|
|
78
|
+
return self.PREFIX_HEV
|
|
79
|
+
elif product_info.has_infrared:
|
|
80
|
+
return self.PREFIX_INFRARED
|
|
81
|
+
else:
|
|
82
|
+
return self.PREFIX_DEFAULT
|
lifx_emulator/handlers/base.py
CHANGED
|
@@ -11,7 +11,7 @@ from lifx_emulator.protocol.packets import Device
|
|
|
11
11
|
from lifx_emulator.protocol.protocol_types import DeviceService as ProtocolDeviceService
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
|
-
from lifx_emulator.
|
|
14
|
+
from lifx_emulator.devices import DeviceState
|
|
15
15
|
|
|
16
16
|
logger = logging.getLogger(__name__)
|
|
17
17
|
|
|
@@ -75,8 +75,7 @@ class GetLabelHandler(PacketHandler):
|
|
|
75
75
|
def handle(
|
|
76
76
|
self, device_state: DeviceState, packet: Any | None, res_required: bool
|
|
77
77
|
) -> list[Any]:
|
|
78
|
-
|
|
79
|
-
return [Device.StateLabel(label=label_bytes)]
|
|
78
|
+
return [Device.StateLabel(label=device_state.label)]
|
|
80
79
|
|
|
81
80
|
|
|
82
81
|
class SetLabelHandler(PacketHandler):
|
|
@@ -91,14 +90,11 @@ class SetLabelHandler(PacketHandler):
|
|
|
91
90
|
res_required: bool,
|
|
92
91
|
) -> list[Any]:
|
|
93
92
|
if packet:
|
|
94
|
-
device_state.label = packet.label
|
|
95
|
-
"utf-8", errors="replace"
|
|
96
|
-
)
|
|
93
|
+
device_state.label = packet.label
|
|
97
94
|
logger.info("Label set to '%s'", device_state.label)
|
|
98
95
|
|
|
99
96
|
if res_required:
|
|
100
|
-
|
|
101
|
-
return [Device.StateLabel(label=label_bytes)]
|
|
97
|
+
return [Device.StateLabel(label=device_state.label)]
|
|
102
98
|
return []
|
|
103
99
|
|
|
104
100
|
|
|
@@ -169,13 +165,10 @@ class GetLocationHandler(PacketHandler):
|
|
|
169
165
|
def handle(
|
|
170
166
|
self, device_state: DeviceState, packet: Any | None, res_required: bool
|
|
171
167
|
) -> list[Any]:
|
|
172
|
-
label_bytes = device_state.location_label.encode("utf-8")[:32].ljust(
|
|
173
|
-
32, b"\x00"
|
|
174
|
-
)
|
|
175
168
|
return [
|
|
176
169
|
Device.StateLocation(
|
|
177
170
|
location=device_state.location_id,
|
|
178
|
-
label=
|
|
171
|
+
label=device_state.location_label,
|
|
179
172
|
updated_at=device_state.location_updated_at,
|
|
180
173
|
)
|
|
181
174
|
]
|
|
@@ -194,9 +187,7 @@ class SetLocationHandler(PacketHandler):
|
|
|
194
187
|
) -> list[Any]:
|
|
195
188
|
if packet:
|
|
196
189
|
device_state.location_id = packet.location
|
|
197
|
-
device_state.location_label = packet.label
|
|
198
|
-
"utf-8", errors="replace"
|
|
199
|
-
)
|
|
190
|
+
device_state.location_label = packet.label
|
|
200
191
|
device_state.location_updated_at = packet.updated_at
|
|
201
192
|
loc_id = packet.location.hex()[:8]
|
|
202
193
|
logger.info(
|
|
@@ -204,13 +195,10 @@ class SetLocationHandler(PacketHandler):
|
|
|
204
195
|
)
|
|
205
196
|
|
|
206
197
|
if res_required:
|
|
207
|
-
label_bytes = device_state.location_label.encode("utf-8")[:32].ljust(
|
|
208
|
-
32, b"\x00"
|
|
209
|
-
)
|
|
210
198
|
return [
|
|
211
199
|
Device.StateLocation(
|
|
212
200
|
location=device_state.location_id,
|
|
213
|
-
label=
|
|
201
|
+
label=device_state.location_label,
|
|
214
202
|
updated_at=device_state.location_updated_at,
|
|
215
203
|
)
|
|
216
204
|
]
|
|
@@ -225,11 +213,10 @@ class GetGroupHandler(PacketHandler):
|
|
|
225
213
|
def handle(
|
|
226
214
|
self, device_state: DeviceState, packet: Any | None, res_required: bool
|
|
227
215
|
) -> list[Any]:
|
|
228
|
-
label_bytes = device_state.group_label.encode("utf-8")[:32].ljust(32, b"\x00")
|
|
229
216
|
return [
|
|
230
217
|
Device.StateGroup(
|
|
231
218
|
group=device_state.group_id,
|
|
232
|
-
label=
|
|
219
|
+
label=device_state.group_label,
|
|
233
220
|
updated_at=device_state.group_updated_at,
|
|
234
221
|
)
|
|
235
222
|
]
|
|
@@ -248,9 +235,7 @@ class SetGroupHandler(PacketHandler):
|
|
|
248
235
|
) -> list[Any]:
|
|
249
236
|
if packet:
|
|
250
237
|
device_state.group_id = packet.group
|
|
251
|
-
device_state.group_label = packet.label
|
|
252
|
-
"utf-8", errors="replace"
|
|
253
|
-
)
|
|
238
|
+
device_state.group_label = packet.label
|
|
254
239
|
device_state.group_updated_at = packet.updated_at
|
|
255
240
|
grp_id = packet.group.hex()[:8]
|
|
256
241
|
logger.info(
|
|
@@ -258,13 +243,10 @@ class SetGroupHandler(PacketHandler):
|
|
|
258
243
|
)
|
|
259
244
|
|
|
260
245
|
if res_required:
|
|
261
|
-
label_bytes = device_state.group_label.encode("utf-8")[:32].ljust(
|
|
262
|
-
32, b"\x00"
|
|
263
|
-
)
|
|
264
246
|
return [
|
|
265
247
|
Device.StateGroup(
|
|
266
248
|
group=device_state.group_id,
|
|
267
|
-
label=
|
|
249
|
+
label=device_state.group_label,
|
|
268
250
|
updated_at=device_state.group_updated_at,
|
|
269
251
|
)
|
|
270
252
|
]
|
|
@@ -10,7 +10,7 @@ from lifx_emulator.protocol.packets import Light
|
|
|
10
10
|
from lifx_emulator.protocol.protocol_types import LightLastHevCycleResult
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
|
-
from lifx_emulator.
|
|
13
|
+
from lifx_emulator.devices import DeviceState
|
|
14
14
|
|
|
15
15
|
logger = logging.getLogger(__name__)
|
|
16
16
|
|
|
@@ -23,12 +23,11 @@ class GetColorHandler(PacketHandler):
|
|
|
23
23
|
def handle(
|
|
24
24
|
self, device_state: DeviceState, packet: Any | None, res_required: bool
|
|
25
25
|
) -> list[Any]:
|
|
26
|
-
label_bytes = device_state.label.encode("utf-8")[:32].ljust(32, b"\x00")
|
|
27
26
|
return [
|
|
28
27
|
Light.StateColor(
|
|
29
28
|
color=device_state.color,
|
|
30
29
|
power=device_state.power_level,
|
|
31
|
-
label=
|
|
30
|
+
label=device_state.label,
|
|
32
31
|
)
|
|
33
32
|
]
|
|
34
33
|
|
|
@@ -53,12 +52,11 @@ class SetColorHandler(PacketHandler):
|
|
|
53
52
|
)
|
|
54
53
|
|
|
55
54
|
if res_required:
|
|
56
|
-
label_bytes = device_state.label.encode("utf-8")[:32].ljust(32, b"\x00")
|
|
57
55
|
return [
|
|
58
56
|
Light.StateColor(
|
|
59
57
|
color=device_state.color,
|
|
60
58
|
power=device_state.power_level,
|
|
61
|
-
label=
|
|
59
|
+
label=device_state.label,
|
|
62
60
|
)
|
|
63
61
|
]
|
|
64
62
|
return []
|
|
@@ -129,12 +127,11 @@ class SetWaveformHandler(PacketHandler):
|
|
|
129
127
|
)
|
|
130
128
|
|
|
131
129
|
if res_required:
|
|
132
|
-
label_bytes = device_state.label.encode("utf-8")[:32].ljust(32, b"\x00")
|
|
133
130
|
return [
|
|
134
131
|
Light.StateColor(
|
|
135
132
|
color=device_state.color,
|
|
136
133
|
power=device_state.power_level,
|
|
137
|
-
label=
|
|
134
|
+
label=device_state.label,
|
|
138
135
|
)
|
|
139
136
|
]
|
|
140
137
|
return []
|
|
@@ -183,12 +180,11 @@ class SetWaveformOptionalHandler(PacketHandler):
|
|
|
183
180
|
)
|
|
184
181
|
|
|
185
182
|
if res_required:
|
|
186
|
-
label_bytes = device_state.label.encode("utf-8")[:32].ljust(32, b"\x00")
|
|
187
183
|
return [
|
|
188
184
|
Light.StateColor(
|
|
189
185
|
color=device_state.color,
|
|
190
186
|
power=device_state.power_level,
|
|
191
|
-
label=
|
|
187
|
+
label=device_state.label,
|
|
192
188
|
)
|
|
193
189
|
]
|
|
194
190
|
return []
|
|
@@ -19,7 +19,7 @@ from lifx_emulator.protocol.protocol_types import (
|
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
if TYPE_CHECKING:
|
|
22
|
-
from lifx_emulator.
|
|
22
|
+
from lifx_emulator.devices import DeviceState
|
|
23
23
|
|
|
24
24
|
logger = logging.getLogger(__name__)
|
|
25
25
|
|
|
@@ -237,16 +237,24 @@ class GetEffectHandler(PacketHandler):
|
|
|
237
237
|
while len(palette) < 16:
|
|
238
238
|
palette.append(LightHsbk(hue=0, saturation=0, brightness=0, kelvin=3500))
|
|
239
239
|
|
|
240
|
-
# Create effect settings
|
|
240
|
+
# Create effect settings with Sky parameters
|
|
241
|
+
from lifx_emulator.protocol.protocol_types import TileEffectSkyType
|
|
242
|
+
|
|
243
|
+
# Use defaults for SKY effect (type=5), otherwise use stored values
|
|
244
|
+
effect_type = device_state.tile_effect_type
|
|
245
|
+
if effect_type == 5: # SKY effect
|
|
246
|
+
sky_type = device_state.tile_effect_sky_type or 2 # Default to CLOUDS
|
|
247
|
+
cloud_sat_min = device_state.tile_effect_cloud_sat_min or 50
|
|
248
|
+
cloud_sat_max = device_state.tile_effect_cloud_sat_max or 180
|
|
249
|
+
else:
|
|
250
|
+
sky_type = device_state.tile_effect_sky_type
|
|
251
|
+
cloud_sat_min = device_state.tile_effect_cloud_sat_min
|
|
252
|
+
cloud_sat_max = device_state.tile_effect_cloud_sat_max
|
|
253
|
+
|
|
241
254
|
parameter = TileEffectParameter(
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
parameter3=0,
|
|
246
|
-
parameter4=0,
|
|
247
|
-
parameter5=0,
|
|
248
|
-
parameter6=0,
|
|
249
|
-
parameter7=0,
|
|
255
|
+
sky_type=TileEffectSkyType(sky_type),
|
|
256
|
+
cloud_saturation_min=cloud_sat_min,
|
|
257
|
+
cloud_saturation_max=cloud_sat_max,
|
|
250
258
|
)
|
|
251
259
|
settings = TileEffectSettings(
|
|
252
260
|
instanceid=0,
|
|
@@ -285,10 +293,22 @@ class SetEffectHandler(PacketHandler):
|
|
|
285
293
|
)
|
|
286
294
|
device_state.tile_effect_palette_count = packet.settings.palette_count
|
|
287
295
|
|
|
296
|
+
# Save Sky effect parameters
|
|
297
|
+
device_state.tile_effect_sky_type = int(packet.settings.parameter.sky_type)
|
|
298
|
+
device_state.tile_effect_cloud_sat_min = (
|
|
299
|
+
packet.settings.parameter.cloud_saturation_min
|
|
300
|
+
)
|
|
301
|
+
device_state.tile_effect_cloud_sat_max = (
|
|
302
|
+
packet.settings.parameter.cloud_saturation_max
|
|
303
|
+
)
|
|
304
|
+
|
|
288
305
|
logger.info(
|
|
289
306
|
f"Tile effect set: type={packet.settings.type}, "
|
|
290
307
|
f"speed={packet.settings.speed}ms, "
|
|
291
|
-
f"palette_count={packet.settings.palette_count}"
|
|
308
|
+
f"palette_count={packet.settings.palette_count}, "
|
|
309
|
+
f"sky_type={packet.settings.parameter.sky_type}, "
|
|
310
|
+
f"cloud_sat=[{packet.settings.parameter.cloud_saturation_min}, "
|
|
311
|
+
f"{packet.settings.parameter.cloud_saturation_max}]"
|
|
292
312
|
)
|
|
293
313
|
|
|
294
314
|
if res_required:
|