lifx-async 5.0.0__py3-none-any.whl → 5.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,6 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  import logging
7
7
  import random
8
- import secrets
9
8
  import time
10
9
  from collections.abc import AsyncGenerator
11
10
  from typing import TYPE_CHECKING, Any, TypeVar
@@ -17,6 +16,7 @@ from lifx.const import (
17
16
  DEFAULT_MAX_RETRIES,
18
17
  DEFAULT_REQUEST_TIMEOUT,
19
18
  LIFX_UDP_PORT,
19
+ TIMEOUT_ERRORS,
20
20
  )
21
21
  from lifx.exceptions import (
22
22
  LifxConnectionError,
@@ -26,6 +26,7 @@ from lifx.exceptions import (
26
26
  )
27
27
  from lifx.network.message import create_message, parse_message
28
28
  from lifx.network.transport import UdpTransport
29
+ from lifx.network.utils import allocate_source
29
30
  from lifx.protocol.header import LifxHeader
30
31
  from lifx.protocol.models import Serial
31
32
 
@@ -193,7 +194,7 @@ class DeviceConnection:
193
194
  await asyncio.wait_for(
194
195
  self._receiver_task, timeout=_RECEIVER_SHUTDOWN_TIMEOUT
195
196
  )
196
- except TimeoutError:
197
+ except TIMEOUT_ERRORS:
197
198
  self._receiver_task.cancel()
198
199
  try:
199
200
  await self._receiver_task
@@ -335,7 +336,7 @@ class DeviceConnection:
335
336
  Returns:
336
337
  Unique source identifier (range: 2 to 4294967295)
337
338
  """
338
- return secrets.randbelow(0xFFFFFFFF - 1) + 2
339
+ return allocate_source()
339
340
 
340
341
  async def _background_receiver(self) -> None:
341
342
  """Background task to receive and route packets.
@@ -559,7 +560,7 @@ class DeviceConnection:
559
560
  header, payload = await asyncio.wait_for(
560
561
  response_queue.get(), timeout=remaining_time
561
562
  )
562
- except TimeoutError:
563
+ except TIMEOUT_ERRORS:
563
564
  if not has_yielded:
564
565
  # No response this attempt, retry
565
566
  raise TimeoutError(
@@ -603,7 +604,7 @@ class DeviceConnection:
603
604
 
604
605
  # Continue loop to wait for more responses
605
606
 
606
- except TimeoutError as e:
607
+ except TIMEOUT_ERRORS as e:
607
608
  last_error = LifxTimeoutError(str(e))
608
609
  if attempt < max_retries:
609
610
  # Sleep with jitter before retry
@@ -702,7 +703,7 @@ class DeviceConnection:
702
703
  header, _payload = await asyncio.wait_for(
703
704
  response_queue.get(), timeout=current_timeout
704
705
  )
705
- except TimeoutError:
706
+ except TIMEOUT_ERRORS:
706
707
  raise TimeoutError(
707
708
  f"No acknowledgement within {current_timeout:.3f}s "
708
709
  f"(attempt {attempt + 1}/{max_retries + 1})"
@@ -731,7 +732,7 @@ class DeviceConnection:
731
732
  yield True
732
733
  return
733
734
 
734
- except TimeoutError as e:
735
+ except TIMEOUT_ERRORS as e:
735
736
  last_error = LifxTimeoutError(str(e))
736
737
  if attempt < max_retries:
737
738
  # Sleep with jitter before retry
lifx/network/discovery.py CHANGED
@@ -3,7 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
- import secrets
7
6
  import time
8
7
  from collections.abc import AsyncGenerator
9
8
  from dataclasses import dataclass, field
@@ -20,6 +19,7 @@ from lifx.const import (
20
19
  from lifx.exceptions import LifxProtocolError, LifxTimeoutError
21
20
  from lifx.network.message import create_message, parse_message
22
21
  from lifx.network.transport import UdpTransport
22
+ from lifx.network.utils import allocate_source
23
23
  from lifx.protocol.base import Packet
24
24
  from lifx.protocol.models import Serial
25
25
  from lifx.protocol.packets import Device as DevicePackets
@@ -219,7 +219,7 @@ async def _discover_with_packet(
219
219
 
220
220
  async with UdpTransport(port=0, broadcast=True) as transport:
221
221
  # Allocate unique source for this discovery session
222
- discovery_source = secrets.randbelow(0xFFFFFFFF - 1) + 2
222
+ discovery_source = allocate_source()
223
223
 
224
224
  message = create_message(
225
225
  packet=packet,
@@ -470,7 +470,7 @@ async def discover_devices(
470
470
  # Create transport with broadcast enabled
471
471
  async with UdpTransport(port=0, broadcast=True) as transport:
472
472
  # Allocate unique source for this discovery session
473
- discovery_source = secrets.randbelow(0xFFFFFFFF - 1) + 2
473
+ discovery_source = allocate_source()
474
474
 
475
475
  # Create discovery message
476
476
  discovery_packet = DevicePackets.GetService()
@@ -13,7 +13,7 @@ import struct
13
13
  from asyncio import DatagramTransport
14
14
  from typing import TYPE_CHECKING
15
15
 
16
- from lifx.const import MDNS_ADDRESS, MDNS_PORT
16
+ from lifx.const import MDNS_ADDRESS, MDNS_PORT, TIMEOUT_ERRORS
17
17
  from lifx.exceptions import LifxNetworkError, LifxTimeoutError
18
18
 
19
19
  if TYPE_CHECKING:
@@ -259,7 +259,7 @@ class MdnsTransport:
259
259
  self._protocol.queue.get(), timeout=timeout
260
260
  )
261
261
  return data, addr
262
- except TimeoutError as e:
262
+ except TIMEOUT_ERRORS as e:
263
263
  raise LifxTimeoutError(f"No mDNS data received within {timeout}s") from e
264
264
  except OSError as e:
265
265
  _LOGGER.debug(
lifx/network/transport.py CHANGED
@@ -6,7 +6,12 @@ import asyncio
6
6
  import logging
7
7
  from typing import TYPE_CHECKING
8
8
 
9
- from lifx.const import DEFAULT_IP_ADDRESS, MAX_PACKET_SIZE, MIN_PACKET_SIZE
9
+ from lifx.const import (
10
+ DEFAULT_IP_ADDRESS,
11
+ MAX_PACKET_SIZE,
12
+ MIN_PACKET_SIZE,
13
+ TIMEOUT_ERRORS,
14
+ )
10
15
  from lifx.exceptions import LifxNetworkError
11
16
  from lifx.exceptions import LifxTimeoutError as LifxTimeoutError
12
17
 
@@ -208,7 +213,7 @@ class UdpTransport:
208
213
  data, addr = await asyncio.wait_for(
209
214
  self._protocol.queue.get(), timeout=timeout
210
215
  )
211
- except TimeoutError as e:
216
+ except TIMEOUT_ERRORS as e:
212
217
  raise LifxTimeoutError(f"No data received within {timeout}s") from e
213
218
  except OSError as e:
214
219
  _LOGGER.error(
@@ -302,7 +307,7 @@ class UdpTransport:
302
307
  continue
303
308
 
304
309
  packets.append((data, addr))
305
- except TimeoutError:
310
+ except TIMEOUT_ERRORS:
306
311
  # Timeout is expected - return what we collected
307
312
  break
308
313
  except OSError:
lifx/network/utils.py ADDED
@@ -0,0 +1,15 @@
1
+ """Network utilities for LIFX protocol communication."""
2
+
3
+ import secrets
4
+
5
+
6
+ def allocate_source() -> int:
7
+ """Allocate unique source identifier for a LIFX protocol request.
8
+
9
+ LIFX protocol defines source as Uint32, with 0 and 1 reserved.
10
+ We generate values in range [2, 0xFFFFFFFF].
11
+
12
+ Returns:
13
+ Unique source identifier (range: 2 to 4294967295)
14
+ """
15
+ return secrets.randbelow(0xFFFFFFFF - 1) + 2
@@ -281,88 +281,3 @@ def unpack_bytes(data: bytes, length: int, offset: int = 0) -> tuple[bytes, int]
281
281
 
282
282
  raw_bytes = data[offset : offset + length]
283
283
  return raw_bytes, offset + length
284
-
285
-
286
- class FieldSerializer:
287
- """Serializer for structured fields with nested types."""
288
-
289
- def __init__(self, field_definitions: dict[str, dict[str, str]]):
290
- """Initialize serializer with field definitions.
291
-
292
- Args:
293
- field_definitions: Dict mapping field names to structure definitions
294
- """
295
- self.field_definitions = field_definitions
296
-
297
- def pack_field(self, field_data: dict[str, Any], field_name: str) -> bytes:
298
- """Pack a structured field.
299
-
300
- Args:
301
- field_data: Dictionary of field values
302
- field_name: Name of the field structure (e.g., "HSBK")
303
-
304
- Returns:
305
- Packed bytes
306
-
307
- Raises:
308
- ValueError: If field_name is unknown
309
- """
310
- if field_name not in self.field_definitions:
311
- raise ValueError(f"Unknown field: {field_name}")
312
-
313
- field_def = self.field_definitions[field_name]
314
- result = b""
315
-
316
- for attr_name, attr_type in field_def.items():
317
- if attr_name not in field_data:
318
- raise ValueError(f"Missing attribute {attr_name} in {field_name}")
319
- result += pack_value(field_data[attr_name], attr_type)
320
-
321
- return result
322
-
323
- def unpack_field(
324
- self, data: bytes, field_name: str, offset: int = 0
325
- ) -> tuple[dict[str, Any], int]:
326
- """Unpack a structured field.
327
-
328
- Args:
329
- data: Bytes to unpack from
330
- field_name: Name of the field structure
331
- offset: Offset to start unpacking
332
-
333
- Returns:
334
- Tuple of (field_dict, new_offset)
335
-
336
- Raises:
337
- ValueError: If field_name is unknown
338
- """
339
- if field_name not in self.field_definitions:
340
- raise ValueError(f"Unknown field: {field_name}")
341
-
342
- field_def = self.field_definitions[field_name]
343
- field_data: dict[str, Any] = {}
344
- current_offset = offset
345
-
346
- for attr_name, attr_type in field_def.items():
347
- value, current_offset = unpack_value(data, attr_type, current_offset)
348
- field_data[attr_name] = value
349
-
350
- return field_data, current_offset
351
-
352
- def get_field_size(self, field_name: str) -> int:
353
- """Get the size in bytes of a field structure.
354
-
355
- Args:
356
- field_name: Name of the field structure
357
-
358
- Returns:
359
- Size in bytes
360
-
361
- Raises:
362
- ValueError: If field_name is unknown
363
- """
364
- if field_name not in self.field_definitions:
365
- raise ValueError(f"Unknown field: {field_name}")
366
-
367
- field_def = self.field_definitions[field_name]
368
- return sum(TYPE_SIZES[attr_type] for attr_type in field_def.values())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lifx-async
3
- Version: 5.0.0
3
+ Version: 5.1.0
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>
@@ -1,34 +1,40 @@
1
- lifx/__init__.py,sha256=DKHG1vFJvPw_LpMkQgZN85gyOSD8dnceq6LnEGgR9vs,2810
2
- lifx/api.py,sha256=tTE9H8eSVrFIr6rEGxedWoR4ChnqvTz2EUgEQk60fgU,35428
1
+ lifx/__init__.py,sha256=GVGsRhvFvKMFmZmdZm9yFwbYh6RywOumGRsa5CyTK84,2914
2
+ lifx/api.py,sha256=A2cNGVewJR_ovzYYffkUT7Lknj0nzruwuhhoOtcAxwM,35134
3
3
  lifx/color.py,sha256=UfeoOiFgFih5edl2Ei-0wSzvZXRTI47yUm9GlNJZeTw,26041
4
- lifx/const.py,sha256=5LEh4h0-bEJlOfpG8fgyht0LkAEV9jkkpuCiuatBhEI,3840
4
+ lifx/const.py,sha256=M_6d1yFaWzalUAWzrVHv6BOIOu_1f6JJslP267Vd_kg,4573
5
5
  lifx/exceptions.py,sha256=pikAMppLn7gXyjiQVWM_tSvXKNh-g366nG_UWyqpHhc,815
6
6
  lifx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ lifx/animation/__init__.py,sha256=1EqPk26BTEADYR5qLD1UKb7cNnVfnLo5vfAnix6yyu0,2307
8
+ lifx/animation/animator.py,sha256=LyruwE4Z0gHnkGJfgug017-Mt0R-3FCrDoEsqGjE5ps,10154
9
+ lifx/animation/framebuffer.py,sha256=ya3mbYtu5hepn2Fxfc3wEck5KhmmAfcbmv0RoIO-yC4,14116
10
+ lifx/animation/orientation.py,sha256=bUMV4czLn5PH3inMObfyvbYD-jaSSH-LWaLOby-mvTs,5839
11
+ lifx/animation/packets.py,sha256=jMNfe0EKb9oMn96UdYqJshlncYTx61IX6OzKo4SjFcg,17017
7
12
  lifx/devices/__init__.py,sha256=4b5QtO0EFWxIqN2lUYgM8uLjWyHI5hUcReiF9QCjCGw,1061
8
13
  lifx/devices/base.py,sha256=mhNLX6FoLBaZtYo9InleneYdb0dk3B2Ze8Z2eqXCNHo,63180
9
- lifx/devices/ceiling.py,sha256=bLAurvqTNmhKMFUUJmLqn1vDFawapYju2i4G0pHOH_4,45790
14
+ lifx/devices/ceiling.py,sha256=cmGeEyads2O5e2H2VBsk6n0An4dZtT59HQvN2F9b4gA,45771
10
15
  lifx/devices/hev.py,sha256=kTRJRYnWyIY8Pkg_jOn978N-_1YXy9fRmBiGgEWscXw,15194
11
16
  lifx/devices/infrared.py,sha256=ePk9qxX_s-hv5gQMvio1Vv8FYiCd68HF0ySbWgSrvuU,8130
12
17
  lifx/devices/light.py,sha256=ZhC7zuruZ9nzmnAR_st2KMUH8UNQAcNK-eQUYnKXm-8,33833
13
- lifx/devices/matrix.py,sha256=TcfVvqFCIhGXjOtaQEARQCXe9XqSFDZ4wEQdRZBiQpA,42301
18
+ lifx/devices/matrix.py,sha256=reB6cS2_cFe3qZKg584oCO-JGLbNTJDWwW9FZ0NLxq0,41693
14
19
  lifx/devices/multizone.py,sha256=7Te5Z_X9hDvdypjMqPGGM2TG0P9QltzFVi7UUxRdbGI,33326
15
20
  lifx/effects/__init__.py,sha256=4DF31yp7RJic5JoltMlz5dCtF5KQobU6NOUtLUKkVKE,1509
16
21
  lifx/effects/base.py,sha256=tKgX5PsV6hipffD2B236rOzudkMwWq59-eQGnfvNKdU,10354
17
- lifx/effects/colorloop.py,sha256=Yz9XcQ_VhTPSnJn1s4WnkoXTRh2_qFJrPhQkIiTF6Tk,15574
22
+ lifx/effects/colorloop.py,sha256=LYlljT7XfaQ-S3mWNMWZ-3q7ce1cfl8nmyLb-VuXTZE,15592
18
23
  lifx/effects/conductor.py,sha256=Oaq-6m1kdUF6bma_U9GcA9onZzh6YRjpExBr-OGHQJI,14552
19
24
  lifx/effects/const.py,sha256=03LfL8v9PtoUs6-2IR3aa6nkyA4Otdno51SFJtntC-U,795
20
25
  lifx/effects/models.py,sha256=MS5D-cxD0Ar8XhqbqKAc9q2sk38IP1vPkYwd8V7jCr8,2446
21
26
  lifx/effects/pulse.py,sha256=k4dtBhhgVHyuwzqzx89jYVKbSRUVQdZj91cklyKarbE,8455
22
27
  lifx/effects/state_manager.py,sha256=iDfYowiCN5IJqcR1s-pM0mQEJpe-RDsMcOOSMmtPVDE,8983
23
28
  lifx/network/__init__.py,sha256=uSyA8r8qISG7qXUHbX8uk9A2E8rvDADgCcf94QIZ9so,499
24
- lifx/network/connection.py,sha256=qPKEkhr9DvoL2cCJoIlDi8rowNS-gUn_F3mesnzyFjs,38412
25
- lifx/network/discovery.py,sha256=syFfkDYWo0AEoBdEBjWqBm4K7UJwZW5x2K0FBMiA2I0,24186
29
+ lifx/network/connection.py,sha256=_DiXNKN80ki266gg2e4kTWvcPxgYQJPJJUP8nVC6woU,38454
30
+ lifx/network/discovery.py,sha256=J0B3yRkbZKx7g01CCIEnjv3gtqSORhmdTYQ6w0ea4WI,24178
26
31
  lifx/network/message.py,sha256=jCLC9v0tbBi54g5CaHLFM_nP1Izu8kJmo2tt23HHBbA,2600
27
- lifx/network/transport.py,sha256=io_3SFYQliNa_upHcKRzrLUkDVsDmNsxa2gQMlxj7Zk,10912
32
+ lifx/network/transport.py,sha256=EykhKmvjAcdiepgCxgzDTj8Fc0b7kAQdyR8AumGXohg,10953
33
+ lifx/network/utils.py,sha256=WLxyPh0JbA4SUHgcSqENMSBB2R8eW_J2fKCpAKZUxt4,421
28
34
  lifx/network/mdns/__init__.py,sha256=LlZgsFe6q5_SIXvXqtuZ_O9tJbcJZ-nsFkD2_wD8_TM,1412
29
35
  lifx/network/mdns/discovery.py,sha256=EZ2zlJmy96rMDmu5J-68ystXJ2gYa18zTYP3iqmTGgU,13200
30
36
  lifx/network/mdns/dns.py,sha256=OsvNSxLepIG3Nhw-kkQF3JrBYI-ikod5SHD2HO5_yGE,9363
31
- lifx/network/mdns/transport.py,sha256=41E_yX9Jx42ffElTcF-73A4ma48b3xkkHnG3DTcO8u8,10250
37
+ lifx/network/mdns/transport.py,sha256=x1IzvOYSsMcTY1zGIg0Cv694KU6Pyp8CaY0YQUI_-Nc,10268
32
38
  lifx/network/mdns/types.py,sha256=9fhH5iuMQxLkFPhmFTf2-kOcUNoWEu7LrN15Qr9tFE0,990
33
39
  lifx/products/__init__.py,sha256=pf2O-fzt6nOrQd-wmzhiog91tMiGa-dDbaSNtU2ZQfE,764
34
40
  lifx/products/generator.py,sha256=DsTCJcEVPmn9sfXSbXYdFZjqMfIbodnIQL46DRASs0g,15731
@@ -41,13 +47,13 @@ lifx/protocol/header.py,sha256=HaYQ5wEjAMgefO3dIxKb0w4VG4fLcfLj-fnHVwfp1ao,7174
41
47
  lifx/protocol/models.py,sha256=eOvOSAWbglR1SYWcC_YpicewtsdbVlQ6E2lfcC4NQrk,8172
42
48
  lifx/protocol/packets.py,sha256=ENp3irGITdV5rGah3eUzgsXqihI95upAPh7AdTsP7sk,43303
43
49
  lifx/protocol/protocol_types.py,sha256=m15A82zVrwAXomTqo-GfNmAIynVRDSV94UqHDkWgiJI,23781
44
- lifx/protocol/serializer.py,sha256=Cl87-Y8_LnvqFANjorJK2CMoRtBGksB_Eq07xHMTqH0,10387
50
+ lifx/protocol/serializer.py,sha256=IEnKKeCRy4Cwz03zkWhg-2pV3Lrro6cMa-3fhWYOngg,7714
45
51
  lifx/theme/__init__.py,sha256=dg4Y25dYq22EemFyxQ1fyb3D_bP2hhxGCd9BE1g_hvk,1320
46
52
  lifx/theme/canvas.py,sha256=4h7lgN8iu_OdchObGDgbxTqQLCb-FRKC-M-YCWef_i4,8048
47
53
  lifx/theme/generators.py,sha256=nq3Yvntq_h-eFHbmmow3LcAdA_hEbRRaP5mv9Bydrjk,6435
48
54
  lifx/theme/library.py,sha256=tKlKZNqJp8lRGDnilWyDm_Qr1vCRGGwuvWVS82anNpQ,21326
49
55
  lifx/theme/theme.py,sha256=qMEx_8E41C0Cc6f083XHiAXEglTv4YlXW0UFsG1rQKg,5521
50
- lifx_async-5.0.0.dist-info/METADATA,sha256=Ozi93MbWMkhFydTJ_nuyQUxRLLH_iGYOxtlr0iBXe2U,2660
51
- lifx_async-5.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
52
- lifx_async-5.0.0.dist-info/licenses/LICENSE,sha256=eBz48GRA3gSiWn3rYZAz2Ewp35snnhV9cSqkVBq7g3k,1832
53
- lifx_async-5.0.0.dist-info/RECORD,,
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,,