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.
Files changed (58) hide show
  1. lifx_emulator/__init__.py +1 -1
  2. lifx_emulator/__main__.py +26 -51
  3. lifx_emulator/api/__init__.py +18 -0
  4. lifx_emulator/api/app.py +154 -0
  5. lifx_emulator/api/mappers/__init__.py +5 -0
  6. lifx_emulator/api/mappers/device_mapper.py +114 -0
  7. lifx_emulator/api/models.py +133 -0
  8. lifx_emulator/api/routers/__init__.py +11 -0
  9. lifx_emulator/api/routers/devices.py +130 -0
  10. lifx_emulator/api/routers/monitoring.py +52 -0
  11. lifx_emulator/api/routers/scenarios.py +247 -0
  12. lifx_emulator/api/services/__init__.py +8 -0
  13. lifx_emulator/api/services/device_service.py +198 -0
  14. lifx_emulator/{api.py → api/templates/dashboard.html} +0 -942
  15. lifx_emulator/devices/__init__.py +37 -0
  16. lifx_emulator/devices/device.py +333 -0
  17. lifx_emulator/devices/manager.py +256 -0
  18. lifx_emulator/{async_storage.py → devices/persistence.py} +3 -3
  19. lifx_emulator/{state_restorer.py → devices/state_restorer.py} +2 -2
  20. lifx_emulator/devices/states.py +346 -0
  21. lifx_emulator/factories/__init__.py +37 -0
  22. lifx_emulator/factories/builder.py +371 -0
  23. lifx_emulator/factories/default_config.py +158 -0
  24. lifx_emulator/factories/factory.py +221 -0
  25. lifx_emulator/factories/firmware_config.py +59 -0
  26. lifx_emulator/factories/serial_generator.py +82 -0
  27. lifx_emulator/handlers/base.py +1 -1
  28. lifx_emulator/handlers/device_handlers.py +10 -28
  29. lifx_emulator/handlers/light_handlers.py +5 -9
  30. lifx_emulator/handlers/multizone_handlers.py +1 -1
  31. lifx_emulator/handlers/tile_handlers.py +31 -11
  32. lifx_emulator/products/generator.py +389 -170
  33. lifx_emulator/products/registry.py +52 -40
  34. lifx_emulator/products/specs.py +12 -13
  35. lifx_emulator/protocol/base.py +175 -63
  36. lifx_emulator/protocol/generator.py +18 -5
  37. lifx_emulator/protocol/packets.py +7 -7
  38. lifx_emulator/protocol/protocol_types.py +35 -62
  39. lifx_emulator/repositories/__init__.py +22 -0
  40. lifx_emulator/repositories/device_repository.py +155 -0
  41. lifx_emulator/repositories/storage_backend.py +107 -0
  42. lifx_emulator/scenarios/__init__.py +22 -0
  43. lifx_emulator/{scenario_manager.py → scenarios/manager.py} +11 -91
  44. lifx_emulator/scenarios/models.py +112 -0
  45. lifx_emulator/{scenario_persistence.py → scenarios/persistence.py} +82 -47
  46. lifx_emulator/server.py +42 -66
  47. {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.1.0.dist-info}/METADATA +1 -1
  48. lifx_emulator-2.1.0.dist-info/RECORD +62 -0
  49. lifx_emulator/device.py +0 -750
  50. lifx_emulator/device_states.py +0 -114
  51. lifx_emulator/factories.py +0 -380
  52. lifx_emulator/storage_protocol.py +0 -100
  53. lifx_emulator-1.0.2.dist-info/RECORD +0 -40
  54. /lifx_emulator/{observers.py → devices/observers.py} +0 -0
  55. /lifx_emulator/{state_serializer.py → devices/state_serializer.py} +0 -0
  56. {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.1.0.dist-info}/WHEEL +0 -0
  57. {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.1.0.dist-info}/entry_points.txt +0 -0
  58. {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
@@ -6,7 +6,7 @@ from abc import ABC, abstractmethod
6
6
  from typing import TYPE_CHECKING, Any
7
7
 
8
8
  if TYPE_CHECKING:
9
- from lifx_emulator.device import DeviceState
9
+ from lifx_emulator.devices import DeviceState
10
10
 
11
11
 
12
12
  class PacketHandler(ABC):
@@ -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.device import DeviceState
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
- label_bytes = device_state.label.encode("utf-8")[:32].ljust(32, b"\x00")
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.rstrip(b"\x00").decode(
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
- label_bytes = device_state.label.encode("utf-8")[:32].ljust(32, b"\x00")
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=label_bytes,
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.rstrip(b"\x00").decode(
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=label_bytes,
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=label_bytes,
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.rstrip(b"\x00").decode(
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=label_bytes,
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.device import DeviceState
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=label_bytes,
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=label_bytes,
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=label_bytes,
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=label_bytes,
187
+ label=device_state.label,
192
188
  )
193
189
  ]
194
190
  return []
@@ -15,7 +15,7 @@ from lifx_emulator.protocol.protocol_types import (
15
15
  )
16
16
 
17
17
  if TYPE_CHECKING:
18
- from lifx_emulator.device import DeviceState
18
+ from lifx_emulator.devices import DeviceState
19
19
 
20
20
  logger = logging.getLogger(__name__)
21
21
 
@@ -19,7 +19,7 @@ from lifx_emulator.protocol.protocol_types import (
19
19
  )
20
20
 
21
21
  if TYPE_CHECKING:
22
- from lifx_emulator.device import DeviceState
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
- parameter0=0,
243
- parameter1=0,
244
- parameter2=0,
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: