lifx-async 4.3.3__py3-none-any.whl → 4.3.5__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
@@ -252,6 +252,8 @@ class Device:
252
252
  self.serial = serial_obj.to_string()
253
253
  self.ip = ip
254
254
  self.port = port
255
+ self._timeout = timeout
256
+ self._max_retries = max_retries
255
257
 
256
258
  # Create lightweight connection handle - connection pooling is internal
257
259
  self.connection = DeviceConnection(
@@ -304,10 +306,16 @@ class Device:
304
306
  ```
305
307
  """
306
308
  if serial is None:
307
- temp_conn = DeviceConnection(serial="000000000000", ip=ip, port=port)
309
+ temp_conn = DeviceConnection(
310
+ serial="000000000000",
311
+ ip=ip,
312
+ port=port,
313
+ timeout=timeout,
314
+ max_retries=max_retries,
315
+ )
308
316
  try:
309
317
  response = await temp_conn.request(
310
- packets.Device.GetService(), timeout=DISCOVERY_TIMEOUT
318
+ packets.Device.GetService(), timeout=timeout
311
319
  )
312
320
  if response and isinstance(response, packets.Device.StateService):
313
321
  if temp_conn.serial and temp_conn.serial != "000000000000":
@@ -890,9 +898,17 @@ class Device:
890
898
 
891
899
  try:
892
900
  # Check each device for the target label
893
- async for disc in discover_devices(timeout=discover_timeout):
901
+ async for disc in discover_devices(
902
+ timeout=discover_timeout,
903
+ device_timeout=self._timeout,
904
+ max_retries=self._max_retries,
905
+ ):
894
906
  temp_conn = DeviceConnection(
895
- serial=disc.serial, ip=disc.ip, port=disc.port
907
+ serial=disc.serial,
908
+ ip=disc.ip,
909
+ port=disc.port,
910
+ timeout=self._timeout,
911
+ max_retries=self._max_retries,
896
912
  )
897
913
 
898
914
  try:
@@ -1063,9 +1079,17 @@ class Device:
1063
1079
 
1064
1080
  try:
1065
1081
  # Check each device for the target label
1066
- async for disc in discover_devices(timeout=discover_timeout):
1082
+ async for disc in discover_devices(
1083
+ timeout=discover_timeout,
1084
+ device_timeout=self._timeout,
1085
+ max_retries=self._max_retries,
1086
+ ):
1067
1087
  temp_conn = DeviceConnection(
1068
- serial=disc.serial, ip=disc.ip, port=disc.port
1088
+ serial=disc.serial,
1089
+ ip=disc.ip,
1090
+ port=disc.port,
1091
+ timeout=self._timeout,
1092
+ max_retries=self._max_retries,
1069
1093
  )
1070
1094
 
1071
1095
  try:
lifx/devices/matrix.py CHANGED
@@ -171,11 +171,6 @@ class MatrixEffect:
171
171
 
172
172
  def __post_init__(self) -> None:
173
173
  """Initialize defaults and validate fields."""
174
- # Initialize default palette if not provided
175
- if self.palette is None:
176
- # Default palette: single white color
177
- self.palette = [HSBK(0, 0, 1.0, 3500)]
178
-
179
174
  # Validate all fields
180
175
  # Speed can be 0 only when effect is OFF
181
176
  if self.effect_type != FirmwareEffect.OFF:
@@ -184,7 +179,11 @@ class MatrixEffect:
184
179
  raise ValueError(f"Effect speed must be non-negative, got {self.speed}")
185
180
 
186
181
  self._validate_duration(self.duration)
187
- self._validate_palette(self.palette)
182
+
183
+ # Only validate palette if provided
184
+ if self.palette is not None:
185
+ self._validate_palette(self.palette)
186
+
188
187
  self._validate_saturation(self.cloud_saturation_min, "cloud_saturation_min")
189
188
  self._validate_saturation(self.cloud_saturation_max, "cloud_saturation_max")
190
189
 
@@ -762,7 +761,7 @@ class MatrixLight(Light):
762
761
  effect_type: Type of effect (OFF, MORPH, FLAME, SKY)
763
762
  speed: Effect speed in seconds (default: 3)
764
763
  duration: Total effect duration in nanoseconds (0 for infinite)
765
- palette: Color palette for the effect (max 16 colors)
764
+ palette: Color palette for the effect (max 16 colors, None for no palette)
766
765
  sky_type: Sky effect type (SUNRISE, SUNSET, CLOUDS)
767
766
  cloud_saturation_min: Minimum cloud saturation (0-255, for CLOUDS)
768
767
  cloud_saturation_max: Maximum cloud saturation (0-255, for CLOUDS)
@@ -780,6 +779,12 @@ class MatrixLight(Light):
780
779
  ... speed=5.0,
781
780
  ... palette=rainbow,
782
781
  ... )
782
+
783
+ >>> # Set effect without a palette
784
+ >>> await matrix.set_effect(
785
+ ... effect_type=FirmwareEffect.FLAME,
786
+ ... speed=3.0,
787
+ ... )
783
788
  """
784
789
  _LOGGER.debug(
785
790
  "Setting matrix effect %s (speed=%d) for %s",
@@ -801,20 +806,22 @@ class MatrixLight(Light):
801
806
  )
802
807
 
803
808
  # Convert to protocol format
804
- # Note: palette is guaranteed to be non-None by MatrixEffect.__post_init__
805
- palette = effect.palette if effect.palette is not None else []
806
809
  proto_palette = []
807
- for color in palette:
808
- proto_palette.append(
809
- LightHsbk(
810
- hue=int(color.hue / 360 * 65535),
811
- saturation=int(color.saturation * 65535),
812
- brightness=int(color.brightness * 65535),
813
- kelvin=color.kelvin,
810
+ palette_count = 0
811
+
812
+ if effect.palette is not None:
813
+ palette_count = len(effect.palette)
814
+ for color in effect.palette:
815
+ proto_palette.append(
816
+ LightHsbk(
817
+ hue=int(color.hue / 360 * 65535),
818
+ saturation=int(color.saturation * 65535),
819
+ brightness=int(color.brightness * 65535),
820
+ kelvin=color.kelvin,
821
+ )
814
822
  )
815
- )
816
823
 
817
- # Pad palette to 16 colors
824
+ # Pad palette to 16 colors (protocol requirement)
818
825
  while len(proto_palette) < 16:
819
826
  proto_palette.append(LightHsbk(0, 0, 0, 3500))
820
827
 
@@ -828,7 +835,7 @@ class MatrixLight(Light):
828
835
  cloud_saturation_min=effect.cloud_saturation_min,
829
836
  cloud_saturation_max=effect.cloud_saturation_max,
830
837
  ),
831
- palette_count=len(palette),
838
+ palette_count=palette_count,
832
839
  palette=proto_palette,
833
840
  )
834
841
 
@@ -467,13 +467,10 @@ class DeviceConnection:
467
467
  correlation_keys: list[tuple[int, int, str]] = []
468
468
 
469
469
  # Calculate per-attempt timeouts with exponential backoff
470
- # Start with a reasonable minimum (100ms) to avoid too-short initial timeouts
471
- min_attempt_timeout = 0.1 # 100ms minimum
472
- if max_retries > 0:
473
- attempt_timeout = max(min_attempt_timeout, timeout / (2 * max_retries))
474
- else:
475
- # Only one attempt total, use entire timeout
476
- attempt_timeout = timeout
470
+ # Use proper exponential backoff distribution: timeout / (2^(n+1) - 1)
471
+ # This ensures total of all attempt timeouts equals the overall timeout budget
472
+ total_weight = (2 ** (max_retries + 1)) - 1
473
+ base_timeout = timeout / total_weight
477
474
 
478
475
  # Idle timeout for multi-response protocols
479
476
  # Stop streaming if no responses for this long after first response
@@ -482,14 +479,17 @@ class DeviceConnection:
482
479
  last_error: Exception | None = None
483
480
  has_yielded = False
484
481
  overall_start = time.monotonic()
482
+ total_sleep_time = 0.0 # Track sleep time to exclude from timeout budget
485
483
 
486
484
  try:
487
485
  for attempt in range(max_retries + 1):
488
486
  # Calculate current attempt timeout with exponential backoff
489
- current_timeout = min(
490
- attempt_timeout * (2**attempt),
491
- timeout - (time.monotonic() - overall_start),
487
+ # Exclude sleep time from elapsed time to preserve timeout budget
488
+ elapsed_response_time = (
489
+ time.monotonic() - overall_start - total_sleep_time
492
490
  )
491
+ ideal_timeout = base_timeout * (2**attempt)
492
+ current_timeout = min(ideal_timeout, timeout - elapsed_response_time)
493
493
 
494
494
  # Check if we've exceeded overall timeout budget
495
495
  if current_timeout <= 0:
@@ -616,6 +616,9 @@ class DeviceConnection:
616
616
  # Sleep with jitter before retry
617
617
  sleep_time = self._calculate_retry_sleep_with_jitter(attempt)
618
618
  await asyncio.sleep(sleep_time)
619
+ total_sleep_time += (
620
+ sleep_time # Track sleep to exclude from timeout
621
+ )
619
622
  continue
620
623
  else:
621
624
  # All retries exhausted
@@ -673,9 +676,14 @@ class DeviceConnection:
673
676
  base_timeout = timeout / total_weight
674
677
 
675
678
  last_error: Exception | None = None
679
+ total_sleep_time = 0.0 # Track sleep time to exclude from timeout budget
680
+ overall_start = time.monotonic()
676
681
 
677
682
  for attempt in range(max_retries + 1):
678
- current_timeout = base_timeout * (2**attempt)
683
+ # Calculate timeout with budget remaining after excluding sleep time
684
+ elapsed_response_time = time.monotonic() - overall_start - total_sleep_time
685
+ ideal_timeout = base_timeout * (2**attempt)
686
+ current_timeout = min(ideal_timeout, timeout - elapsed_response_time)
679
687
  sequence = attempt
680
688
 
681
689
  # Correlation key: (source, sequence, serial)
@@ -737,6 +745,9 @@ class DeviceConnection:
737
745
  # Sleep with jitter before retry
738
746
  sleep_time = self._calculate_retry_sleep_with_jitter(attempt)
739
747
  await asyncio.sleep(sleep_time)
748
+ total_sleep_time += (
749
+ sleep_time # Track sleep to exclude from timeout
750
+ )
740
751
  continue
741
752
  else:
742
753
  break
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lifx-async
3
- Version: 4.3.3
3
+ Version: 4.3.5
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,11 +5,11 @@ 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=bxnFRkYWmOfih9wDALKpiepOzlEabtuNS21tZhPVk6U,40765
8
+ lifx/devices/base.py,sha256=uD3hQe2kjycRZneSptON6psOhoEgPRHVelCoLgdHbFw,41482
9
9
  lifx/devices/hev.py,sha256=2zZNYm3TFrL755B4cRPNdYtcDLZEQwGl_22112WsSZc,9504
10
10
  lifx/devices/infrared.py,sha256=TrCgJyEioIPlFumMmcSmuGYmRsSGlQ5Rllg6_9Wtg4Y,4248
11
11
  lifx/devices/light.py,sha256=9rL24fa44Y7QrRBDSQuG6xpWsaPbQTTm4ExvrDnYWHo,27572
12
- lifx/devices/matrix.py,sha256=zj_AE2C0WfSjMZw8X7Y6KkG1PUr0ViLC7zvVrVnWMPI,32504
12
+ lifx/devices/matrix.py,sha256=mleYYsXkvvfXxV_pScb_D7VoX0AEJkClpzpNXOzQyGs,32643
13
13
  lifx/devices/multizone.py,sha256=c-lXcp8c1Mhs8me6smGkqQFrOOxdoGjWrOO5HnAVooY,27209
14
14
  lifx/effects/__init__.py,sha256=4DF31yp7RJic5JoltMlz5dCtF5KQobU6NOUtLUKkVKE,1509
15
15
  lifx/effects/base.py,sha256=YO0Hbg2VYHKPtfYnWxmrtzYoPGOi9BUXhn8HVFKv5IM,10283
@@ -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=dgkTptyFf_3Cfd2_UCh_M75lVmIHTdGuU92nzqQcsdY,37358
23
+ lifx/network/connection.py,sha256=K5zzwYWKBvSBMqcP6a5Fp9JkVIiOLXJSAi1XMT4g-Go,38191
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.3.dist-info/METADATA,sha256=gI8tKKKOoM36x6oeh3w5d8YBgDxeUIXlwTybiRRr7XE,2609
44
- lifx_async-4.3.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
45
- lifx_async-4.3.3.dist-info/licenses/LICENSE,sha256=eBz48GRA3gSiWn3rYZAz2Ewp35snnhV9cSqkVBq7g3k,1832
46
- lifx_async-4.3.3.dist-info/RECORD,,
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,,