lifx-async 4.3.5__py3-none-any.whl → 4.3.7__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/devices/base.py CHANGED
@@ -19,7 +19,7 @@ from lifx.const import (
19
19
  LIFX_LOCATION_NAMESPACE,
20
20
  LIFX_UDP_PORT,
21
21
  )
22
- from lifx.exceptions import LifxDeviceNotFoundError
22
+ from lifx.exceptions import LifxDeviceNotFoundError, LifxUnsupportedCommandError
23
23
  from lifx.network.connection import DeviceConnection
24
24
  from lifx.products.registry import ProductInfo, get_product
25
25
  from lifx.protocol import packets
@@ -152,6 +152,23 @@ class Device:
152
152
  ```
153
153
  """
154
154
 
155
+ @staticmethod
156
+ def _raise_if_unhandled(response: object) -> None:
157
+ """Raise LifxUnsupportedCommandError if device doesn't support the command.
158
+
159
+ Args:
160
+ response: The response from connection.request()
161
+
162
+ Raises:
163
+ LifxUnsupportedCommandError: If response is StateUnhandled or False
164
+ """
165
+ if isinstance(response, packets.Device.StateUnhandled):
166
+ raise LifxUnsupportedCommandError(
167
+ f"Device does not support packet type {response.unhandled_type}"
168
+ )
169
+ if response is False:
170
+ raise LifxUnsupportedCommandError("Device does not support this command")
171
+
155
172
  def __init__(
156
173
  self,
157
174
  serial: str,
@@ -456,6 +473,7 @@ class Device:
456
473
  LifxDeviceNotFoundError: If device is not connected
457
474
  LifxTimeoutError: If device does not respond
458
475
  LifxProtocolError: If response is invalid
476
+ LifxUnsupportedCommandError: If device doesn't support this command
459
477
 
460
478
  Example:
461
479
  ```python
@@ -469,6 +487,7 @@ class Device:
469
487
  """
470
488
  # Request automatically unpacks and decodes label
471
489
  state = await self.connection.request(packets.Device.GetLabel())
490
+ self._raise_if_unhandled(state)
472
491
 
473
492
  # Store label
474
493
  self._label = state.label
@@ -492,6 +511,7 @@ class Device:
492
511
  ValueError: If label is too long
493
512
  LifxDeviceNotFoundError: If device is not connected
494
513
  LifxTimeoutError: If device does not respond
514
+ LifxUnsupportedCommandError: If device doesn't support this command
495
515
 
496
516
  Example:
497
517
  ```python
@@ -508,9 +528,10 @@ class Device:
508
528
  label_bytes = label_bytes.ljust(32, b"\x00")
509
529
 
510
530
  # Request automatically handles acknowledgement
511
- await self.connection.request(
531
+ result = await self.connection.request(
512
532
  packets.Device.SetLabel(label=label_bytes),
513
533
  )
534
+ self._raise_if_unhandled(result)
514
535
 
515
536
  # Update cached state
516
537
  self._label = label
@@ -535,6 +556,7 @@ class Device:
535
556
  LifxDeviceNotFoundError: If device is not connected
536
557
  LifxTimeoutError: If device does not respond
537
558
  LifxProtocolError: If response is invalid
559
+ LifxUnsupportedCommandError: If device doesn't support this command
538
560
 
539
561
  Example:
540
562
  ```python
@@ -544,6 +566,7 @@ class Device:
544
566
  """
545
567
  # Request automatically unpacks response
546
568
  state = await self.connection.request(packets.Device.GetPower())
569
+ self._raise_if_unhandled(state)
547
570
 
548
571
  # Power level is uint16 (0 or 65535)
549
572
  _LOGGER.debug(
@@ -566,6 +589,7 @@ class Device:
566
589
  ValueError: If integer value is not 0 or 65535
567
590
  LifxDeviceNotFoundError: If device is not connected
568
591
  LifxTimeoutError: If device does not respond
592
+ LifxUnsupportedCommandError: If device doesn't support this command
569
593
 
570
594
  Example:
571
595
  ```python
@@ -591,9 +615,10 @@ class Device:
591
615
  raise TypeError(f"Expected bool or int, got {type(level).__name__}")
592
616
 
593
617
  # Request automatically handles acknowledgement
594
- await self.connection.request(
618
+ result = await self.connection.request(
595
619
  packets.Device.SetPower(level=power_level),
596
620
  )
621
+ self._raise_if_unhandled(result)
597
622
 
598
623
  _LOGGER.debug(
599
624
  {
@@ -616,6 +641,7 @@ class Device:
616
641
  LifxDeviceNotFoundError: If device is not connected
617
642
  LifxTimeoutError: If device does not respond
618
643
  LifxProtocolError: If response is invalid
644
+ LifxUnsupportedCommandError: If device doesn't support this command
619
645
 
620
646
  Example:
621
647
  ```python
@@ -625,6 +651,7 @@ class Device:
625
651
  """
626
652
  # Request automatically unpacks response
627
653
  state = await self.connection.request(packets.Device.GetVersion())
654
+ self._raise_if_unhandled(state)
628
655
 
629
656
  version = DeviceVersion(
630
657
  vendor=state.vendor,
@@ -655,6 +682,7 @@ class Device:
655
682
  LifxDeviceNotFoundError: If device is not connected
656
683
  LifxTimeoutError: If device does not respond
657
684
  LifxProtocolError: If response is invalid
685
+ LifxUnsupportedCommandError: If device doesn't support this command
658
686
 
659
687
  Example:
660
688
  ```python
@@ -665,6 +693,7 @@ class Device:
665
693
  """
666
694
  # Request automatically unpacks response
667
695
  state = await self.connection.request(packets.Device.GetInfo()) # type: ignore
696
+ self._raise_if_unhandled(state)
668
697
 
669
698
  info = DeviceInfo(time=state.time, uptime=state.uptime, downtime=state.downtime)
670
699
 
@@ -694,6 +723,7 @@ class Device:
694
723
  LifxDeviceNotFoundError: If device is not connected
695
724
  LifxTimeoutError: If device does not respond
696
725
  LifxProtocolError: If response is invalid
726
+ LifxUnsupportedCommandError: If device doesn't support this command
697
727
 
698
728
  Example:
699
729
  ```python
@@ -704,6 +734,7 @@ class Device:
704
734
  """
705
735
  # Request WiFi info from device
706
736
  state = await self.connection.request(packets.Device.GetWifiInfo())
737
+ self._raise_if_unhandled(state)
707
738
 
708
739
  # Extract WiFi info from response
709
740
  wifi_info = WifiInfo(signal=state.signal)
@@ -730,6 +761,7 @@ class Device:
730
761
  LifxDeviceNotFoundError: If device is not connected
731
762
  LifxTimeoutError: If device does not respond
732
763
  LifxProtocolError: If response is invalid
764
+ LifxUnsupportedCommandError: If device doesn't support this command
733
765
 
734
766
  Example:
735
767
  ```python
@@ -739,6 +771,7 @@ class Device:
739
771
  """
740
772
  # Request automatically unpacks response
741
773
  state = await self.connection.request(packets.Device.GetHostFirmware()) # type: ignore
774
+ self._raise_if_unhandled(state)
742
775
 
743
776
  firmware = FirmwareInfo(
744
777
  build=state.build,
@@ -778,6 +811,7 @@ class Device:
778
811
  LifxDeviceNotFoundError: If device is not connected
779
812
  LifxTimeoutError: If device does not respond
780
813
  LifxProtocolError: If response is invalid
814
+ LifxUnsupportedCommandError: If device doesn't support this command
781
815
 
782
816
  Example:
783
817
  ```python
@@ -787,6 +821,7 @@ class Device:
787
821
  """
788
822
  # Request automatically unpacks response
789
823
  state = await self.connection.request(packets.Device.GetWifiFirmware()) # type: ignore
824
+ self._raise_if_unhandled(state)
790
825
 
791
826
  firmware = FirmwareInfo(
792
827
  build=state.build,
@@ -822,6 +857,7 @@ class Device:
822
857
  LifxDeviceNotFoundError: If device is not connected
823
858
  LifxTimeoutError: If device does not respond
824
859
  LifxProtocolError: If response is invalid
860
+ LifxUnsupportedCommandError: If device doesn't support this command
825
861
 
826
862
  Example:
827
863
  ```python
@@ -832,6 +868,7 @@ class Device:
832
868
  """
833
869
  # Request automatically unpacks response
834
870
  state = await self.connection.request(packets.Device.GetLocation()) # type: ignore
871
+ self._raise_if_unhandled(state)
835
872
 
836
873
  location = LocationInfo(
837
874
  location=state.location,
@@ -873,6 +910,7 @@ class Device:
873
910
  LifxDeviceNotFoundError: If device is not connected
874
911
  LifxTimeoutError: If device does not respond
875
912
  ValueError: If label is invalid
913
+ LifxUnsupportedCommandError: If device doesn't support this command
876
914
 
877
915
  Example:
878
916
  ```python
@@ -967,11 +1005,12 @@ class Device:
967
1005
  updated_at = int(time.time() * 1e9)
968
1006
 
969
1007
  # Update this device
970
- await self.connection.request(
1008
+ result = await self.connection.request(
971
1009
  packets.Device.SetLocation(
972
1010
  location=location_uuid_to_use, label=label_bytes, updated_at=updated_at
973
1011
  ),
974
1012
  )
1013
+ self._raise_if_unhandled(result)
975
1014
 
976
1015
  # Update cached state
977
1016
  location_info = LocationInfo(
@@ -1003,6 +1042,7 @@ class Device:
1003
1042
  LifxDeviceNotFoundError: If device is not connected
1004
1043
  LifxTimeoutError: If device does not respond
1005
1044
  LifxProtocolError: If response is invalid
1045
+ LifxUnsupportedCommandError: If device doesn't support this command
1006
1046
 
1007
1047
  Example:
1008
1048
  ```python
@@ -1013,6 +1053,7 @@ class Device:
1013
1053
  """
1014
1054
  # Request automatically unpacks response
1015
1055
  state = await self.connection.request(packets.Device.GetGroup()) # type: ignore
1056
+ self._raise_if_unhandled(state)
1016
1057
 
1017
1058
  group = GroupInfo(
1018
1059
  group=state.group,
@@ -1054,6 +1095,7 @@ class Device:
1054
1095
  LifxDeviceNotFoundError: If device is not connected
1055
1096
  LifxTimeoutError: If device does not respond
1056
1097
  ValueError: If label is invalid
1098
+ LifxUnsupportedCommandError: If device doesn't support this command
1057
1099
 
1058
1100
  Example:
1059
1101
  ```python
@@ -1148,11 +1190,12 @@ class Device:
1148
1190
  updated_at = int(time.time() * 1e9)
1149
1191
 
1150
1192
  # Update this device
1151
- await self.connection.request(
1193
+ result = await self.connection.request(
1152
1194
  packets.Device.SetGroup(
1153
1195
  group=group_uuid_to_use, label=label_bytes, updated_at=updated_at
1154
1196
  ),
1155
1197
  )
1198
+ self._raise_if_unhandled(result)
1156
1199
 
1157
1200
  # Update cached state
1158
1201
  group_info = GroupInfo(
@@ -1181,6 +1224,7 @@ class Device:
1181
1224
  Raises:
1182
1225
  LifxDeviceNotFoundError: If device is not connected
1183
1226
  LifxTimeoutError: If device does not respond
1227
+ LifxUnsupportedCommandError: If device doesn't support this command
1184
1228
 
1185
1229
  Example:
1186
1230
  ```python
@@ -1194,9 +1238,10 @@ class Device:
1194
1238
  comes back online and is discoverable again.
1195
1239
  """
1196
1240
  # Send reboot request
1197
- await self.connection.request(
1241
+ result = await self.connection.request(
1198
1242
  packets.Device.SetReboot(),
1199
1243
  )
1244
+ self._raise_if_unhandled(result)
1200
1245
  _LOGGER.debug(
1201
1246
  {
1202
1247
  "class": "Device",
lifx/devices/hev.py CHANGED
@@ -70,6 +70,7 @@ class HevLight(Light):
70
70
  LifxDeviceNotFoundError: If device is not connected
71
71
  LifxTimeoutError: If device does not respond
72
72
  LifxProtocolError: If response is invalid
73
+ LifxUnsupportedCommandError: If device doesn't support this command
73
74
 
74
75
  Example:
75
76
  ```python
@@ -82,6 +83,7 @@ class HevLight(Light):
82
83
  """
83
84
  # Request HEV cycle state
84
85
  state = await self.connection.request(packets.Light.GetHevCycle())
86
+ self._raise_if_unhandled(state)
85
87
 
86
88
  # Create state object
87
89
  cycle_state = HevCycleState(
@@ -116,6 +118,7 @@ class HevLight(Light):
116
118
  ValueError: If duration is negative
117
119
  LifxDeviceNotFoundError: If device is not connected
118
120
  LifxTimeoutError: If device does not respond
121
+ LifxUnsupportedCommandError: If device doesn't support this command
119
122
 
120
123
  Example:
121
124
  ```python
@@ -130,12 +133,13 @@ class HevLight(Light):
130
133
  raise ValueError(f"Duration must be non-negative, got {duration_seconds}")
131
134
 
132
135
  # Request automatically handles acknowledgement
133
- await self.connection.request(
136
+ result = await self.connection.request(
134
137
  packets.Light.SetHevCycle(
135
138
  enable=enable,
136
139
  duration_s=duration_seconds,
137
140
  ),
138
141
  )
142
+ self._raise_if_unhandled(result)
139
143
 
140
144
  _LOGGER.debug(
141
145
  {
@@ -156,6 +160,7 @@ class HevLight(Light):
156
160
  LifxDeviceNotFoundError: If device is not connected
157
161
  LifxTimeoutError: If device does not respond
158
162
  LifxProtocolError: If response is invalid
163
+ LifxUnsupportedCommandError: If device doesn't support this command
159
164
 
160
165
  Example:
161
166
  ```python
@@ -166,6 +171,7 @@ class HevLight(Light):
166
171
  """
167
172
  # Request HEV configuration
168
173
  state = await self.connection.request(packets.Light.GetHevCycleConfiguration())
174
+ self._raise_if_unhandled(state)
169
175
 
170
176
  # Create config object
171
177
  config = HevConfig(
@@ -201,6 +207,7 @@ class HevLight(Light):
201
207
  ValueError: If duration is negative
202
208
  LifxDeviceNotFoundError: If device is not connected
203
209
  LifxTimeoutError: If device does not respond
210
+ LifxUnsupportedCommandError: If device doesn't support this command
204
211
 
205
212
  Example:
206
213
  ```python
@@ -212,12 +219,13 @@ class HevLight(Light):
212
219
  raise ValueError(f"Duration must be non-negative, got {duration_seconds}")
213
220
 
214
221
  # Request automatically handles acknowledgement
215
- await self.connection.request(
222
+ result = await self.connection.request(
216
223
  packets.Light.SetHevCycleConfiguration(
217
224
  indication=indication,
218
225
  duration_s=duration_seconds,
219
226
  ),
220
227
  )
228
+ self._raise_if_unhandled(result)
221
229
 
222
230
  # Update cached state
223
231
  self._hev_config = HevConfig(indication=indication, duration_s=duration_seconds)
@@ -242,6 +250,7 @@ class HevLight(Light):
242
250
  LifxDeviceNotFoundError: If device is not connected
243
251
  LifxTimeoutError: If device does not respond
244
252
  LifxProtocolError: If response is invalid
253
+ LifxUnsupportedCommandError: If device doesn't support this command
245
254
 
246
255
  Example:
247
256
  ```python
@@ -254,6 +263,7 @@ class HevLight(Light):
254
263
  """
255
264
  # Request last HEV result
256
265
  state = await self.connection.request(packets.Light.GetLastHevCycleResult())
266
+ self._raise_if_unhandled(state)
257
267
 
258
268
  # Store cached state
259
269
  self._hev_result = state.result
lifx/devices/infrared.py CHANGED
@@ -58,6 +58,7 @@ class InfraredLight(Light):
58
58
  LifxDeviceNotFoundError: If device is not connected
59
59
  LifxTimeoutError: If device does not respond
60
60
  LifxProtocolError: If response is invalid
61
+ LifxUnsupportedCommandError: If device doesn't support this command
61
62
 
62
63
  Example:
63
64
  ```python
@@ -68,6 +69,7 @@ class InfraredLight(Light):
68
69
  """
69
70
  # Request infrared state
70
71
  state = await self.connection.request(packets.Light.GetInfrared())
72
+ self._raise_if_unhandled(state)
71
73
 
72
74
  # Convert from uint16 (0-65535) to float (0.0-1.0)
73
75
  brightness = state.brightness / 65535.0
@@ -96,6 +98,7 @@ class InfraredLight(Light):
96
98
  ValueError: If brightness is out of range
97
99
  LifxDeviceNotFoundError: If device is not connected
98
100
  LifxTimeoutError: If device does not respond
101
+ LifxUnsupportedCommandError: If device doesn't support this command
99
102
 
100
103
  Example:
101
104
  ```python
@@ -115,9 +118,10 @@ class InfraredLight(Light):
115
118
  brightness_u16 = max(0, min(65535, int(round(brightness * 65535))))
116
119
 
117
120
  # Request automatically handles acknowledgement
118
- await self.connection.request(
121
+ result = await self.connection.request(
119
122
  packets.Light.SetInfrared(brightness=brightness_u16),
120
123
  )
124
+ self._raise_if_unhandled(result)
121
125
 
122
126
  # Update cached state
123
127
  self._infrared = brightness
lifx/devices/light.py CHANGED
@@ -83,6 +83,7 @@ class Light(Device):
83
83
  LifxDeviceNotFoundError: If device is not connected
84
84
  LifxTimeoutError: If device does not respond
85
85
  LifxProtocolError: If response is invalid
86
+ LifxUnsupportedCommandError: If device doesn't support this command
86
87
 
87
88
  Example:
88
89
  ```python
@@ -92,6 +93,7 @@ class Light(Device):
92
93
  """
93
94
  # Request automatically unpacks response and decodes labels
94
95
  state = await self.connection.request(packets.Light.GetColor())
96
+ self._raise_if_unhandled(state)
95
97
 
96
98
  # Convert from protocol HSBK to user-friendly HSBK
97
99
  color = HSBK.from_protocol(state.color)
@@ -133,6 +135,7 @@ class Light(Device):
133
135
  Raises:
134
136
  LifxDeviceNotFoundError: If device is not connected
135
137
  LifxTimeoutError: If device does not respond
138
+ LifxUnsupportedCommandError: If device doesn't support this command
136
139
 
137
140
  Example:
138
141
  ```python
@@ -150,12 +153,13 @@ class Light(Device):
150
153
  duration_ms = int(duration * 1000)
151
154
 
152
155
  # Request automatically handles acknowledgement
153
- await self.connection.request(
156
+ result = await self.connection.request(
154
157
  packets.Light.SetColor(
155
158
  color=protocol_color,
156
159
  duration=duration_ms,
157
160
  ),
158
161
  )
162
+ self._raise_if_unhandled(result)
159
163
 
160
164
  _LOGGER.debug(
161
165
  {
@@ -357,6 +361,7 @@ class Light(Device):
357
361
  LifxDeviceNotFoundError: If device is not connected
358
362
  LifxTimeoutError: If device does not respond
359
363
  LifxProtocolError: If response is invalid
364
+ LifxUnsupportedCommandError: If device doesn't support this command
360
365
 
361
366
  Example:
362
367
  ```python
@@ -366,6 +371,7 @@ class Light(Device):
366
371
  """
367
372
  # Request automatically unpacks response
368
373
  state = await self.connection.request(packets.Light.GetPower())
374
+ self._raise_if_unhandled(state)
369
375
 
370
376
  # Power level is uint16 (0 or 65535)
371
377
  _LOGGER.debug(
@@ -394,6 +400,7 @@ class Light(Device):
394
400
  LifxDeviceNotFoundError: If device is not connected
395
401
  LifxTimeoutError: If device does not respond
396
402
  LifxProtocolError: If response is invalid
403
+ LifxUnsupportedCommandError: If device doesn't support this command
397
404
 
398
405
  Example:
399
406
  ```python
@@ -406,6 +413,7 @@ class Light(Device):
406
413
  """
407
414
  # Request automatically unpacks response
408
415
  state = await self.connection.request(packets.Sensor.GetAmbientLight())
416
+ self._raise_if_unhandled(state)
409
417
 
410
418
  _LOGGER.debug(
411
419
  {
@@ -432,6 +440,7 @@ class Light(Device):
432
440
  ValueError: If integer value is not 0 or 65535
433
441
  LifxDeviceNotFoundError: If device is not connected
434
442
  LifxTimeoutError: If device does not respond
443
+ LifxUnsupportedCommandError: If device doesn't support this command
435
444
 
436
445
  Example:
437
446
  ```python
@@ -460,9 +469,10 @@ class Light(Device):
460
469
  duration_ms = int(duration * 1000)
461
470
 
462
471
  # Request automatically handles acknowledgement
463
- await self.connection.request(
472
+ result = await self.connection.request(
464
473
  packets.Light.SetPower(level=power_level, duration=duration_ms),
465
474
  )
475
+ self._raise_if_unhandled(result)
466
476
 
467
477
  _LOGGER.debug(
468
478
  {
@@ -499,6 +509,7 @@ class Light(Device):
499
509
  ValueError: If parameters are out of range
500
510
  LifxDeviceNotFoundError: If device is not connected
501
511
  LifxTimeoutError: If device does not respond
512
+ LifxUnsupportedCommandError: If device doesn't support this command
502
513
 
503
514
  Example:
504
515
  ```python
@@ -537,7 +548,7 @@ class Light(Device):
537
548
  skew_ratio_i16 = int(skew_ratio * 65535) - 32768 # Convert to int16 range
538
549
 
539
550
  # Send request
540
- await self.connection.request(
551
+ result = await self.connection.request(
541
552
  packets.Light.SetWaveform(
542
553
  transient=bool(transient),
543
554
  color=protocol_color,
@@ -547,6 +558,7 @@ class Light(Device):
547
558
  waveform=waveform,
548
559
  ),
549
560
  )
561
+ self._raise_if_unhandled(result)
550
562
  _LOGGER.debug(
551
563
  {
552
564
  "class": "Device",
@@ -602,6 +614,7 @@ class Light(Device):
602
614
  ValueError: If parameters are out of range
603
615
  LifxDeviceNotFoundError: If device is not connected
604
616
  LifxTimeoutError: If device does not respond
617
+ LifxUnsupportedCommandError: If device doesn't support this command
605
618
 
606
619
  Example:
607
620
  ```python
@@ -647,7 +660,7 @@ class Light(Device):
647
660
  skew_ratio_i16 = int(skew_ratio * 65535) - 32768 # Convert to int16 range
648
661
 
649
662
  # Send request
650
- await self.connection.request(
663
+ result = await self.connection.request(
651
664
  packets.Light.SetWaveformOptional(
652
665
  transient=bool(transient),
653
666
  color=protocol_color,
@@ -661,6 +674,7 @@ class Light(Device):
661
674
  set_kelvin=set_kelvin,
662
675
  ),
663
676
  )
677
+ self._raise_if_unhandled(result)
664
678
  _LOGGER.debug(
665
679
  {
666
680
  "class": "Device",
lifx/devices/matrix.py CHANGED
@@ -311,6 +311,11 @@ class MatrixLight(Light):
311
311
  Returns:
312
312
  List of TileInfo objects describing each tile in the chain
313
313
 
314
+ Raises:
315
+ LifxDeviceNotFoundError: If device is not connected
316
+ LifxTimeoutError: If device does not respond
317
+ LifxUnsupportedCommandError: If device doesn't support this command
318
+
314
319
  Example:
315
320
  >>> chain = await matrix.get_device_chain()
316
321
  >>> for tile in chain:
@@ -321,6 +326,7 @@ class MatrixLight(Light):
321
326
  response: packets.Tile.StateDeviceChain = await self.connection.request(
322
327
  packets.Tile.GetDeviceChain()
323
328
  )
329
+ self._raise_if_unhandled(response)
324
330
 
325
331
  # Parse tiles from response
326
332
  tiles = []
@@ -393,6 +399,11 @@ class MatrixLight(Light):
393
399
  returns the actual zone count (e.g., 64 for 8x8, 16 for 4x4). For tiles
394
400
  with >64 zones (e.g., 128 for 16x8 Ceiling), returns 64 (protocol limit).
395
401
 
402
+ Raises:
403
+ LifxDeviceNotFoundError: If device is not connected
404
+ LifxTimeoutError: If device does not respond
405
+ LifxUnsupportedCommandError: If device doesn't support this command
406
+
396
407
  Example:
397
408
  >>> # Get all colors from first tile (no parameters needed)
398
409
  >>> colors = await matrix.get64()
@@ -432,6 +443,7 @@ class MatrixLight(Light):
432
443
  rect=TileBufferRect(fb_index=0, x=x, y=y, width=width),
433
444
  )
434
445
  )
446
+ self._raise_if_unhandled(response)
435
447
 
436
448
  max_colors = device_chain[0].width * device_chain[0].height
437
449
 
@@ -714,6 +726,11 @@ class MatrixLight(Light):
714
726
  Returns:
715
727
  MatrixEffect describing the current effect state
716
728
 
729
+ Raises:
730
+ LifxDeviceNotFoundError: If device is not connected
731
+ LifxTimeoutError: If device does not respond
732
+ LifxUnsupportedCommandError: If device doesn't support this command
733
+
717
734
  Example:
718
735
  >>> effect = await matrix.get_effect()
719
736
  >>> print(f"Effect type: {effect.effect_type}")
@@ -723,6 +740,7 @@ class MatrixLight(Light):
723
740
  response: packets.Tile.StateEffect = await self.connection.request(
724
741
  packets.Tile.GetEffect()
725
742
  )
743
+ self._raise_if_unhandled(response)
726
744
 
727
745
  # Convert protocol effect to MatrixEffect
728
746
  palette = [
lifx/devices/multizone.py CHANGED
@@ -189,6 +189,7 @@ class MultiZoneLight(Light):
189
189
  LifxDeviceNotFoundError: If device is not connected
190
190
  LifxTimeoutError: If device does not respond
191
191
  LifxProtocolError: If response is invalid
192
+ LifxUnsupportedCommandError: If device doesn't support this command
192
193
 
193
194
  Example:
194
195
  ```python
@@ -205,6 +206,7 @@ class MultiZoneLight(Light):
205
206
  state = await self.connection.request(
206
207
  packets.MultiZone.GetColorZones(start_index=0, end_index=0)
207
208
  )
209
+ self._raise_if_unhandled(state)
208
210
 
209
211
  count = state.count
210
212
 
@@ -245,6 +247,7 @@ class MultiZoneLight(Light):
245
247
  LifxDeviceNotFoundError: If device is not connected
246
248
  LifxTimeoutError: If device does not respond
247
249
  LifxProtocolError: If response is invalid
250
+ LifxUnsupportedCommandError: If device doesn't support this command
248
251
 
249
252
  Example:
250
253
  ```python
@@ -279,6 +282,7 @@ class MultiZoneLight(Light):
279
282
  start_index=current_start, end_index=current_end
280
283
  )
281
284
  ):
285
+ self._raise_if_unhandled(state)
282
286
  # Extract colors from response (up to 8 colors)
283
287
  zones_in_response = min(8, current_end - current_start + 1)
284
288
  for i in range(zones_in_response):
@@ -336,6 +340,7 @@ class MultiZoneLight(Light):
336
340
  LifxDeviceNotFoundError: If device is not connected
337
341
  LifxTimeoutError: If device does not respond
338
342
  LifxProtocolError: If response is invalid
343
+ LifxUnsupportedCommandError: If device doesn't support this command
339
344
 
340
345
  Example:
341
346
  ```python
@@ -361,6 +366,7 @@ class MultiZoneLight(Light):
361
366
  packets.MultiZone.GetExtendedColorZones(),
362
367
  timeout=2.0, # Allow time for multiple responses
363
368
  ):
369
+ self._raise_if_unhandled(packet)
364
370
  # Only process valid colors based on colors_count
365
371
  for i in range(packet.colors_count):
366
372
  if i >= len(packet.colors):
@@ -449,6 +455,7 @@ class MultiZoneLight(Light):
449
455
  ValueError: If zone indices are invalid
450
456
  LifxDeviceNotFoundError: If device is not connected
451
457
  LifxTimeoutError: If device does not respond
458
+ LifxUnsupportedCommandError: If device doesn't support this command
452
459
 
453
460
  Example:
454
461
  ```python
@@ -477,7 +484,7 @@ class MultiZoneLight(Light):
477
484
  duration_ms = int(duration * 1000)
478
485
 
479
486
  # Send request
480
- await self.connection.request(
487
+ result = await self.connection.request(
481
488
  packets.MultiZone.SetColorZones(
482
489
  start_index=start,
483
490
  end_index=end,
@@ -486,6 +493,7 @@ class MultiZoneLight(Light):
486
493
  apply=apply,
487
494
  ),
488
495
  )
496
+ self._raise_if_unhandled(result)
489
497
 
490
498
  _LOGGER.debug(
491
499
  {
@@ -529,6 +537,7 @@ class MultiZoneLight(Light):
529
537
  ValueError: If colors list is too long or zone index is invalid
530
538
  LifxDeviceNotFoundError: If device is not connected
531
539
  LifxTimeoutError: If device does not respond
540
+ LifxUnsupportedCommandError: If device doesn't support this command
532
541
 
533
542
  Example:
534
543
  ```python
@@ -556,7 +565,7 @@ class MultiZoneLight(Light):
556
565
  duration_ms = int(duration * 1000)
557
566
 
558
567
  # Send request
559
- await self.connection.request(
568
+ result = await self.connection.request(
560
569
  packets.MultiZone.SetExtendedColorZones(
561
570
  duration=duration_ms,
562
571
  apply=apply,
@@ -565,6 +574,7 @@ class MultiZoneLight(Light):
565
574
  colors=protocol_colors,
566
575
  ),
567
576
  )
577
+ self._raise_if_unhandled(result)
568
578
 
569
579
  _LOGGER.debug(
570
580
  {
@@ -602,6 +612,7 @@ class MultiZoneLight(Light):
602
612
  LifxDeviceNotFoundError: If device is not connected
603
613
  LifxTimeoutError: If device does not respond
604
614
  LifxProtocolError: If response is invalid
615
+ LifxUnsupportedCommandError: If device doesn't support this command
605
616
 
606
617
  Example:
607
618
  ```python
@@ -616,6 +627,7 @@ class MultiZoneLight(Light):
616
627
  """
617
628
  # Request automatically unpacks response
618
629
  state = await self.connection.request(packets.MultiZone.GetEffect())
630
+ self._raise_if_unhandled(state)
619
631
 
620
632
  settings = state.settings
621
633
  effect_type = settings.effect_type
@@ -672,6 +684,7 @@ class MultiZoneLight(Light):
672
684
  Raises:
673
685
  LifxDeviceNotFoundError: If device is not connected
674
686
  LifxTimeoutError: If device does not respond
687
+ LifxUnsupportedCommandError: If device doesn't support this command
675
688
 
676
689
  Example:
677
690
  ```python
@@ -701,7 +714,7 @@ class MultiZoneLight(Light):
701
714
  parameters = parameters[:8]
702
715
 
703
716
  # Send request
704
- await self.connection.request(
717
+ result = await self.connection.request(
705
718
  packets.MultiZone.SetEffect(
706
719
  settings=MultiZoneEffectSettings(
707
720
  instanceid=0, # 0 for new effect
@@ -721,10 +734,11 @@ class MultiZoneLight(Light):
721
734
  ),
722
735
  ),
723
736
  )
737
+ self._raise_if_unhandled(result)
724
738
 
725
739
  # Update cached state
726
- result = effect if effect.effect_type != FirmwareEffect.OFF else None
727
- self._multizone_effect = result
740
+ cached_effect = effect if effect.effect_type != FirmwareEffect.OFF else None
741
+ self._multizone_effect = cached_effect
728
742
 
729
743
  _LOGGER.debug(
730
744
  {
@@ -22,7 +22,6 @@ from lifx.exceptions import (
22
22
  LifxConnectionError,
23
23
  LifxProtocolError,
24
24
  LifxTimeoutError,
25
- LifxUnsupportedCommandError,
26
25
  )
27
26
  from lifx.network.message import create_message, parse_message
28
27
  from lifx.network.transport import UdpTransport
@@ -443,13 +442,12 @@ class DeviceConnection:
443
442
  LifxConnectionError: If connection is not open
444
443
  LifxProtocolError: If response correlation validation fails
445
444
  LifxTimeoutError: If no response after all retries
446
- LifxUnsupportedCommandError: If device doesn't support command
447
445
  """
448
446
  if not self._is_open or self._transport is None:
449
- raise LifxConnectionError("Connection not open")
447
+ raise LifxConnectionError("Connection not open") # pragma: no cover
450
448
 
451
449
  if timeout is None:
452
- timeout = self.timeout
450
+ timeout = self.timeout # pragma: no cover
453
451
 
454
452
  if max_retries is None:
455
453
  max_retries = self.max_retries
@@ -597,12 +595,6 @@ class DeviceConnection:
597
595
  f"got {header.sequence}, max expected {max_expected}"
598
596
  )
599
597
 
600
- # Check for StateUnhandled
601
- if header.pkt_type == _STATE_UNHANDLED_PKT_TYPE:
602
- raise LifxUnsupportedCommandError(
603
- "Request unsupported by device: received StateUnhandled"
604
- )
605
-
606
598
  # Yield response (can be from any retry attempt)
607
599
  has_yielded = True
608
600
  last_response_time = time.monotonic()
@@ -640,7 +632,7 @@ class DeviceConnection:
640
632
  request: Any,
641
633
  timeout: float | None = None,
642
634
  max_retries: int | None = None,
643
- ) -> AsyncGenerator[None, None]:
635
+ ) -> AsyncGenerator[bool, None]:
644
636
  """Internal implementation of request_ack_stream with retry logic.
645
637
 
646
638
  This is an async generator that sends a request requiring acknowledgement
@@ -652,18 +644,17 @@ class DeviceConnection:
652
644
  max_retries: Maximum retries
653
645
 
654
646
  Yields:
655
- None (single yield on successful ack)
647
+ True for successful ACK, False if device returned StateUnhandled
656
648
 
657
649
  Raises:
658
650
  LifxConnectionError: If connection is not open
659
651
  LifxTimeoutError: If no ack after all retries
660
- LifxUnsupportedCommandError: If device doesn't support command
661
652
  """
662
653
  if not self._is_open or self._transport is None:
663
- raise LifxConnectionError("Connection not open")
654
+ raise LifxConnectionError("Connection not open") # pragma: no cover
664
655
 
665
656
  if timeout is None:
666
- timeout = self.timeout
657
+ timeout = self.timeout # pragma: no cover
667
658
 
668
659
  if max_retries is None:
669
660
  max_retries = self.max_retries
@@ -729,14 +720,13 @@ class DeviceConnection:
729
720
  f"{Serial.from_protocol(header.target).to_string()})"
730
721
  )
731
722
 
732
- # Check for StateUnhandled
723
+ # Check for StateUnhandled - return False to indicate unsupported
733
724
  if header.pkt_type == _STATE_UNHANDLED_PKT_TYPE:
734
- raise LifxUnsupportedCommandError(
735
- "Request unsupported by device: received StateUnhandled"
736
- )
725
+ yield False
726
+ return
737
727
 
738
728
  # ACK received successfully
739
- yield
729
+ yield True
740
730
  return
741
731
 
742
732
  except TimeoutError as e:
@@ -791,14 +781,14 @@ class DeviceConnection:
791
781
  timeout: Request timeout in seconds
792
782
 
793
783
  Yields:
794
- Unpacked response packet instances
795
- For SET packets: yields True once (acknowledgement)
784
+ Unpacked response packet instances (including StateUnhandled if device
785
+ doesn't support the command)
786
+ For SET packets: yields True (acknowledgement) or False (StateUnhandled)
796
787
 
797
788
  Raises:
798
789
  LifxTimeoutError: If request times out
799
790
  LifxProtocolError: If response invalid
800
791
  LifxConnectionError: If connection fails
801
- LifxUnsupportedCommandError: If command not supported
802
792
 
803
793
  Example:
804
794
  ```python
@@ -808,11 +798,16 @@ class DeviceConnection:
808
798
  label = state.label # Already decoded to string
809
799
  break
810
800
 
811
- # SET request yields True (acknowledgement)
812
- async for _ in conn.request_stream(
801
+ # SET request yields True (acknowledgement) or False (StateUnhandled)
802
+ async for result in conn.request_stream(
813
803
  packets.Light.SetColor(color=hsbk, duration=1000)
814
804
  ):
815
- # Acknowledgement received
805
+ if result:
806
+ # Acknowledgement received
807
+ pass
808
+ else:
809
+ # Device doesn't support this command
810
+ pass
816
811
  break
817
812
 
818
813
  # Multi-response GET - stream all responses
@@ -875,9 +870,12 @@ class DeviceConnection:
875
870
 
876
871
  elif packet_kind == "SET":
877
872
  # Request acknowledgement
878
- async for _ in self._request_ack_stream_impl(packet, timeout=timeout):
873
+ async for ack_result in self._request_ack_stream_impl(
874
+ packet, timeout=timeout
875
+ ):
879
876
  # Log the request/ack cycle
880
877
  request_values = packet.as_dict
878
+ reply_packet = "Acknowledgement" if ack_result else "StateUnhandled"
881
879
  _LOGGER.debug(
882
880
  {
883
881
  "class": "DeviceConnection",
@@ -887,7 +885,7 @@ class DeviceConnection:
887
885
  "values": request_values,
888
886
  },
889
887
  "reply": {
890
- "packet": "Acknowledgement",
888
+ "packet": reply_packet,
891
889
  "values": {},
892
890
  },
893
891
  "serial": self.serial,
@@ -895,7 +893,7 @@ class DeviceConnection:
895
893
  }
896
894
  )
897
895
 
898
- yield True
896
+ yield ack_result
899
897
  return
900
898
 
901
899
  else:
@@ -934,7 +932,7 @@ class DeviceConnection:
934
932
  yield response_packet
935
933
  return
936
934
  else:
937
- raise LifxUnsupportedCommandError(
935
+ raise LifxProtocolError(
938
936
  f"Cannot auto-handle packet kind: {packet_kind}"
939
937
  )
940
938
  else:
@@ -961,14 +959,14 @@ class DeviceConnection:
961
959
  timeout: Request timeout in seconds
962
960
 
963
961
  Returns:
964
- Single unpacked response packet
965
- True for SET acknowledgement
962
+ Single unpacked response packet (including StateUnhandled if device
963
+ doesn't support the command)
964
+ For SET packets: True (acknowledgement) or False (StateUnhandled)
966
965
 
967
966
  Raises:
968
967
  LifxTimeoutError: If no response within timeout
969
968
  LifxProtocolError: If response invalid
970
969
  LifxConnectionError: If connection fails
971
- LifxUnsupportedCommandError: If command not supported
972
970
 
973
971
  Example:
974
972
  ```python
@@ -977,10 +975,13 @@ class DeviceConnection:
977
975
  color = HSBK.from_protocol(state.color)
978
976
  label = state.label # Already decoded to string
979
977
 
980
- # SET request returns True
978
+ # SET request returns True or False
981
979
  success = await conn.request(
982
980
  packets.Light.SetColor(color=hsbk, duration=1000)
983
981
  )
982
+ if not success:
983
+ # Device doesn't support this command (returned StateUnhandled)
984
+ pass
984
985
  ```
985
986
  """
986
987
  async for response in self.request_stream(packet, timeout):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lifx-async
3
- Version: 4.3.5
3
+ Version: 4.3.7
4
4
  Summary: A modern, type-safe, async Python library for controlling LIFX lights
5
5
  Author-email: Avi Miller <me@dje.li>
6
6
  Maintainer-email: Avi Miller <me@dje.li>
@@ -5,12 +5,12 @@ lifx/const.py,sha256=dW64lf_jwAD40GSd6hkFkrni5j-w2qkV3pl6YNdCxv4,3426
5
5
  lifx/exceptions.py,sha256=pikAMppLn7gXyjiQVWM_tSvXKNh-g366nG_UWyqpHhc,815
6
6
  lifx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  lifx/devices/__init__.py,sha256=V7hW8sM_RwFgbR4Hv1ByR1JLhYq7Ft1X9pylQjCXYB8,777
8
- lifx/devices/base.py,sha256=uD3hQe2kjycRZneSptON6psOhoEgPRHVelCoLgdHbFw,41482
9
- lifx/devices/hev.py,sha256=2zZNYm3TFrL755B4cRPNdYtcDLZEQwGl_22112WsSZc,9504
10
- lifx/devices/infrared.py,sha256=TrCgJyEioIPlFumMmcSmuGYmRsSGlQ5Rllg6_9Wtg4Y,4248
11
- lifx/devices/light.py,sha256=9rL24fa44Y7QrRBDSQuG6xpWsaPbQTTm4ExvrDnYWHo,27572
12
- lifx/devices/matrix.py,sha256=mleYYsXkvvfXxV_pScb_D7VoX0AEJkClpzpNXOzQyGs,32643
13
- lifx/devices/multizone.py,sha256=c-lXcp8c1Mhs8me6smGkqQFrOOxdoGjWrOO5HnAVooY,27209
8
+ lifx/devices/base.py,sha256=IxwFbKHnmfjhqzlqW66jqHslLUAHq1ocgSTVYXu6yxc,43906
9
+ lifx/devices/hev.py,sha256=ow4AU3eOVAcMK2KKAyQUTB7z6EDoRz7StwVOvwwS4Sk,10124
10
+ lifx/devices/infrared.py,sha256=q8q_cpjdRwojk76jBEdBeIYmqAA4FuTy7ZUquy2yEdg,4498
11
+ lifx/devices/light.py,sha256=EvUeCtjMS23PUoj3cOshFJ8SYT517ksH_3p27J3Sr2o,28452
12
+ lifx/devices/matrix.py,sha256=8VI02LtL_hzIzyXMu1be6QEY8W1e7-jtuitKJ6clFW8,33426
13
+ lifx/devices/multizone.py,sha256=JaKpMvpxz7-RnhkJ1gS6uYrXwEFdJUpyldQsGtXZb_g,28106
14
14
  lifx/effects/__init__.py,sha256=4DF31yp7RJic5JoltMlz5dCtF5KQobU6NOUtLUKkVKE,1509
15
15
  lifx/effects/base.py,sha256=YO0Hbg2VYHKPtfYnWxmrtzYoPGOi9BUXhn8HVFKv5IM,10283
16
16
  lifx/effects/colorloop.py,sha256=kuuyENJS2irAN8vZAFsDa2guQdDbmmc4PJNiyZTfFPE,15840
@@ -20,7 +20,7 @@ lifx/effects/models.py,sha256=MS5D-cxD0Ar8XhqbqKAc9q2sk38IP1vPkYwd8V7jCr8,2446
20
20
  lifx/effects/pulse.py,sha256=t5eyjfFWG1xT-RXKghRqHYJ9CG_50tPu4jsDapJZ2mw,8721
21
21
  lifx/effects/state_manager.py,sha256=iDfYowiCN5IJqcR1s-pM0mQEJpe-RDsMcOOSMmtPVDE,8983
22
22
  lifx/network/__init__.py,sha256=uSyA8r8qISG7qXUHbX8uk9A2E8rvDADgCcf94QIZ9so,499
23
- lifx/network/connection.py,sha256=K5zzwYWKBvSBMqcP6a5Fp9JkVIiOLXJSAi1XMT4g-Go,38191
23
+ lifx/network/connection.py,sha256=hM7BxpG4udLCMWV18trbgbV_yjPsX5e_V4boCf8eZYs,38278
24
24
  lifx/network/discovery.py,sha256=FoFoZcw3dtJs1daESiZiNXytanKQsMTdF9PjOxEgHM0,23804
25
25
  lifx/network/message.py,sha256=jCLC9v0tbBi54g5CaHLFM_nP1Izu8kJmo2tt23HHBbA,2600
26
26
  lifx/network/transport.py,sha256=8QS0YV32rdP0EDiPEwuvZXbplRWL08pmjKybd87mkZ0,11070
@@ -40,7 +40,7 @@ lifx/theme/canvas.py,sha256=4h7lgN8iu_OdchObGDgbxTqQLCb-FRKC-M-YCWef_i4,8048
40
40
  lifx/theme/generators.py,sha256=L0X6_iApLx6XDboGlYunaVsl6nvUCqMfn23VQmRkyCk,6125
41
41
  lifx/theme/library.py,sha256=tKlKZNqJp8lRGDnilWyDm_Qr1vCRGGwuvWVS82anNpQ,21326
42
42
  lifx/theme/theme.py,sha256=qMEx_8E41C0Cc6f083XHiAXEglTv4YlXW0UFsG1rQKg,5521
43
- lifx_async-4.3.5.dist-info/METADATA,sha256=S7yNp4EX1b3pNjZUoGGcF7GX5_YpHgpeh0qhKClN0S4,2609
44
- lifx_async-4.3.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
45
- lifx_async-4.3.5.dist-info/licenses/LICENSE,sha256=eBz48GRA3gSiWn3rYZAz2Ewp35snnhV9cSqkVBq7g3k,1832
46
- lifx_async-4.3.5.dist-info/RECORD,,
43
+ lifx_async-4.3.7.dist-info/METADATA,sha256=LiDbfeEvXW8ofmnMBzN2OP_4nRa_gSo8Q2RWAqFM6U8,2609
44
+ lifx_async-4.3.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
45
+ lifx_async-4.3.7.dist-info/licenses/LICENSE,sha256=eBz48GRA3gSiWn3rYZAz2Ewp35snnhV9cSqkVBq7g3k,1832
46
+ lifx_async-4.3.7.dist-info/RECORD,,