lifx-async 5.1.0__py3-none-any.whl → 5.1.1__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/animation/packets.py CHANGED
@@ -51,7 +51,7 @@ ACK_REQUIRED = 0
51
51
  RES_REQUIRED = 0
52
52
 
53
53
 
54
- @dataclass
54
+ @dataclass(slots=True)
55
55
  class PacketTemplate:
56
56
  """Prebaked packet template for zero-allocation animation.
57
57
 
@@ -63,12 +63,14 @@ class PacketTemplate:
63
63
  color_offset: Byte offset where color data starts
64
64
  color_count: Number of HSBK colors in this packet
65
65
  hsbk_start: Starting index in the input HSBK array
66
+ fmt: Pre-computed struct format string for bulk color packing
66
67
  """
67
68
 
68
69
  data: bytearray
69
70
  color_offset: int
70
71
  color_count: int
71
72
  hsbk_start: int
73
+ fmt: str
72
74
 
73
75
 
74
76
  def _build_header(
@@ -103,9 +105,8 @@ def _build_header(
103
105
  # Frame Address (16 bytes)
104
106
  # target (8 bytes) + reserved (6 bytes) + flags (1 byte) + sequence (1 byte)
105
107
  target_padded = target + b"\x00\x00" if len(target) == 6 else target
106
- target_int = int.from_bytes(target_padded, byteorder="little")
107
108
  flags = (RES_REQUIRED & 0b1) | ((ACK_REQUIRED & 0b1) << 1)
108
- struct.pack_into("<Q6sBB", header, 8, target_int, b"\x00" * 6, flags, 0)
109
+ struct.pack_into("<8s6sBB", header, 8, target_padded, b"\x00" * 6, flags, 0)
109
110
 
110
111
  # Protocol Header (12 bytes)
111
112
  struct.pack_into("<QHH", header, 24, 0, pkt_type, 0)
@@ -277,12 +278,14 @@ class MatrixPacketGenerator(PacketGenerator):
277
278
  # Combine header + payload
278
279
  packet = header + payload
279
280
 
281
+ color_count = min(self._pixels_per_tile, 64)
280
282
  templates.append(
281
283
  PacketTemplate(
282
284
  data=packet,
283
285
  color_offset=HEADER_SIZE + self._COLORS_OFFSET_IN_PAYLOAD,
284
- color_count=min(self._pixels_per_tile, 64),
286
+ color_count=color_count,
285
287
  hsbk_start=tile_idx * self._pixels_per_tile,
288
+ fmt=f"<{'HHHH' * color_count}",
286
289
  )
287
290
  )
288
291
 
@@ -338,6 +341,7 @@ class MatrixPacketGenerator(PacketGenerator):
338
341
  color_offset=HEADER_SIZE + self._COLORS_OFFSET_IN_PAYLOAD,
339
342
  color_count=color_count,
340
343
  hsbk_start=tile_pixel_start + color_start,
344
+ fmt=f"<{'HHHH' * color_count}",
341
345
  )
342
346
  )
343
347
 
@@ -369,6 +373,7 @@ class MatrixPacketGenerator(PacketGenerator):
369
373
  color_offset=0, # No colors
370
374
  color_count=0,
371
375
  hsbk_start=0,
376
+ fmt="",
372
377
  )
373
378
  )
374
379
 
@@ -387,10 +392,13 @@ class MatrixPacketGenerator(PacketGenerator):
387
392
  if tmpl.color_count == 0:
388
393
  continue # Skip CopyFrameBuffer packets
389
394
 
390
- for i in range(tmpl.color_count):
391
- h, s, b, k = hsbk[tmpl.hsbk_start + i]
392
- offset = tmpl.color_offset + i * 8
393
- struct.pack_into("<HHHH", tmpl.data, offset, h, s, b, k)
395
+ # Flatten HSBK tuples and pack in one bulk call
396
+ start = tmpl.hsbk_start
397
+ end = start + tmpl.color_count
398
+ flat: list[int] = []
399
+ for h, s, b, k in hsbk[start:end]:
400
+ flat.extend((h, s, b, k))
401
+ struct.pack_into(tmpl.fmt, tmpl.data, tmpl.color_offset, *flat)
394
402
 
395
403
 
396
404
  class MultiZonePacketGenerator(PacketGenerator):
@@ -476,6 +484,7 @@ class MultiZonePacketGenerator(PacketGenerator):
476
484
  color_offset=HEADER_SIZE + self._COLORS_OFFSET_IN_PAYLOAD,
477
485
  color_count=zone_count,
478
486
  hsbk_start=zone_start,
487
+ fmt=f"<{'HHHH' * zone_count}",
479
488
  )
480
489
  )
481
490
 
@@ -491,7 +500,10 @@ class MultiZonePacketGenerator(PacketGenerator):
491
500
  hsbk: Protocol-ready HSBK data for all zones
492
501
  """
493
502
  for tmpl in templates:
494
- for i in range(tmpl.color_count):
495
- h, s, b, k = hsbk[tmpl.hsbk_start + i]
496
- offset = tmpl.color_offset + i * 8
497
- struct.pack_into("<HHHH", tmpl.data, offset, h, s, b, k)
503
+ # Flatten HSBK tuples and pack in one bulk call
504
+ start = tmpl.hsbk_start
505
+ end = start + tmpl.color_count
506
+ flat: list[int] = []
507
+ for h, s, b, k in hsbk[start:end]:
508
+ flat.extend((h, s, b, k))
509
+ struct.pack_into(tmpl.fmt, tmpl.data, tmpl.color_offset, *flat)
lifx/devices/base.py CHANGED
@@ -710,29 +710,29 @@ class Device(Generic[StateT]):
710
710
 
711
711
  return self._mac_address
712
712
 
713
- async def _ensure_capabilities(self) -> None:
714
- """Ensure device capabilities are populated.
713
+ def _process_capabilities(
714
+ self, version: DeviceVersion, host_firmware: FirmwareInfo
715
+ ) -> None:
716
+ """Process device capabilities from already-fetched version and firmware data.
715
717
 
716
- This fetches the device version and firmware to determine product capabilities.
717
- If the device claims extended_multizone support but firmware is too old,
718
- the capability is removed.
718
+ Looks up product info from version.product, checks extended_multizone firmware
719
+ requirement, and sets self._capabilities. No-op if capabilities already set.
719
720
 
720
- Called automatically when entering context manager, but can be called manually.
721
+ Args:
722
+ version: Device version info (vendor, product)
723
+ host_firmware: Host firmware info for extended multizone check
721
724
  """
722
- if self._capabilities is not None: # pragma: no cover
725
+ if self._capabilities is not None:
723
726
  return
724
727
 
725
- # Get device version to determine product ID
726
- version = await self.get_version()
727
728
  self._capabilities = get_product(version.product)
728
729
 
729
730
  # If device has extended_multizone with minimum firmware requirement, verify it
730
731
  if self._capabilities and self._capabilities.has_extended_multizone:
731
732
  if self._capabilities.min_ext_mz_firmware is not None:
732
- firmware = await self.get_host_firmware()
733
733
  firmware_version = (
734
- firmware.version_major << 16
735
- ) | firmware.version_minor
734
+ host_firmware.version_major << 16
735
+ ) | host_firmware.version_minor
736
736
 
737
737
  # If firmware is too old, remove the extended_multizone capability
738
738
  if (
@@ -744,6 +744,23 @@ class Device(Generic[StateT]):
744
744
  ~ProductCapability.EXTENDED_MULTIZONE
745
745
  )
746
746
 
747
+ async def _ensure_capabilities(self) -> None:
748
+ """Ensure device capabilities are populated.
749
+
750
+ This fetches the device version and firmware to determine product capabilities.
751
+ If the device claims extended_multizone support but firmware is too old,
752
+ the capability is removed.
753
+
754
+ Called automatically when entering context manager, but can be called manually.
755
+ """
756
+ if self._capabilities is not None: # pragma: no cover
757
+ return
758
+
759
+ # Get device version to determine product ID
760
+ version = await self.get_version()
761
+ host_firmware = await self.get_host_firmware()
762
+ self._process_capabilities(version, host_firmware)
763
+
747
764
  @property
748
765
  def capabilities(self) -> ProductInfo | None:
749
766
  """Get device product capabilities.
@@ -1661,32 +1678,62 @@ class Device(Generic[StateT]):
1661
1678
  This is an all-or-nothing operation - either all state is fetched successfully
1662
1679
  or an exception is raised.
1663
1680
 
1681
+ When capabilities are not pre-loaded, get_version() runs in parallel with the
1682
+ other GET requests to save one network round-trip.
1683
+
1664
1684
  Raises:
1665
1685
  LifxTimeoutError: If device does not respond within timeout
1666
1686
  LifxDeviceNotFoundError: If device cannot be reached
1667
1687
  LifxProtocolError: If responses are invalid
1668
1688
  """
1669
- # Ensure capabilities are loaded
1670
- await self._ensure_capabilities()
1671
- capabilities = self._create_capabilities()
1672
-
1673
- # Fetch semi-static and volatile state in parallel
1674
- # get_color returns color, power, and label in one request
1675
- (
1676
- label,
1677
- power,
1678
- host_firmware,
1679
- wifi_firmware,
1680
- location_info,
1681
- group_info,
1682
- ) = await asyncio.gather(
1683
- self.get_label(),
1684
- self.get_power(),
1685
- self.get_host_firmware(),
1686
- self.get_wifi_firmware(),
1687
- self.get_location(),
1688
- self.get_group(),
1689
- )
1689
+ if self._capabilities is not None:
1690
+ # Capabilities pre-loaded: skip get_version(), run 6 requests in parallel
1691
+ capabilities = self._create_capabilities()
1692
+
1693
+ (
1694
+ label,
1695
+ power,
1696
+ host_firmware,
1697
+ wifi_firmware,
1698
+ location_info,
1699
+ group_info,
1700
+ ) = await asyncio.gather(
1701
+ self.get_label(),
1702
+ self.get_power(),
1703
+ self.get_host_firmware(),
1704
+ self.get_wifi_firmware(),
1705
+ self.get_location(),
1706
+ self.get_group(),
1707
+ )
1708
+ else:
1709
+ # Capabilities not loaded: include get_version() in parallel batch
1710
+ # Schedule get_version() concurrently alongside the 6-arg gather
1711
+ version_task = asyncio.ensure_future(self.get_version())
1712
+ try:
1713
+ (
1714
+ label,
1715
+ power,
1716
+ host_firmware,
1717
+ wifi_firmware,
1718
+ location_info,
1719
+ group_info,
1720
+ ) = await asyncio.gather(
1721
+ self.get_label(),
1722
+ self.get_power(),
1723
+ self.get_host_firmware(),
1724
+ self.get_wifi_firmware(),
1725
+ self.get_location(),
1726
+ self.get_group(),
1727
+ )
1728
+ version = await version_task
1729
+ except Exception:
1730
+ if not version_task.done():
1731
+ version_task.cancel()
1732
+ # Await to consume cancellation and avoid leaked task warnings
1733
+ await asyncio.gather(version_task, return_exceptions=True)
1734
+ raise
1735
+ self._process_capabilities(version, host_firmware)
1736
+ capabilities = self._create_capabilities()
1690
1737
 
1691
1738
  # Get MAC address (already calculated in get_host_firmware)
1692
1739
  mac_address = await self.get_mac_address()
lifx/devices/light.py CHANGED
@@ -946,9 +946,8 @@ class Light(Device[LightState]):
946
946
  """Initialize light state transactionally.
947
947
 
948
948
  Extends base implementation to fetch color in addition to base state.
949
-
950
- Args:
951
- timeout: Timeout for state initialization
949
+ When capabilities are not pre-loaded, get_version() runs in parallel with
950
+ the other GET requests to save one network round-trip.
952
951
 
953
952
  Raises:
954
953
  LifxTimeoutError: If device does not respond within timeout
@@ -957,26 +956,50 @@ class Light(Device[LightState]):
957
956
  """
958
957
  import time
959
958
 
960
- # Ensure capabilities are loaded
961
- await self._ensure_capabilities()
962
- capabilities = self._create_capabilities()
963
-
964
- # Fetch semi-static and volatile state in parallel
965
- # get_color returns color, power, and label in one request
966
959
  try:
967
- (
968
- (color, power, label),
969
- host_firmware,
970
- wifi_firmware,
971
- location_info,
972
- group_info,
973
- ) = await asyncio.gather(
974
- self.get_color(),
975
- self.get_host_firmware(),
976
- self.get_wifi_firmware(),
977
- self.get_location(),
978
- self.get_group(),
979
- )
960
+ if self._capabilities is not None:
961
+ # Capabilities pre-loaded: skip get_version()
962
+ capabilities = self._create_capabilities()
963
+
964
+ (
965
+ (color, power, label),
966
+ host_firmware,
967
+ wifi_firmware,
968
+ location_info,
969
+ group_info,
970
+ ) = await asyncio.gather(
971
+ self.get_color(),
972
+ self.get_host_firmware(),
973
+ self.get_wifi_firmware(),
974
+ self.get_location(),
975
+ self.get_group(),
976
+ )
977
+ else:
978
+ # Capabilities not loaded: include get_version() in parallel batch
979
+ version_task = asyncio.ensure_future(self.get_version())
980
+ try:
981
+ (
982
+ (color, power, label),
983
+ host_firmware,
984
+ wifi_firmware,
985
+ location_info,
986
+ group_info,
987
+ ) = await asyncio.gather(
988
+ self.get_color(),
989
+ self.get_host_firmware(),
990
+ self.get_wifi_firmware(),
991
+ self.get_location(),
992
+ self.get_group(),
993
+ )
994
+ version = await version_task
995
+ except Exception:
996
+ if not version_task.done():
997
+ version_task.cancel()
998
+ # Await to consume cancellation and avoid leaked task warnings
999
+ await asyncio.gather(version_task, return_exceptions=True)
1000
+ raise
1001
+ self._process_capabilities(version, host_firmware)
1002
+ capabilities = self._create_capabilities()
980
1003
 
981
1004
  # Get MAC address (already calculated in get_host_firmware)
982
1005
  mac_address = await self.get_mac_address()
@@ -110,6 +110,21 @@ class DeviceConnection:
110
110
  self._is_open = False
111
111
  self._is_opening = False # Flag to prevent concurrent open() calls
112
112
 
113
+ # Pre-compute serial bytes for fast comparison in background receiver
114
+ self._is_discovery = serial == "000000000000"
115
+ if not self._is_discovery:
116
+ serial_obj = Serial.from_string(serial)
117
+ self._target_bytes: bytes | None = serial_obj.to_protocol()
118
+ else:
119
+ self._target_bytes = None
120
+
121
+ # Pre-compute target bytes for send_packet() to avoid
122
+ # re-parsing on every send
123
+ if self._target_bytes is not None:
124
+ self._send_target: bytes = self._target_bytes
125
+ else:
126
+ self._send_target: bytes = b"\x00" * 8
127
+
113
128
  # Background receiver task infrastructure
114
129
  # Key: (source, sequence, serial) → Queue of (header, payload) tuples
115
130
  self._pending_requests: dict[
@@ -262,12 +277,11 @@ class DeviceConnection:
262
277
  if source is None:
263
278
  source = self._allocate_source()
264
279
 
265
- target = Serial.from_string(self.serial).to_protocol()
266
280
  message = create_message(
267
281
  packet=packet,
268
282
  source=source,
269
283
  sequence=sequence,
270
- target=target,
284
+ target=self._send_target,
271
285
  ack_required=ack_required,
272
286
  res_required=res_required,
273
287
  )
@@ -360,12 +374,19 @@ class DeviceConnection:
360
374
  )
361
375
 
362
376
  # Compute correlation key (includes serial for defense-in-depth)
363
- # For discovery connections (self.serial == "000000000000"), always use
364
- # "000000000000" for correlation regardless of response serial
365
- if self.serial == "000000000000":
377
+ # For discovery connections, always use "000000000000" for correlation
378
+ # regardless of response serial
379
+ if self._is_discovery:
366
380
  serial = "000000000000"
367
381
  else:
368
- serial = Serial.from_protocol(header.target).to_string()
382
+ # Compare target bytes directly to avoid string conversion
383
+ if (
384
+ self._target_bytes is not None
385
+ and header.target == self._target_bytes
386
+ ):
387
+ serial = self.serial
388
+ else:
389
+ serial = Serial.from_protocol(header.target).to_string()
369
390
  key = (header.source, header.sequence, serial)
370
391
 
371
392
  # Route to waiting request
@@ -572,11 +593,14 @@ class DeviceConnection:
572
593
 
573
594
  # Validate correlation (defense in depth)
574
595
  # For discovery connections, skip serial validation
575
- if self.serial != "000000000000":
576
- response_serial = Serial.from_protocol(
577
- header.target
578
- ).to_string()
579
- if response_serial != self.serial:
596
+ if not self._is_discovery:
597
+ if (
598
+ self._target_bytes is not None
599
+ and header.target != self._target_bytes
600
+ ):
601
+ response_serial = Serial.from_protocol(
602
+ header.target
603
+ ).to_string()
580
604
  raise LifxProtocolError(
581
605
  f"Response serial mismatch: "
582
606
  f"expected {self.serial}, got {response_serial}"
@@ -710,16 +734,21 @@ class DeviceConnection:
710
734
  )
711
735
 
712
736
  # Validate correlation
737
+ serial_mismatch = (
738
+ self._target_bytes is not None
739
+ and header.target != self._target_bytes
740
+ )
713
741
  if (
714
742
  header.source != request_source
715
743
  or header.sequence != sequence
716
- or Serial.from_protocol(header.target).to_string() != self.serial
744
+ or serial_mismatch
717
745
  ):
746
+ response_serial = Serial.from_protocol(header.target).to_string()
718
747
  raise LifxProtocolError(
719
748
  f"ACK correlation mismatch: "
720
749
  f"expected ({request_source}, {sequence}, {self.serial}), "
721
750
  f"got ({header.source}, {header.sequence}, "
722
- f"{Serial.from_protocol(header.target).to_string()})"
751
+ f"{response_serial})"
723
752
  )
724
753
 
725
754
  # Check for StateUnhandled - return False to indicate unsupported
@@ -848,6 +877,10 @@ class DeviceConnection:
848
877
  serial = Serial(value=header.target_serial).to_string()
849
878
  if self.serial == "000000000000" and serial != self.serial:
850
879
  self.serial = serial
880
+ # Refresh cached fields now that we know the real serial
881
+ self._is_discovery = False
882
+ self._target_bytes = Serial.from_string(serial).to_protocol()
883
+ self._send_target = self._target_bytes
851
884
 
852
885
  # Unpack (labels are automatically decoded by Packet.unpack())
853
886
  response_packet = packet_class.unpack(payload)
lifx/protocol/base.py CHANGED
@@ -6,8 +6,42 @@ Provides generic pack/unpack functionality for all packet types.
6
6
  from __future__ import annotations
7
7
 
8
8
  import logging
9
+ import re
9
10
  from dataclasses import asdict, dataclass
10
- from typing import Any, ClassVar
11
+ from typing import TYPE_CHECKING, Any, ClassVar
12
+
13
+ if TYPE_CHECKING:
14
+ from types import ModuleType
15
+
16
+ # Lazy-cached module references to avoid circular imports at module load time.
17
+ # These modules import from each other indirectly through packets.py, so we
18
+ # defer the import to first use and cache the result at module level.
19
+ _serializer: ModuleType | None = None
20
+ _protocol_types: ModuleType | None = None
21
+
22
+
23
+ def _get_serializer() -> ModuleType:
24
+ """Get the serializer module, importing on first use."""
25
+ global _serializer # noqa: PLW0603
26
+ if _serializer is None:
27
+ from lifx.protocol import serializer
28
+
29
+ _serializer = serializer
30
+ return _serializer
31
+
32
+
33
+ def _get_protocol_types() -> ModuleType:
34
+ """Get the protocol_types module, importing on first use."""
35
+ global _protocol_types # noqa: PLW0603
36
+ if _protocol_types is None:
37
+ from lifx.protocol import protocol_types
38
+
39
+ _protocol_types = protocol_types
40
+ return _protocol_types
41
+
42
+
43
+ _ARRAY_PATTERN = re.compile(r"\[(\d+)\](.+)")
44
+ _PASCAL_TO_SNAKE = re.compile(r"(?<!^)(?=[A-Z])")
11
45
 
12
46
  _LOGGER = logging.getLogger(__name__)
13
47
 
@@ -37,7 +71,7 @@ class Packet:
37
71
  Returns:
38
72
  Packed bytes ready to send in a LIFX message payload
39
73
  """
40
- from lifx.protocol import serializer
74
+ serializer = _get_serializer()
41
75
 
42
76
  result = b""
43
77
 
@@ -154,14 +188,12 @@ class Packet:
154
188
  @staticmethod
155
189
  def _protocol_to_python_name(name: str) -> str:
156
190
  """Convert protocol name (PascalCase) to Python name (snake_case)."""
157
- import re
158
-
159
- snake = re.sub(r"(?<!^)(?=[A-Z])", "_", name)
191
+ snake = _PASCAL_TO_SNAKE.sub("_", name)
160
192
  return snake.lower()
161
193
 
162
194
  def _pack_field_value(self, value: Any, field_type: str, size_bytes: int) -> bytes:
163
195
  """Pack a single field value based on its type."""
164
- from lifx.protocol import serializer
196
+ serializer = _get_serializer()
165
197
 
166
198
  # Parse field type
167
199
  base_type, array_count, is_nested = self._parse_field_type(field_type)
@@ -211,25 +243,19 @@ class Packet:
211
243
  cls, data: bytes, field_type: str, size_bytes: int, offset: int
212
244
  ) -> tuple[Any, int]:
213
245
  """Unpack a single field value based on its type."""
214
- from lifx.protocol import serializer
215
- from lifx.protocol.protocol_types import (
216
- DeviceService,
217
- FirmwareEffect,
218
- LightLastHevCycleResult,
219
- LightWaveform,
220
- MultiZoneApplicationRequest,
221
- )
246
+ serializer = _get_serializer()
247
+ protocol_types = _get_protocol_types()
222
248
 
223
249
  # Parse field type
224
250
  base_type, array_count, is_nested = cls._parse_field_type(field_type)
225
251
 
226
252
  # Check if it's an enum (Button/Relay enums excluded)
227
253
  enum_types = {
228
- "DeviceService": DeviceService,
229
- "LightLastHevCycleResult": LightLastHevCycleResult,
230
- "LightWaveform": LightWaveform,
231
- "MultiZoneApplicationRequest": MultiZoneApplicationRequest,
232
- "FirmwareEffect": FirmwareEffect,
254
+ "DeviceService": protocol_types.DeviceService,
255
+ "LightLastHevCycleResult": protocol_types.LightLastHevCycleResult,
256
+ "LightWaveform": protocol_types.LightWaveform,
257
+ "MultiZoneApplicationRequest": protocol_types.MultiZoneApplicationRequest,
258
+ "FirmwareEffect": protocol_types.FirmwareEffect,
233
259
  }
234
260
  is_enum = is_nested and base_type in enum_types
235
261
 
@@ -247,9 +273,7 @@ class Packet:
247
273
  result.append(enum_class(item_raw))
248
274
  return result, current_offset
249
275
  elif is_nested:
250
- # Array of nested structures - need to import dynamically
251
- from lifx.protocol import protocol_types
252
-
276
+ # Array of nested structures
253
277
  struct_class = getattr(protocol_types, base_type)
254
278
  result = []
255
279
  current_offset = offset
@@ -276,9 +300,7 @@ class Packet:
276
300
  value_raw, new_offset = serializer.unpack_value(data, "uint8", offset)
277
301
  return enum_class(value_raw), new_offset
278
302
  elif is_nested:
279
- # Nested structure - import dynamically
280
- from lifx.protocol import protocol_types
281
-
303
+ # Nested structure
282
304
  struct_class = getattr(protocol_types, base_type)
283
305
  # Check if it's a Packet subclass or protocol_types class
284
306
  if issubclass(struct_class, cls):
@@ -324,10 +346,8 @@ class Packet:
324
346
  Returns:
325
347
  Tuple of (base_type, array_count, is_nested)
326
348
  """
327
- import re
328
-
329
349
  # Check for array: [N]type
330
- array_match = re.match(r"\[(\d+)\](.+)", field_type)
350
+ array_match = _ARRAY_PATTERN.match(field_type)
331
351
  if array_match:
332
352
  count = int(array_match.group(1))
333
353
  inner_type = array_match.group(2)
lifx/protocol/header.py CHANGED
@@ -141,8 +141,8 @@ class LifxHeader:
141
141
  flags = (int(self.res_required) & 0b1) | ((int(self.ack_required) & 0b1) << 1)
142
142
 
143
143
  frame_addr = struct.pack(
144
- "<Q6sBB",
145
- int.from_bytes(self.target, byteorder="little"),
144
+ "<8s6sBB",
145
+ self.target,
146
146
  b"\x00" * 6, # reserved
147
147
  flags,
148
148
  self.sequence,
@@ -189,8 +189,7 @@ class LifxHeader:
189
189
  raise ValueError("Addressable bit must be set")
190
190
 
191
191
  # Unpack Frame Address (16 bytes)
192
- target_int, _reserved, flags, sequence = struct.unpack("<Q6sBB", data[8:24])
193
- target = target_int.to_bytes(8, byteorder="little")
192
+ target, _reserved, flags, sequence = struct.unpack("<8s6sBB", data[8:24])
194
193
 
195
194
  res_required = bool(flags & 0b1)
196
195
  ack_required = bool((flags >> 1) & 0b1)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lifx-async
3
- Version: 5.1.0
3
+ Version: 5.1.1
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>
@@ -8,13 +8,13 @@ lifx/animation/__init__.py,sha256=1EqPk26BTEADYR5qLD1UKb7cNnVfnLo5vfAnix6yyu0,23
8
8
  lifx/animation/animator.py,sha256=LyruwE4Z0gHnkGJfgug017-Mt0R-3FCrDoEsqGjE5ps,10154
9
9
  lifx/animation/framebuffer.py,sha256=ya3mbYtu5hepn2Fxfc3wEck5KhmmAfcbmv0RoIO-yC4,14116
10
10
  lifx/animation/orientation.py,sha256=bUMV4czLn5PH3inMObfyvbYD-jaSSH-LWaLOby-mvTs,5839
11
- lifx/animation/packets.py,sha256=jMNfe0EKb9oMn96UdYqJshlncYTx61IX6OzKo4SjFcg,17017
11
+ lifx/animation/packets.py,sha256=dnbwpmuirCbxph9mK_FNb7WCi7Mju-pRoVKfcIOIfUI,17498
12
12
  lifx/devices/__init__.py,sha256=4b5QtO0EFWxIqN2lUYgM8uLjWyHI5hUcReiF9QCjCGw,1061
13
- lifx/devices/base.py,sha256=mhNLX6FoLBaZtYo9InleneYdb0dk3B2Ze8Z2eqXCNHo,63180
13
+ lifx/devices/base.py,sha256=NV0GxG0-O7VLT55_z6nBqdU-YGHxdmaPs2Ki00EZkRY,65200
14
14
  lifx/devices/ceiling.py,sha256=cmGeEyads2O5e2H2VBsk6n0An4dZtT59HQvN2F9b4gA,45771
15
15
  lifx/devices/hev.py,sha256=kTRJRYnWyIY8Pkg_jOn978N-_1YXy9fRmBiGgEWscXw,15194
16
16
  lifx/devices/infrared.py,sha256=ePk9qxX_s-hv5gQMvio1Vv8FYiCd68HF0ySbWgSrvuU,8130
17
- lifx/devices/light.py,sha256=ZhC7zuruZ9nzmnAR_st2KMUH8UNQAcNK-eQUYnKXm-8,33833
17
+ lifx/devices/light.py,sha256=9yjmEeUqO4ATK6NaDMnlDzdddHlCouMlJcjotgCZzkI,35074
18
18
  lifx/devices/matrix.py,sha256=reB6cS2_cFe3qZKg584oCO-JGLbNTJDWwW9FZ0NLxq0,41693
19
19
  lifx/devices/multizone.py,sha256=7Te5Z_X9hDvdypjMqPGGM2TG0P9QltzFVi7UUxRdbGI,33326
20
20
  lifx/effects/__init__.py,sha256=4DF31yp7RJic5JoltMlz5dCtF5KQobU6NOUtLUKkVKE,1509
@@ -26,7 +26,7 @@ lifx/effects/models.py,sha256=MS5D-cxD0Ar8XhqbqKAc9q2sk38IP1vPkYwd8V7jCr8,2446
26
26
  lifx/effects/pulse.py,sha256=k4dtBhhgVHyuwzqzx89jYVKbSRUVQdZj91cklyKarbE,8455
27
27
  lifx/effects/state_manager.py,sha256=iDfYowiCN5IJqcR1s-pM0mQEJpe-RDsMcOOSMmtPVDE,8983
28
28
  lifx/network/__init__.py,sha256=uSyA8r8qISG7qXUHbX8uk9A2E8rvDADgCcf94QIZ9so,499
29
- lifx/network/connection.py,sha256=_DiXNKN80ki266gg2e4kTWvcPxgYQJPJJUP8nVC6woU,38454
29
+ lifx/network/connection.py,sha256=XUPSHBkeR3cLVwglJWmPZqSYUTPoB5_kzRMJXBgzixs,39866
30
30
  lifx/network/discovery.py,sha256=J0B3yRkbZKx7g01CCIEnjv3gtqSORhmdTYQ6w0ea4WI,24178
31
31
  lifx/network/message.py,sha256=jCLC9v0tbBi54g5CaHLFM_nP1Izu8kJmo2tt23HHBbA,2600
32
32
  lifx/network/transport.py,sha256=EykhKmvjAcdiepgCxgzDTj8Fc0b7kAQdyR8AumGXohg,10953
@@ -41,9 +41,9 @@ lifx/products/generator.py,sha256=DsTCJcEVPmn9sfXSbXYdFZjqMfIbodnIQL46DRASs0g,15
41
41
  lifx/products/quirks.py,sha256=B8Kb4pxaXmovMbjgXRfPPWre5JEvJrn8d6PAWK_FT1U,2544
42
42
  lifx/products/registry.py,sha256=ILIJlQxcxJUzRH-LGU_bnHjV-TxDEucKovuJcWvG4q8,43831
43
43
  lifx/protocol/__init__.py,sha256=-wjC-wBcb7fxi5I-mJr2Ad8K2YRflJFdLLdobfD-W1Q,56
44
- lifx/protocol/base.py,sha256=x4cKT5sbaEmILbmPH3y5Lwk6gj3h9Xv_JvTX91cPQwM,12354
44
+ lifx/protocol/base.py,sha256=xqO3qYpiSMY-_vcR6P2OEgITIbzm0AKRZs1yzOz7Y7M,13078
45
45
  lifx/protocol/generator.py,sha256=RWu4zC-b_JhonLM5fbAufQyKxEI2myLyCnt7tu3POMI,60690
46
- lifx/protocol/header.py,sha256=HaYQ5wEjAMgefO3dIxKb0w4VG4fLcfLj-fnHVwfp1ao,7174
46
+ lifx/protocol/header.py,sha256=8KXe6C-T1XMyzepFV9fJ4_gsnO05HDk07XVtkwhiEU0,7076
47
47
  lifx/protocol/models.py,sha256=eOvOSAWbglR1SYWcC_YpicewtsdbVlQ6E2lfcC4NQrk,8172
48
48
  lifx/protocol/packets.py,sha256=ENp3irGITdV5rGah3eUzgsXqihI95upAPh7AdTsP7sk,43303
49
49
  lifx/protocol/protocol_types.py,sha256=m15A82zVrwAXomTqo-GfNmAIynVRDSV94UqHDkWgiJI,23781
@@ -53,7 +53,7 @@ lifx/theme/canvas.py,sha256=4h7lgN8iu_OdchObGDgbxTqQLCb-FRKC-M-YCWef_i4,8048
53
53
  lifx/theme/generators.py,sha256=nq3Yvntq_h-eFHbmmow3LcAdA_hEbRRaP5mv9Bydrjk,6435
54
54
  lifx/theme/library.py,sha256=tKlKZNqJp8lRGDnilWyDm_Qr1vCRGGwuvWVS82anNpQ,21326
55
55
  lifx/theme/theme.py,sha256=qMEx_8E41C0Cc6f083XHiAXEglTv4YlXW0UFsG1rQKg,5521
56
- lifx_async-5.1.0.dist-info/METADATA,sha256=bT9ZFTUQCHcsnPwdyskaOeOfBQ_HP71_e-6dvtGn6SE,2660
57
- lifx_async-5.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
58
- lifx_async-5.1.0.dist-info/licenses/LICENSE,sha256=eBz48GRA3gSiWn3rYZAz2Ewp35snnhV9cSqkVBq7g3k,1832
59
- lifx_async-5.1.0.dist-info/RECORD,,
56
+ lifx_async-5.1.1.dist-info/METADATA,sha256=Dd2gx4lZLL_JVddm3LeWwOgBWxm_JNAw1FkgWV0u6qw,2660
57
+ lifx_async-5.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
58
+ lifx_async-5.1.1.dist-info/licenses/LICENSE,sha256=eBz48GRA3gSiWn3rYZAz2Ewp35snnhV9cSqkVBq7g3k,1832
59
+ lifx_async-5.1.1.dist-info/RECORD,,