lifx-emulator 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lifx_emulator/__init__.py +31 -0
- lifx_emulator/__main__.py +607 -0
- lifx_emulator/api.py +1825 -0
- lifx_emulator/async_storage.py +308 -0
- lifx_emulator/constants.py +33 -0
- lifx_emulator/device.py +750 -0
- lifx_emulator/device_states.py +114 -0
- lifx_emulator/factories.py +380 -0
- lifx_emulator/handlers/__init__.py +39 -0
- lifx_emulator/handlers/base.py +49 -0
- lifx_emulator/handlers/device_handlers.py +340 -0
- lifx_emulator/handlers/light_handlers.py +372 -0
- lifx_emulator/handlers/multizone_handlers.py +249 -0
- lifx_emulator/handlers/registry.py +110 -0
- lifx_emulator/handlers/tile_handlers.py +309 -0
- lifx_emulator/observers.py +139 -0
- lifx_emulator/products/__init__.py +28 -0
- lifx_emulator/products/generator.py +771 -0
- lifx_emulator/products/registry.py +1446 -0
- lifx_emulator/products/specs.py +242 -0
- lifx_emulator/products/specs.yml +327 -0
- lifx_emulator/protocol/__init__.py +1 -0
- lifx_emulator/protocol/base.py +334 -0
- lifx_emulator/protocol/const.py +8 -0
- lifx_emulator/protocol/generator.py +1371 -0
- lifx_emulator/protocol/header.py +159 -0
- lifx_emulator/protocol/packets.py +1351 -0
- lifx_emulator/protocol/protocol_types.py +844 -0
- lifx_emulator/protocol/serializer.py +379 -0
- lifx_emulator/scenario_manager.py +402 -0
- lifx_emulator/scenario_persistence.py +206 -0
- lifx_emulator/server.py +482 -0
- lifx_emulator/state_restorer.py +259 -0
- lifx_emulator/state_serializer.py +130 -0
- lifx_emulator/storage_protocol.py +100 -0
- lifx_emulator-1.0.0.dist-info/METADATA +445 -0
- lifx_emulator-1.0.0.dist-info/RECORD +40 -0
- lifx_emulator-1.0.0.dist-info/WHEEL +4 -0
- lifx_emulator-1.0.0.dist-info/entry_points.txt +2 -0
- lifx_emulator-1.0.0.dist-info/licenses/LICENSE +35 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""Device packet handlers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
from lifx_emulator.handlers.base import PacketHandler
|
|
10
|
+
from lifx_emulator.protocol.packets import Device
|
|
11
|
+
from lifx_emulator.protocol.protocol_types import DeviceService as ProtocolDeviceService
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from lifx_emulator.device import DeviceState
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GetServiceHandler(PacketHandler):
|
|
20
|
+
"""Handle DeviceGetService (2) -> DeviceStateService (3)."""
|
|
21
|
+
|
|
22
|
+
PKT_TYPE = Device.GetService.PKT_TYPE
|
|
23
|
+
|
|
24
|
+
def handle(
|
|
25
|
+
self, device_state: DeviceState, packet: Any | None, res_required: bool
|
|
26
|
+
) -> list[Any]:
|
|
27
|
+
logger.debug(
|
|
28
|
+
"Sending StateService: %s [%s]",
|
|
29
|
+
ProtocolDeviceService.UDP,
|
|
30
|
+
device_state.port,
|
|
31
|
+
)
|
|
32
|
+
return [
|
|
33
|
+
Device.StateService(
|
|
34
|
+
service=ProtocolDeviceService.UDP, port=device_state.port
|
|
35
|
+
)
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class GetPowerHandler(PacketHandler):
|
|
40
|
+
"""Handle DeviceGetPower (20) -> DeviceStatePower (22)."""
|
|
41
|
+
|
|
42
|
+
PKT_TYPE = Device.GetPower.PKT_TYPE
|
|
43
|
+
|
|
44
|
+
def handle(
|
|
45
|
+
self, device_state: DeviceState, packet: Any | None, res_required: bool
|
|
46
|
+
) -> list[Any]:
|
|
47
|
+
return [Device.StatePower(level=device_state.power_level)]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SetPowerHandler(PacketHandler):
|
|
51
|
+
"""Handle DeviceSetPower (21) -> DeviceStatePower (22)."""
|
|
52
|
+
|
|
53
|
+
PKT_TYPE = Device.SetPower.PKT_TYPE
|
|
54
|
+
|
|
55
|
+
def handle(
|
|
56
|
+
self,
|
|
57
|
+
device_state: DeviceState,
|
|
58
|
+
packet: Device.SetPower | None,
|
|
59
|
+
res_required: bool,
|
|
60
|
+
) -> list[Any]:
|
|
61
|
+
if packet:
|
|
62
|
+
device_state.power_level = packet.level
|
|
63
|
+
logger.info("Power set to %s", device_state.power_level)
|
|
64
|
+
|
|
65
|
+
if res_required:
|
|
66
|
+
return [Device.StatePower(level=device_state.power_level)]
|
|
67
|
+
return []
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class GetLabelHandler(PacketHandler):
|
|
71
|
+
"""Handle DeviceGetLabel (23) -> DeviceStateLabel (25)."""
|
|
72
|
+
|
|
73
|
+
PKT_TYPE = Device.GetLabel.PKT_TYPE
|
|
74
|
+
|
|
75
|
+
def handle(
|
|
76
|
+
self, device_state: DeviceState, packet: Any | None, res_required: bool
|
|
77
|
+
) -> list[Any]:
|
|
78
|
+
label_bytes = device_state.label.encode("utf-8")[:32].ljust(32, b"\x00")
|
|
79
|
+
return [Device.StateLabel(label=label_bytes)]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class SetLabelHandler(PacketHandler):
|
|
83
|
+
"""Handle DeviceSetLabel (24) -> DeviceStateLabel (25)."""
|
|
84
|
+
|
|
85
|
+
PKT_TYPE = Device.SetLabel.PKT_TYPE
|
|
86
|
+
|
|
87
|
+
def handle(
|
|
88
|
+
self,
|
|
89
|
+
device_state: DeviceState,
|
|
90
|
+
packet: Device.SetLabel | None,
|
|
91
|
+
res_required: bool,
|
|
92
|
+
) -> list[Any]:
|
|
93
|
+
if packet:
|
|
94
|
+
device_state.label = packet.label.rstrip(b"\x00").decode(
|
|
95
|
+
"utf-8", errors="replace"
|
|
96
|
+
)
|
|
97
|
+
logger.info("Label set to '%s'", device_state.label)
|
|
98
|
+
|
|
99
|
+
if res_required:
|
|
100
|
+
label_bytes = device_state.label.encode("utf-8")[:32].ljust(32, b"\x00")
|
|
101
|
+
return [Device.StateLabel(label=label_bytes)]
|
|
102
|
+
return []
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class GetVersionHandler(PacketHandler):
|
|
106
|
+
"""Handle DeviceGetVersion (32) -> DeviceStateVersion (33)."""
|
|
107
|
+
|
|
108
|
+
PKT_TYPE = Device.GetVersion.PKT_TYPE
|
|
109
|
+
|
|
110
|
+
def handle(
|
|
111
|
+
self, device_state: DeviceState, packet: Any | None, res_required: bool
|
|
112
|
+
) -> list[Any]:
|
|
113
|
+
return [
|
|
114
|
+
Device.StateVersion(
|
|
115
|
+
vendor=device_state.vendor, product=device_state.product
|
|
116
|
+
)
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class GetInfoHandler(PacketHandler):
|
|
121
|
+
"""Handle DeviceGetInfo (34) -> DeviceStateInfo (35)."""
|
|
122
|
+
|
|
123
|
+
PKT_TYPE = Device.GetInfo.PKT_TYPE
|
|
124
|
+
|
|
125
|
+
def handle(
|
|
126
|
+
self, device_state: DeviceState, packet: Any | None, res_required: bool
|
|
127
|
+
) -> list[Any]:
|
|
128
|
+
current_time = int(time.time() * 1e9) # nanoseconds
|
|
129
|
+
return [
|
|
130
|
+
Device.StateInfo(
|
|
131
|
+
time=current_time, uptime=device_state.uptime_ns, downtime=0
|
|
132
|
+
)
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class GetHostFirmwareHandler(PacketHandler):
|
|
137
|
+
"""Handle DeviceGetHostFirmware (14) -> DeviceStateHostFirmware (15)."""
|
|
138
|
+
|
|
139
|
+
PKT_TYPE = Device.GetHostFirmware.PKT_TYPE
|
|
140
|
+
|
|
141
|
+
def handle(
|
|
142
|
+
self, device_state: DeviceState, packet: Any | None, res_required: bool
|
|
143
|
+
) -> list[Any]:
|
|
144
|
+
return [
|
|
145
|
+
Device.StateHostFirmware(
|
|
146
|
+
build=device_state.build_timestamp,
|
|
147
|
+
version_minor=device_state.version_minor,
|
|
148
|
+
version_major=device_state.version_major,
|
|
149
|
+
)
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class GetWifiInfoHandler(PacketHandler):
|
|
154
|
+
"""Handle DeviceGetWifiInfo (16) -> DeviceStateWifiInfo (17)."""
|
|
155
|
+
|
|
156
|
+
PKT_TYPE = Device.GetWifiInfo.PKT_TYPE
|
|
157
|
+
|
|
158
|
+
def handle(
|
|
159
|
+
self, device_state: DeviceState, packet: Any | None, res_required: bool
|
|
160
|
+
) -> list[Any]:
|
|
161
|
+
return [Device.StateWifiInfo(signal=device_state.wifi_signal)]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class GetLocationHandler(PacketHandler):
|
|
165
|
+
"""Handle DeviceGetLocation (48) -> DeviceStateLocation (50)."""
|
|
166
|
+
|
|
167
|
+
PKT_TYPE = Device.GetLocation.PKT_TYPE
|
|
168
|
+
|
|
169
|
+
def handle(
|
|
170
|
+
self, device_state: DeviceState, packet: Any | None, res_required: bool
|
|
171
|
+
) -> list[Any]:
|
|
172
|
+
label_bytes = device_state.location_label.encode("utf-8")[:32].ljust(
|
|
173
|
+
32, b"\x00"
|
|
174
|
+
)
|
|
175
|
+
return [
|
|
176
|
+
Device.StateLocation(
|
|
177
|
+
location=device_state.location_id,
|
|
178
|
+
label=label_bytes,
|
|
179
|
+
updated_at=device_state.location_updated_at,
|
|
180
|
+
)
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class SetLocationHandler(PacketHandler):
|
|
185
|
+
"""Handle DeviceSetLocation (49) -> DeviceStateLocation (50)."""
|
|
186
|
+
|
|
187
|
+
PKT_TYPE = Device.SetLocation.PKT_TYPE
|
|
188
|
+
|
|
189
|
+
def handle(
|
|
190
|
+
self,
|
|
191
|
+
device_state: DeviceState,
|
|
192
|
+
packet: Device.SetLocation | None,
|
|
193
|
+
res_required: bool,
|
|
194
|
+
) -> list[Any]:
|
|
195
|
+
if packet:
|
|
196
|
+
device_state.location_id = packet.location
|
|
197
|
+
device_state.location_label = packet.label.rstrip(b"\x00").decode(
|
|
198
|
+
"utf-8", errors="replace"
|
|
199
|
+
)
|
|
200
|
+
device_state.location_updated_at = packet.updated_at
|
|
201
|
+
loc_id = packet.location.hex()[:8]
|
|
202
|
+
logger.info(
|
|
203
|
+
"Location set to '%s' (id=%s...)", device_state.location_label, loc_id
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if res_required:
|
|
207
|
+
label_bytes = device_state.location_label.encode("utf-8")[:32].ljust(
|
|
208
|
+
32, b"\x00"
|
|
209
|
+
)
|
|
210
|
+
return [
|
|
211
|
+
Device.StateLocation(
|
|
212
|
+
location=device_state.location_id,
|
|
213
|
+
label=label_bytes,
|
|
214
|
+
updated_at=device_state.location_updated_at,
|
|
215
|
+
)
|
|
216
|
+
]
|
|
217
|
+
return []
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class GetGroupHandler(PacketHandler):
|
|
221
|
+
"""Handle DeviceGetGroup (51) -> DeviceStateGroup (53)."""
|
|
222
|
+
|
|
223
|
+
PKT_TYPE = Device.GetGroup.PKT_TYPE
|
|
224
|
+
|
|
225
|
+
def handle(
|
|
226
|
+
self, device_state: DeviceState, packet: Any | None, res_required: bool
|
|
227
|
+
) -> list[Any]:
|
|
228
|
+
label_bytes = device_state.group_label.encode("utf-8")[:32].ljust(32, b"\x00")
|
|
229
|
+
return [
|
|
230
|
+
Device.StateGroup(
|
|
231
|
+
group=device_state.group_id,
|
|
232
|
+
label=label_bytes,
|
|
233
|
+
updated_at=device_state.group_updated_at,
|
|
234
|
+
)
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class SetGroupHandler(PacketHandler):
|
|
239
|
+
"""Handle DeviceSetGroup (52) -> DeviceStateGroup (53)."""
|
|
240
|
+
|
|
241
|
+
PKT_TYPE = Device.SetGroup.PKT_TYPE
|
|
242
|
+
|
|
243
|
+
def handle(
|
|
244
|
+
self,
|
|
245
|
+
device_state: DeviceState,
|
|
246
|
+
packet: Device.SetGroup | None,
|
|
247
|
+
res_required: bool,
|
|
248
|
+
) -> list[Any]:
|
|
249
|
+
if packet:
|
|
250
|
+
device_state.group_id = packet.group
|
|
251
|
+
device_state.group_label = packet.label.rstrip(b"\x00").decode(
|
|
252
|
+
"utf-8", errors="replace"
|
|
253
|
+
)
|
|
254
|
+
device_state.group_updated_at = packet.updated_at
|
|
255
|
+
grp_id = packet.group.hex()[:8]
|
|
256
|
+
logger.info(
|
|
257
|
+
"Group set to '%s' (id=%s...)", device_state.group_label, grp_id
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
if res_required:
|
|
261
|
+
label_bytes = device_state.group_label.encode("utf-8")[:32].ljust(
|
|
262
|
+
32, b"\x00"
|
|
263
|
+
)
|
|
264
|
+
return [
|
|
265
|
+
Device.StateGroup(
|
|
266
|
+
group=device_state.group_id,
|
|
267
|
+
label=label_bytes,
|
|
268
|
+
updated_at=device_state.group_updated_at,
|
|
269
|
+
)
|
|
270
|
+
]
|
|
271
|
+
return []
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class EchoRequestHandler(PacketHandler):
|
|
275
|
+
"""Handle DeviceEchoRequest (58) -> DeviceEchoResponse (59)."""
|
|
276
|
+
|
|
277
|
+
PKT_TYPE = Device.EchoRequest.PKT_TYPE
|
|
278
|
+
|
|
279
|
+
def handle(
|
|
280
|
+
self,
|
|
281
|
+
device_state: DeviceState,
|
|
282
|
+
packet: Device.EchoRequest | None,
|
|
283
|
+
res_required: bool,
|
|
284
|
+
) -> list[Any]:
|
|
285
|
+
payload = packet.payload if packet else b"\x00" * 64
|
|
286
|
+
return [Device.EchoResponse(payload=payload[:64].ljust(64, b"\x00"))]
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class GetWifiFirmwareHandler(PacketHandler):
|
|
290
|
+
"""Handle DeviceGetWifiFirmware (18) -> DeviceStateWifiFirmware (19)."""
|
|
291
|
+
|
|
292
|
+
PKT_TYPE = Device.GetWifiFirmware.PKT_TYPE
|
|
293
|
+
|
|
294
|
+
def handle(
|
|
295
|
+
self, device_state: DeviceState, packet: Any | None, res_required: bool
|
|
296
|
+
) -> list[Any]:
|
|
297
|
+
build = int(time.time()) - 1000000000 # Example build timestamp
|
|
298
|
+
return [
|
|
299
|
+
Device.StateWifiFirmware(
|
|
300
|
+
build=build,
|
|
301
|
+
version_minor=device_state.version_minor,
|
|
302
|
+
version_major=device_state.version_major,
|
|
303
|
+
)
|
|
304
|
+
]
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class SetRebootHandler(PacketHandler):
|
|
308
|
+
"""Handle DeviceSetReboot (38) - just acknowledge, don't actually reboot."""
|
|
309
|
+
|
|
310
|
+
PKT_TYPE = Device.SetReboot.PKT_TYPE
|
|
311
|
+
|
|
312
|
+
def handle(
|
|
313
|
+
self, device_state: DeviceState, packet: Any | None, res_required: bool
|
|
314
|
+
) -> list[Any]:
|
|
315
|
+
serial = device_state.serial
|
|
316
|
+
logger.info("Device %s: Received reboot request (ignored in emulator)", serial)
|
|
317
|
+
# In a real device, this would trigger a reboot
|
|
318
|
+
# In emulator, we just acknowledge it
|
|
319
|
+
return []
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# List of all device handlers for easy registration
|
|
323
|
+
ALL_DEVICE_HANDLERS = [
|
|
324
|
+
GetServiceHandler(),
|
|
325
|
+
GetPowerHandler(),
|
|
326
|
+
SetPowerHandler(),
|
|
327
|
+
GetLabelHandler(),
|
|
328
|
+
SetLabelHandler(),
|
|
329
|
+
GetVersionHandler(),
|
|
330
|
+
GetInfoHandler(),
|
|
331
|
+
GetHostFirmwareHandler(),
|
|
332
|
+
GetWifiInfoHandler(),
|
|
333
|
+
GetLocationHandler(),
|
|
334
|
+
SetLocationHandler(),
|
|
335
|
+
GetGroupHandler(),
|
|
336
|
+
SetGroupHandler(),
|
|
337
|
+
EchoRequestHandler(),
|
|
338
|
+
GetWifiFirmwareHandler(),
|
|
339
|
+
SetRebootHandler(),
|
|
340
|
+
]
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""Light packet handlers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from lifx_emulator.handlers.base import PacketHandler
|
|
9
|
+
from lifx_emulator.protocol.packets import Light
|
|
10
|
+
from lifx_emulator.protocol.protocol_types import LightLastHevCycleResult
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from lifx_emulator.device import DeviceState
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GetColorHandler(PacketHandler):
|
|
19
|
+
"""Handle LightGet (101) -> LightState (107)."""
|
|
20
|
+
|
|
21
|
+
PKT_TYPE = Light.GetColor.PKT_TYPE
|
|
22
|
+
|
|
23
|
+
def handle(
|
|
24
|
+
self, device_state: DeviceState, packet: Any | None, res_required: bool
|
|
25
|
+
) -> list[Any]:
|
|
26
|
+
label_bytes = device_state.label.encode("utf-8")[:32].ljust(32, b"\x00")
|
|
27
|
+
return [
|
|
28
|
+
Light.StateColor(
|
|
29
|
+
color=device_state.color,
|
|
30
|
+
power=device_state.power_level,
|
|
31
|
+
label=label_bytes,
|
|
32
|
+
)
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SetColorHandler(PacketHandler):
|
|
37
|
+
"""Handle LightSetColor (102) -> LightState (107)."""
|
|
38
|
+
|
|
39
|
+
PKT_TYPE = Light.SetColor.PKT_TYPE
|
|
40
|
+
|
|
41
|
+
def handle(
|
|
42
|
+
self,
|
|
43
|
+
device_state: DeviceState,
|
|
44
|
+
packet: Light.SetColor | None,
|
|
45
|
+
res_required: bool,
|
|
46
|
+
) -> list[Any]:
|
|
47
|
+
if packet:
|
|
48
|
+
device_state.color = packet.color
|
|
49
|
+
c = packet.color
|
|
50
|
+
logger.info(
|
|
51
|
+
f"Color set to HSBK({c.hue}, {c.saturation}, "
|
|
52
|
+
f"{c.brightness}, {c.kelvin}), duration={packet.duration}ms"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if res_required:
|
|
56
|
+
label_bytes = device_state.label.encode("utf-8")[:32].ljust(32, b"\x00")
|
|
57
|
+
return [
|
|
58
|
+
Light.StateColor(
|
|
59
|
+
color=device_state.color,
|
|
60
|
+
power=device_state.power_level,
|
|
61
|
+
label=label_bytes,
|
|
62
|
+
)
|
|
63
|
+
]
|
|
64
|
+
return []
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class GetPowerHandler(PacketHandler):
|
|
68
|
+
"""Handle LightGetPower (116) -> LightStatePower (118)."""
|
|
69
|
+
|
|
70
|
+
PKT_TYPE = Light.GetPower.PKT_TYPE
|
|
71
|
+
|
|
72
|
+
def handle(
|
|
73
|
+
self, device_state: DeviceState, packet: Any | None, res_required: bool
|
|
74
|
+
) -> list[Any]:
|
|
75
|
+
return [Light.StatePower(level=device_state.power_level)]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class SetPowerHandler(PacketHandler):
|
|
79
|
+
"""Handle LightSetPower (117) -> LightStatePower (118)."""
|
|
80
|
+
|
|
81
|
+
PKT_TYPE = Light.SetPower.PKT_TYPE
|
|
82
|
+
|
|
83
|
+
def handle(
|
|
84
|
+
self,
|
|
85
|
+
device_state: DeviceState,
|
|
86
|
+
packet: Light.SetPower | None,
|
|
87
|
+
res_required: bool,
|
|
88
|
+
) -> list[Any]:
|
|
89
|
+
if packet:
|
|
90
|
+
device_state.power_level = packet.level
|
|
91
|
+
logger.info(
|
|
92
|
+
f"Light power set to {packet.level}, duration={packet.duration}ms"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if res_required:
|
|
96
|
+
return [Light.StatePower(level=device_state.power_level)]
|
|
97
|
+
return []
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class SetWaveformHandler(PacketHandler):
|
|
101
|
+
"""Handle LightSetWaveform (103) -> LightState (107)."""
|
|
102
|
+
|
|
103
|
+
PKT_TYPE = Light.SetWaveform.PKT_TYPE
|
|
104
|
+
|
|
105
|
+
def handle(
|
|
106
|
+
self,
|
|
107
|
+
device_state: DeviceState,
|
|
108
|
+
packet: Light.SetWaveform | None,
|
|
109
|
+
res_required: bool,
|
|
110
|
+
) -> list[Any]:
|
|
111
|
+
if packet:
|
|
112
|
+
# Store waveform state
|
|
113
|
+
device_state.waveform_active = True
|
|
114
|
+
device_state.waveform_transient = packet.transient
|
|
115
|
+
device_state.waveform_color = packet.color
|
|
116
|
+
device_state.waveform_period_ms = packet.period
|
|
117
|
+
device_state.waveform_cycles = packet.cycles
|
|
118
|
+
device_state.waveform_skew_ratio = packet.skew_ratio
|
|
119
|
+
device_state.waveform_type = int(packet.waveform)
|
|
120
|
+
|
|
121
|
+
# If not transient, update the color state
|
|
122
|
+
if not packet.transient:
|
|
123
|
+
device_state.color = packet.color
|
|
124
|
+
|
|
125
|
+
logger.info(
|
|
126
|
+
f"Waveform set: type={packet.waveform}, "
|
|
127
|
+
f"transient={packet.transient}, period={packet.period}ms, "
|
|
128
|
+
f"cycles={packet.cycles}, skew={packet.skew_ratio}"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if res_required:
|
|
132
|
+
label_bytes = device_state.label.encode("utf-8")[:32].ljust(32, b"\x00")
|
|
133
|
+
return [
|
|
134
|
+
Light.StateColor(
|
|
135
|
+
color=device_state.color,
|
|
136
|
+
power=device_state.power_level,
|
|
137
|
+
label=label_bytes,
|
|
138
|
+
)
|
|
139
|
+
]
|
|
140
|
+
return []
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class SetWaveformOptionalHandler(PacketHandler):
|
|
144
|
+
"""Handle LightSetWaveformOptional (119) -> LightState (107)."""
|
|
145
|
+
|
|
146
|
+
PKT_TYPE = Light.SetWaveformOptional.PKT_TYPE
|
|
147
|
+
|
|
148
|
+
def handle(
|
|
149
|
+
self,
|
|
150
|
+
device_state: DeviceState,
|
|
151
|
+
packet: Light.SetWaveformOptional | None,
|
|
152
|
+
res_required: bool,
|
|
153
|
+
) -> list[Any]:
|
|
154
|
+
if packet:
|
|
155
|
+
# Store waveform state
|
|
156
|
+
device_state.waveform_active = True
|
|
157
|
+
device_state.waveform_transient = packet.transient
|
|
158
|
+
device_state.waveform_period_ms = packet.period
|
|
159
|
+
device_state.waveform_cycles = packet.cycles
|
|
160
|
+
device_state.waveform_skew_ratio = packet.skew_ratio
|
|
161
|
+
device_state.waveform_type = int(packet.waveform)
|
|
162
|
+
|
|
163
|
+
# Apply color components selectively based on flags
|
|
164
|
+
if not packet.transient:
|
|
165
|
+
if packet.set_hue:
|
|
166
|
+
device_state.color.hue = packet.color.hue
|
|
167
|
+
if packet.set_saturation:
|
|
168
|
+
device_state.color.saturation = packet.color.saturation
|
|
169
|
+
if packet.set_brightness:
|
|
170
|
+
device_state.color.brightness = packet.color.brightness
|
|
171
|
+
if packet.set_kelvin:
|
|
172
|
+
device_state.color.kelvin = packet.color.kelvin
|
|
173
|
+
|
|
174
|
+
# Store the waveform color (all components)
|
|
175
|
+
device_state.waveform_color = packet.color
|
|
176
|
+
|
|
177
|
+
logger.info(
|
|
178
|
+
f"Waveform optional set: type={packet.waveform}, "
|
|
179
|
+
f"transient={packet.transient}, period={packet.period}ms, "
|
|
180
|
+
f"cycles={packet.cycles}, components=[H:{packet.set_hue},"
|
|
181
|
+
f"S:{packet.set_saturation},B:{packet.set_brightness},"
|
|
182
|
+
f"K:{packet.set_kelvin}]"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if res_required:
|
|
186
|
+
label_bytes = device_state.label.encode("utf-8")[:32].ljust(32, b"\x00")
|
|
187
|
+
return [
|
|
188
|
+
Light.StateColor(
|
|
189
|
+
color=device_state.color,
|
|
190
|
+
power=device_state.power_level,
|
|
191
|
+
label=label_bytes,
|
|
192
|
+
)
|
|
193
|
+
]
|
|
194
|
+
return []
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class GetInfraredHandler(PacketHandler):
|
|
198
|
+
"""Handle LightGetInfrared (120) -> LightStateInfrared (121)."""
|
|
199
|
+
|
|
200
|
+
PKT_TYPE = Light.GetInfrared.PKT_TYPE
|
|
201
|
+
|
|
202
|
+
def handle(
|
|
203
|
+
self, device_state: DeviceState, packet: Any | None, res_required: bool
|
|
204
|
+
) -> list[Any]:
|
|
205
|
+
if not device_state.has_infrared:
|
|
206
|
+
return []
|
|
207
|
+
return [Light.StateInfrared(brightness=device_state.infrared_brightness)]
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class SetInfraredHandler(PacketHandler):
|
|
211
|
+
"""Handle LightSetInfrared (122) -> LightStateInfrared (121)."""
|
|
212
|
+
|
|
213
|
+
PKT_TYPE = Light.SetInfrared.PKT_TYPE
|
|
214
|
+
|
|
215
|
+
def handle(
|
|
216
|
+
self,
|
|
217
|
+
device_state: DeviceState,
|
|
218
|
+
packet: Light.SetInfrared | None,
|
|
219
|
+
res_required: bool,
|
|
220
|
+
) -> list[Any]:
|
|
221
|
+
if not device_state.has_infrared:
|
|
222
|
+
return []
|
|
223
|
+
if packet:
|
|
224
|
+
device_state.infrared_brightness = packet.brightness
|
|
225
|
+
logger.info("Infrared brightness set to %s", packet.brightness)
|
|
226
|
+
|
|
227
|
+
if res_required:
|
|
228
|
+
return [Light.StateInfrared(brightness=device_state.infrared_brightness)]
|
|
229
|
+
return []
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class GetHevCycleHandler(PacketHandler):
|
|
233
|
+
"""Handle LightGetHevCycle (142) -> LightStateHevCycle (144)."""
|
|
234
|
+
|
|
235
|
+
PKT_TYPE = Light.GetHevCycle.PKT_TYPE
|
|
236
|
+
|
|
237
|
+
def handle(
|
|
238
|
+
self, device_state: DeviceState, packet: Any | None, res_required: bool
|
|
239
|
+
) -> list[Any]:
|
|
240
|
+
if not device_state.has_hev:
|
|
241
|
+
return []
|
|
242
|
+
return [
|
|
243
|
+
Light.StateHevCycle(
|
|
244
|
+
duration_s=device_state.hev_cycle_duration_s,
|
|
245
|
+
remaining_s=device_state.hev_cycle_remaining_s,
|
|
246
|
+
last_power=device_state.hev_cycle_last_power,
|
|
247
|
+
)
|
|
248
|
+
]
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class SetHevCycleHandler(PacketHandler):
|
|
252
|
+
"""Handle LightSetHevCycle (143) -> LightStateHevCycle (144)."""
|
|
253
|
+
|
|
254
|
+
PKT_TYPE = Light.SetHevCycle.PKT_TYPE
|
|
255
|
+
|
|
256
|
+
def handle(
|
|
257
|
+
self,
|
|
258
|
+
device_state: DeviceState,
|
|
259
|
+
packet: Light.SetHevCycle | None,
|
|
260
|
+
res_required: bool,
|
|
261
|
+
) -> list[Any]:
|
|
262
|
+
if not device_state.has_hev:
|
|
263
|
+
return []
|
|
264
|
+
if packet:
|
|
265
|
+
device_state.hev_cycle_duration_s = packet.duration_s
|
|
266
|
+
if packet.enable:
|
|
267
|
+
device_state.hev_cycle_remaining_s = packet.duration_s
|
|
268
|
+
else:
|
|
269
|
+
device_state.hev_cycle_remaining_s = 0
|
|
270
|
+
logger.info(
|
|
271
|
+
f"HEV cycle set: enable={packet.enable}, duration={packet.duration_s}s"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
if res_required:
|
|
275
|
+
return [
|
|
276
|
+
Light.StateHevCycle(
|
|
277
|
+
duration_s=device_state.hev_cycle_duration_s,
|
|
278
|
+
remaining_s=device_state.hev_cycle_remaining_s,
|
|
279
|
+
last_power=device_state.hev_cycle_last_power,
|
|
280
|
+
)
|
|
281
|
+
]
|
|
282
|
+
return []
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class GetHevCycleConfigurationHandler(PacketHandler):
|
|
286
|
+
"""Handle LightGetHevCycleConfiguration (145).
|
|
287
|
+
|
|
288
|
+
Returns LightStateHevCycleConfiguration (147).
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
PKT_TYPE = Light.GetHevCycleConfiguration.PKT_TYPE
|
|
292
|
+
|
|
293
|
+
def handle(
|
|
294
|
+
self, device_state: DeviceState, packet: Any | None, res_required: bool
|
|
295
|
+
) -> list[Any]:
|
|
296
|
+
if not device_state.has_hev:
|
|
297
|
+
return []
|
|
298
|
+
return [
|
|
299
|
+
Light.StateHevCycleConfiguration(
|
|
300
|
+
indication=device_state.hev_indication,
|
|
301
|
+
duration_s=device_state.hev_cycle_duration_s,
|
|
302
|
+
)
|
|
303
|
+
]
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class SetHevCycleConfigurationHandler(PacketHandler):
|
|
307
|
+
"""Handle LightSetHevCycleConfiguration (146).
|
|
308
|
+
|
|
309
|
+
Returns LightStateHevCycleConfiguration (147).
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
PKT_TYPE = Light.SetHevCycleConfiguration.PKT_TYPE
|
|
313
|
+
|
|
314
|
+
def handle(
|
|
315
|
+
self,
|
|
316
|
+
device_state: DeviceState,
|
|
317
|
+
packet: Light.SetHevCycleConfiguration | None,
|
|
318
|
+
res_required: bool,
|
|
319
|
+
) -> list[Any]:
|
|
320
|
+
if not device_state.has_hev:
|
|
321
|
+
return []
|
|
322
|
+
if packet:
|
|
323
|
+
device_state.hev_indication = packet.indication
|
|
324
|
+
device_state.hev_cycle_duration_s = packet.duration_s
|
|
325
|
+
logger.info(
|
|
326
|
+
f"HEV config set: indication={packet.indication}, "
|
|
327
|
+
f"duration={packet.duration_s}s"
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
if res_required:
|
|
331
|
+
return [
|
|
332
|
+
Light.StateHevCycleConfiguration(
|
|
333
|
+
indication=device_state.hev_indication,
|
|
334
|
+
duration_s=device_state.hev_cycle_duration_s,
|
|
335
|
+
)
|
|
336
|
+
]
|
|
337
|
+
return []
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class GetLastHevCycleResultHandler(PacketHandler):
|
|
341
|
+
"""Handle LightGetLastHevCycleResult (148) -> LightStateLastHevCycleResult (149)."""
|
|
342
|
+
|
|
343
|
+
PKT_TYPE = Light.GetLastHevCycleResult.PKT_TYPE
|
|
344
|
+
|
|
345
|
+
def handle(
|
|
346
|
+
self, device_state: DeviceState, packet: Any | None, res_required: bool
|
|
347
|
+
) -> list[Any]:
|
|
348
|
+
if not device_state.has_hev:
|
|
349
|
+
return []
|
|
350
|
+
return [
|
|
351
|
+
Light.StateLastHevCycleResult(
|
|
352
|
+
result=LightLastHevCycleResult(device_state.hev_last_result)
|
|
353
|
+
)
|
|
354
|
+
]
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# List of all light handlers for easy registration
|
|
358
|
+
ALL_LIGHT_HANDLERS = [
|
|
359
|
+
GetColorHandler(),
|
|
360
|
+
SetColorHandler(),
|
|
361
|
+
GetPowerHandler(),
|
|
362
|
+
SetPowerHandler(),
|
|
363
|
+
SetWaveformHandler(),
|
|
364
|
+
SetWaveformOptionalHandler(),
|
|
365
|
+
GetInfraredHandler(),
|
|
366
|
+
SetInfraredHandler(),
|
|
367
|
+
GetHevCycleHandler(),
|
|
368
|
+
SetHevCycleHandler(),
|
|
369
|
+
GetHevCycleConfigurationHandler(),
|
|
370
|
+
SetHevCycleConfigurationHandler(),
|
|
371
|
+
GetLastHevCycleResultHandler(),
|
|
372
|
+
]
|