lifx-async 4.7.3__py3-none-any.whl → 4.7.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/ceiling.py CHANGED
@@ -201,9 +201,15 @@ class CeilingLight(MatrixLight):
201
201
  """
202
202
  matrix_state = await super()._initialize_state()
203
203
 
204
- # Get ceiling component colors
205
- uplight_color = await self.get_uplight_color()
206
- downlight_colors = await self.get_downlight_colors()
204
+ # Extract ceiling component colors from already-fetched tile_colors
205
+ # (parent _initialize_state already called get_all_tile_colors)
206
+ tile_colors = matrix_state.tile_colors
207
+ uplight_color = tile_colors[self.uplight_zone]
208
+ downlight_colors = list(tile_colors[self.downlight_zones])
209
+
210
+ # Cache for is_on properties
211
+ self._last_uplight_color = uplight_color
212
+ self._last_downlight_colors = downlight_colors
207
213
 
208
214
  # Create ceiling state from matrix state
209
215
  ceiling_state = CeilingLightState.from_matrix_state(
@@ -231,9 +237,15 @@ class CeilingLight(MatrixLight):
231
237
  """
232
238
  await super().refresh_state()
233
239
 
234
- # Get ceiling component colors
235
- uplight_color = await self.get_uplight_color()
236
- downlight_colors = await self.get_downlight_colors()
240
+ # Extract ceiling component colors from already-fetched tile_colors
241
+ # (parent refresh_state already called get_all_tile_colors)
242
+ tile_colors = self._state.tile_colors
243
+ uplight_color = tile_colors[self.uplight_zone]
244
+ downlight_colors = list(tile_colors[self.downlight_zones])
245
+
246
+ # Cache for is_on properties
247
+ self._last_uplight_color = uplight_color
248
+ self._last_downlight_colors = downlight_colors
237
249
 
238
250
  # Update ceiling-specific state fields
239
251
  state = cast(CeilingLightState, self._state)
@@ -335,6 +347,22 @@ class CeilingLight(MatrixLight):
335
347
 
336
348
  return layout.downlight_zones
337
349
 
350
+ @property
351
+ def downlight_zone_count(self) -> int:
352
+ """Number of downlight zones.
353
+
354
+ Returns:
355
+ Zone count (63 for standard 8x8, 127 for Capsule 16x8)
356
+
357
+ Raises:
358
+ LifxError: If device version is not available or not a Ceiling product
359
+ """
360
+ # downlight_zones is slice(0, N), so stop equals the count
361
+ stop = self.downlight_zones.stop
362
+ if stop is None:
363
+ raise LifxError("Invalid downlight zones configuration")
364
+ return stop
365
+
338
366
  @property
339
367
  def uplight_is_on(self) -> bool:
340
368
  """True if uplight component is currently on.
@@ -484,7 +512,7 @@ class CeilingLight(MatrixLight):
484
512
  "Cannot set downlight color with brightness=0. "
485
513
  "Use turn_downlight_off() instead."
486
514
  )
487
- downlight_colors = [colors] * len(range(*self.downlight_zones.indices(256)))
515
+ downlight_colors = [colors] * self.downlight_zone_count
488
516
  else:
489
517
  if all(c.brightness == 0 for c in colors):
490
518
  raise ValueError(
@@ -492,10 +520,10 @@ class CeilingLight(MatrixLight):
492
520
  "Use turn_downlight_off() instead."
493
521
  )
494
522
 
495
- expected_count = len(range(*self.downlight_zones.indices(256)))
496
- if len(colors) != expected_count:
523
+ if len(colors) != self.downlight_zone_count:
497
524
  raise ValueError(
498
- f"Expected {expected_count} colors for downlight, got {len(colors)}"
525
+ f"Expected {self.downlight_zone_count} colors for downlight, "
526
+ f"got {len(colors)}"
499
527
  )
500
528
  downlight_colors = colors
501
529
 
@@ -522,6 +550,10 @@ class CeilingLight(MatrixLight):
522
550
  ) -> None:
523
551
  """Turn uplight component on.
524
552
 
553
+ If the entire light is off, this will set the color instantly and then
554
+ turn on the light with the specified duration, so the light fades to
555
+ the target color instead of flashing to its previous state.
556
+
525
557
  Args:
526
558
  color: Optional HSBK color. If provided:
527
559
  - Uses this color immediately
@@ -533,14 +565,61 @@ class CeilingLight(MatrixLight):
533
565
  ValueError: If color.brightness == 0
534
566
  LifxTimeoutError: Device did not respond
535
567
  """
536
- if color is not None:
537
- if color.brightness == 0:
538
- raise ValueError("Cannot turn on uplight with brightness=0")
539
- await self.set_uplight_color(color, duration)
568
+ # Validate provided color early
569
+ if color is not None and color.brightness == 0:
570
+ raise ValueError("Cannot turn on uplight with brightness=0")
571
+
572
+ # Check if light is off first to determine which path to take
573
+ if await self.get_power() == 0:
574
+ # Light is off - single fetch for both determining color and modification
575
+ all_colors = await self.get_all_tile_colors()
576
+ tile_colors = all_colors[0]
577
+
578
+ # Determine target color (pass pre-fetched colors to avoid extra fetch)
579
+ if color is not None:
580
+ target_color = color
581
+ else:
582
+ target_color = await self._determine_uplight_brightness(tile_colors)
583
+
584
+ # Store current downlight colors BEFORE zeroing them out
585
+ # This allows turn_downlight_on() to restore them later
586
+ downlight_colors = tile_colors[self.downlight_zones]
587
+ self._stored_downlight_state = list(downlight_colors)
588
+
589
+ # Set uplight zone to target color
590
+ tile_colors[self.uplight_zone] = target_color
591
+
592
+ # Zero out downlight zones so they stay off when power turns on
593
+ for i in range(*self.downlight_zones.indices(len(tile_colors))):
594
+ tile_colors[i] = HSBK(
595
+ hue=tile_colors[i].hue,
596
+ saturation=tile_colors[i].saturation,
597
+ brightness=0.0,
598
+ kelvin=tile_colors[i].kelvin,
599
+ )
600
+
601
+ # Set all colors instantly (duration=0) while light is off
602
+ await self.set_matrix_colors(0, tile_colors, duration=0)
603
+
604
+ # Update stored state for uplight
605
+ self._stored_uplight_state = target_color
606
+ self._last_uplight_color = target_color
607
+
608
+ # Turn on with the requested duration - light fades on to target color
609
+ await super().set_power(True, duration)
610
+
611
+ # Persist AFTER device operations complete
612
+ if self._state_file:
613
+ self._save_state_to_file()
540
614
  else:
541
- # Determine color using priority logic
542
- determined_color = await self._determine_uplight_brightness()
543
- await self.set_uplight_color(determined_color, duration)
615
+ # Light is already on - determine target color first, then set
616
+ if color is not None:
617
+ target_color = color
618
+ else:
619
+ target_color = await self._determine_uplight_brightness()
620
+
621
+ # set_uplight_color will fetch and modify (single fetch in that method)
622
+ await self.set_uplight_color(target_color, duration)
544
623
 
545
624
  async def turn_uplight_off(
546
625
  self, color: HSBK | None = None, duration: float = 0.0
@@ -560,30 +639,35 @@ class CeilingLight(MatrixLight):
560
639
  Note:
561
640
  Sets uplight zone brightness to 0 on device while preserving H, S, K.
562
641
  """
642
+ if color is not None and color.brightness == 0:
643
+ raise ValueError(
644
+ "Provided color cannot have brightness=0. "
645
+ "Omit the parameter to use current color."
646
+ )
647
+
648
+ # Fetch current state once and reuse to calculate brightness
649
+ all_colors = await self.get_all_tile_colors()
650
+ tile_colors = all_colors[0]
651
+
652
+ # Determine which color to store
563
653
  if color is not None:
564
- if color.brightness == 0:
565
- raise ValueError(
566
- "Provided color cannot have brightness=0. "
567
- "Omit the parameter to use current color."
568
- )
569
- # Store the provided color
570
- self._stored_uplight_state = color
654
+ stored_color = color
571
655
  else:
572
- # Get and store current color
573
- current_color = await self.get_uplight_color()
574
- self._stored_uplight_state = current_color
656
+ stored_color = tile_colors[self.uplight_zone]
657
+ self._last_uplight_color = stored_color
658
+
659
+ # Store for future restoration
660
+ self._stored_uplight_state = stored_color
575
661
 
576
662
  # Create color with brightness=0 for device
577
663
  off_color = HSBK(
578
- hue=self._stored_uplight_state.hue,
579
- saturation=self._stored_uplight_state.saturation,
664
+ hue=stored_color.hue,
665
+ saturation=stored_color.saturation,
580
666
  brightness=0.0,
581
- kelvin=self._stored_uplight_state.kelvin,
667
+ kelvin=stored_color.kelvin,
582
668
  )
583
669
 
584
- # Get all colors and update uplight zone
585
- all_colors = await self.get_all_tile_colors()
586
- tile_colors = all_colors[0]
670
+ # Update uplight zone and send immediately
587
671
  tile_colors[self.uplight_zone] = off_color
588
672
  await self.set_matrix_colors(0, tile_colors, duration=int(duration * 1000))
589
673
 
@@ -599,6 +683,10 @@ class CeilingLight(MatrixLight):
599
683
  ) -> None:
600
684
  """Turn downlight component on.
601
685
 
686
+ If the entire light is off, this will set the colors instantly and then
687
+ turn on the light with the specified duration, so the light fades to
688
+ the target colors instead of flashing to its previous state.
689
+
602
690
  Args:
603
691
  colors: Optional colors. Can be:
604
692
  - None: uses brightness determination logic
@@ -612,12 +700,76 @@ class CeilingLight(MatrixLight):
612
700
  ValueError: If list length doesn't match downlight zone count
613
701
  LifxTimeoutError: Device did not respond
614
702
  """
703
+ # Validate provided colors early
615
704
  if colors is not None:
616
- await self.set_downlight_colors(colors, duration)
705
+ if isinstance(colors, HSBK):
706
+ if colors.brightness == 0:
707
+ raise ValueError("Cannot turn on downlight with brightness=0")
708
+ else:
709
+ if all(c.brightness == 0 for c in colors):
710
+ raise ValueError("Cannot turn on downlight with brightness=0")
711
+ if len(colors) != self.downlight_zone_count:
712
+ raise ValueError(
713
+ f"Expected {self.downlight_zone_count} colors for downlight, "
714
+ f"got {len(colors)}"
715
+ )
716
+
717
+ # Check if light is off first to determine which path to take
718
+ if await self.get_power() == 0:
719
+ # Light is off - single fetch for both determining colors and modification
720
+ all_colors = await self.get_all_tile_colors()
721
+ tile_colors = all_colors[0]
722
+
723
+ # Determine target colors (pass pre-fetched colors to avoid extra fetch)
724
+ if colors is not None:
725
+ if isinstance(colors, HSBK):
726
+ target_colors = [colors] * self.downlight_zone_count
727
+ else:
728
+ target_colors = list(colors)
729
+ else:
730
+ target_colors = await self._determine_downlight_brightness(tile_colors)
731
+
732
+ # Store current uplight color BEFORE zeroing it out
733
+ # This allows turn_uplight_on() to restore it later
734
+ self._stored_uplight_state = tile_colors[self.uplight_zone]
735
+
736
+ # Set downlight zones to target colors
737
+ tile_colors[self.downlight_zones] = target_colors
738
+
739
+ # Zero out uplight zone so it stays off when power turns on
740
+ uplight_color = tile_colors[self.uplight_zone]
741
+ tile_colors[self.uplight_zone] = HSBK(
742
+ hue=uplight_color.hue,
743
+ saturation=uplight_color.saturation,
744
+ brightness=0.0,
745
+ kelvin=uplight_color.kelvin,
746
+ )
747
+
748
+ # Set all colors instantly (duration=0) while light is off
749
+ await self.set_matrix_colors(0, tile_colors, duration=0)
750
+
751
+ # Update stored state for downlight
752
+ self._stored_downlight_state = target_colors
753
+ self._last_downlight_colors = target_colors
754
+
755
+ # Turn on with the requested duration - light fades on to target colors
756
+ await super().set_power(True, duration)
757
+
758
+ # Persist AFTER device operations complete
759
+ if self._state_file:
760
+ self._save_state_to_file()
617
761
  else:
618
- # Determine colors using priority logic
619
- determined_colors = await self._determine_downlight_brightness()
620
- await self.set_downlight_colors(determined_colors, duration)
762
+ # Light is already on - determine target colors first, then set
763
+ if colors is not None:
764
+ if isinstance(colors, HSBK):
765
+ target_colors = [colors] * self.downlight_zone_count
766
+ else:
767
+ target_colors = list(colors)
768
+ else:
769
+ target_colors = await self._determine_downlight_brightness()
770
+
771
+ # set_downlight_colors will fetch and modify (single fetch in that method)
772
+ await self.set_downlight_colors(target_colors, duration)
621
773
 
622
774
  async def set_power(self, level: bool | int, duration: float = 0.0) -> None:
623
775
  """Set light power state, capturing component colors before turning off.
@@ -663,21 +815,75 @@ class CeilingLight(MatrixLight):
663
815
  else:
664
816
  raise TypeError(f"Expected bool or int, got {type(level).__name__}")
665
817
 
666
- # If turning off, capture current colors for both components
818
+ # If turning off, capture current colors for both components with single fetch
667
819
  if turning_off:
668
- # Always capture colors - even if brightness is 0, the hue/sat/kelvin
669
- # are still useful for turn_on. Brightness will be determined at
670
- # turn-on time using the standard inference logic.
671
- self._stored_uplight_state = await self.get_uplight_color()
672
- self._stored_downlight_state = await self.get_downlight_colors()
820
+ # Single fetch to capture both uplight and downlight colors
821
+ all_colors = await self.get_all_tile_colors()
822
+ tile_colors = all_colors[0]
673
823
 
674
- # Persist if enabled
675
- if self._state_file:
676
- self._save_state_to_file()
824
+ # Extract and store both component colors
825
+ self._stored_uplight_state = tile_colors[self.uplight_zone]
826
+ self._stored_downlight_state = list(tile_colors[self.downlight_zones])
827
+
828
+ # Also update cache for is_on properties
829
+ self._last_uplight_color = self._stored_uplight_state
830
+ self._last_downlight_colors = self._stored_downlight_state
677
831
 
678
832
  # Call parent to perform actual power change
679
833
  await super().set_power(level, duration)
680
834
 
835
+ # Persist AFTER device operation completes
836
+ if turning_off and self._state_file:
837
+ self._save_state_to_file()
838
+
839
+ async def set_color(self, color: HSBK, duration: float = 0.0) -> None:
840
+ """Set light color, updating component state tracking.
841
+
842
+ Overrides Light.set_color() to track the color change in the ceiling
843
+ light's component state. When set_color() is called, all zones (uplight
844
+ and downlight) are set to the same color. This override ensures that
845
+ the cached component colors stay in sync so that subsequent component
846
+ control methods (like turn_uplight_on or turn_downlight_on) use the
847
+ correct color values.
848
+
849
+ Args:
850
+ color: HSBK color to set for the entire light
851
+ duration: Transition duration in seconds (default 0.0)
852
+
853
+ Raises:
854
+ LifxDeviceNotFoundError: If device is not connected
855
+ LifxTimeoutError: If device does not respond
856
+ LifxUnsupportedCommandError: If device doesn't support this command
857
+
858
+ Example:
859
+ ```python
860
+ from lifx.color import HSBK
861
+
862
+ # Set entire ceiling light to warm white
863
+ await ceiling.set_color(
864
+ HSBK(hue=0, saturation=0, brightness=1.0, kelvin=2700)
865
+ )
866
+
867
+ # Later component control will use this color
868
+ await ceiling.turn_uplight_off() # Uplight off
869
+ await ceiling.turn_uplight_on() # Restores to warm white
870
+ ```
871
+ """
872
+ # Call parent to perform actual color change
873
+ await super().set_color(color, duration)
874
+
875
+ # Update cached component colors - all zones now have the same color
876
+ self._last_uplight_color = color
877
+ self._last_downlight_colors = [color] * self.downlight_zone_count
878
+
879
+ # Also update stored state for restoration
880
+ self._stored_uplight_state = color
881
+ self._stored_downlight_state = [color] * self.downlight_zone_count
882
+
883
+ # Persist if enabled
884
+ if self._state_file:
885
+ self._save_state_to_file()
886
+
681
887
  async def turn_downlight_off(
682
888
  self, colors: HSBK | list[HSBK] | None = None, duration: float = 0.0
683
889
  ) -> None:
@@ -699,35 +905,40 @@ class CeilingLight(MatrixLight):
699
905
  Note:
700
906
  Sets all downlight zone brightness to 0 on device while preserving H, S, K.
701
907
  """
702
- expected_count = len(range(*self.downlight_zones.indices(256)))
703
-
908
+ # Validate provided colors early (before fetching)
909
+ stored_colors: list[HSBK] | None = None
704
910
  if colors is not None:
705
- # Validate and normalize provided colors
706
911
  if isinstance(colors, HSBK):
707
912
  if colors.brightness == 0:
708
913
  raise ValueError(
709
914
  "Provided color cannot have brightness=0. "
710
915
  "Omit the parameter to use current colors."
711
916
  )
712
- colors_to_store = [colors] * expected_count
917
+ stored_colors = [colors] * self.downlight_zone_count
713
918
  else:
714
919
  if all(c.brightness == 0 for c in colors):
715
920
  raise ValueError(
716
921
  "Provided colors cannot have brightness=0. "
717
922
  "Omit the parameter to use current colors."
718
923
  )
719
- if len(colors) != expected_count:
924
+ if len(colors) != self.downlight_zone_count:
720
925
  raise ValueError(
721
- f"Expected {expected_count} colors for downlight, "
926
+ f"Expected {self.downlight_zone_count} colors for downlight, "
722
927
  f"got {len(colors)}"
723
928
  )
724
- colors_to_store = colors
929
+ stored_colors = list(colors)
725
930
 
726
- self._stored_downlight_state = colors_to_store
727
- else:
728
- # Get and store current colors
729
- current_colors = await self.get_downlight_colors()
730
- self._stored_downlight_state = current_colors
931
+ # Fetch current state once and reuse to calculate brightness
932
+ all_colors = await self.get_all_tile_colors()
933
+ tile_colors = all_colors[0]
934
+
935
+ # If colors not provided, extract from fetched data
936
+ if stored_colors is None:
937
+ stored_colors = list(tile_colors[self.downlight_zones])
938
+ self._last_downlight_colors = stored_colors
939
+
940
+ # Store for future restoration
941
+ self._stored_downlight_state = stored_colors
731
942
 
732
943
  # Create colors with brightness=0 for device
733
944
  off_colors = [
@@ -737,12 +948,10 @@ class CeilingLight(MatrixLight):
737
948
  brightness=0.0,
738
949
  kelvin=c.kelvin,
739
950
  )
740
- for c in self._stored_downlight_state
951
+ for c in stored_colors
741
952
  ]
742
953
 
743
- # Get all colors and update downlight zones
744
- all_colors = await self.get_all_tile_colors()
745
- tile_colors = all_colors[0]
954
+ # Update downlight zones and send immediately
746
955
  tile_colors[self.downlight_zones] = off_colors
747
956
  await self.set_matrix_colors(0, tile_colors, duration=int(duration * 1000))
748
957
 
@@ -753,89 +962,122 @@ class CeilingLight(MatrixLight):
753
962
  if self._state_file:
754
963
  self._save_state_to_file()
755
964
 
756
- async def _determine_uplight_brightness(self) -> HSBK:
965
+ async def _determine_uplight_brightness(
966
+ self, tile_colors: list[HSBK] | None = None
967
+ ) -> HSBK:
757
968
  """Determine uplight brightness using priority logic.
758
969
 
759
970
  Priority order:
760
- 1. Stored state (if available)
761
- 2. Infer from downlight average brightness
971
+ 1. Stored state (if available AND brightness > 0)
972
+ 2. Infer from downlight average brightness (using stored H, S, K if available)
762
973
  3. Hardcoded default (0.8)
763
974
 
975
+ Args:
976
+ tile_colors: Optional pre-fetched tile colors to avoid redundant fetch.
977
+ If None, will fetch from device.
978
+
764
979
  Returns:
765
980
  HSBK color for uplight
766
981
  """
767
- # 1. Stored state
768
- if self._stored_uplight_state is not None:
982
+ # 1. Stored state (only if brightness > 0)
983
+ if (
984
+ self._stored_uplight_state is not None
985
+ and self._stored_uplight_state.brightness > 0
986
+ ):
769
987
  return self._stored_uplight_state
770
988
 
771
- # Get current uplight color for H, S, K
772
- current_uplight = await self.get_uplight_color()
989
+ # Get current colors (use pre-fetched if available)
990
+ if tile_colors is None:
991
+ all_colors = await self.get_all_tile_colors()
992
+ tile_colors = all_colors[0]
993
+
994
+ current_uplight = tile_colors[self.uplight_zone]
995
+ downlight_colors = tile_colors[self.downlight_zones]
996
+
997
+ # Cache for is_on properties
998
+ self._last_uplight_color = current_uplight
999
+ self._last_downlight_colors = list(downlight_colors)
1000
+
1001
+ # Determine which color source to use for H, S, K
1002
+ source_color = self._stored_uplight_state or current_uplight
773
1003
 
774
1004
  # 2. Infer from downlight average brightness
775
- try:
776
- downlight_colors = await self.get_downlight_colors()
777
- avg_brightness = sum(c.brightness for c in downlight_colors) / len(
778
- downlight_colors
779
- )
1005
+ avg_brightness = sum(c.brightness for c in downlight_colors) / len(
1006
+ downlight_colors
1007
+ )
780
1008
 
781
- # Only use inferred brightness if it's > 0
782
- # If all downlights are off (brightness=0), skip to default
783
- if avg_brightness > 0:
784
- return HSBK(
785
- hue=current_uplight.hue,
786
- saturation=current_uplight.saturation,
787
- brightness=avg_brightness,
788
- kelvin=current_uplight.kelvin,
789
- )
790
- except Exception: # nosec B110
791
- # If inference fails, fall through to default
792
- pass
1009
+ # Only use inferred brightness if it's > 0
1010
+ # If all downlights are off (brightness=0), skip to default
1011
+ if avg_brightness > 0:
1012
+ return HSBK(
1013
+ hue=source_color.hue,
1014
+ saturation=source_color.saturation,
1015
+ brightness=avg_brightness,
1016
+ kelvin=source_color.kelvin,
1017
+ )
793
1018
 
794
1019
  # 3. Hardcoded default (0.8)
795
1020
  return HSBK(
796
- hue=current_uplight.hue,
797
- saturation=current_uplight.saturation,
1021
+ hue=source_color.hue,
1022
+ saturation=source_color.saturation,
798
1023
  brightness=0.8,
799
- kelvin=current_uplight.kelvin,
1024
+ kelvin=source_color.kelvin,
800
1025
  )
801
1026
 
802
- async def _determine_downlight_brightness(self) -> list[HSBK]:
1027
+ async def _determine_downlight_brightness(
1028
+ self, tile_colors: list[HSBK] | None = None
1029
+ ) -> list[HSBK]:
803
1030
  """Determine downlight brightness using priority logic.
804
1031
 
805
1032
  Priority order:
806
- 1. Stored state (if available)
1033
+ 1. Stored state (if available AND any brightness > 0)
807
1034
  2. Infer from uplight brightness
808
1035
  3. Hardcoded default (0.8)
809
1036
 
1037
+ Args:
1038
+ tile_colors: Optional pre-fetched tile colors to avoid redundant fetch.
1039
+ If None, will fetch from device.
1040
+
810
1041
  Returns:
811
1042
  List of HSBK colors for downlight zones
812
1043
  """
813
- # 1. Stored state
1044
+ # 1. Stored state (only if any color has brightness > 0)
814
1045
  if self._stored_downlight_state is not None:
815
- return self._stored_downlight_state
1046
+ if any(c.brightness > 0 for c in self._stored_downlight_state):
1047
+ return self._stored_downlight_state
816
1048
 
817
- # Get current downlight colors for H, S, K
818
- current_downlight = await self.get_downlight_colors()
1049
+ # Get current colors (use pre-fetched if available)
1050
+ if tile_colors is None:
1051
+ all_colors = await self.get_all_tile_colors()
1052
+ tile_colors = all_colors[0]
819
1053
 
820
- # 2. Infer from uplight brightness
821
- try:
822
- uplight_color = await self.get_uplight_color()
1054
+ current_downlight = list(tile_colors[self.downlight_zones])
1055
+ uplight_color = tile_colors[self.uplight_zone]
823
1056
 
824
- # Only use inferred brightness if it's > 0
825
- # If uplight is off (brightness=0), skip to default
826
- if uplight_color.brightness > 0:
827
- return [
828
- HSBK(
829
- hue=c.hue,
830
- saturation=c.saturation,
831
- brightness=uplight_color.brightness,
832
- kelvin=c.kelvin,
833
- )
834
- for c in current_downlight
835
- ]
836
- except Exception: # nosec B110
837
- # If inference fails, fall through to default
838
- pass
1057
+ # Cache for is_on properties
1058
+ self._last_downlight_colors = current_downlight
1059
+ self._last_uplight_color = uplight_color
1060
+
1061
+ # Prefer stored H, S, K if available, otherwise use current
1062
+ source_colors: list[HSBK] = (
1063
+ self._stored_downlight_state
1064
+ if self._stored_downlight_state is not None
1065
+ else current_downlight
1066
+ )
1067
+
1068
+ # 2. Infer from uplight brightness
1069
+ # Only use inferred brightness if it's > 0
1070
+ # If uplight is off (brightness=0), skip to default
1071
+ if uplight_color.brightness > 0:
1072
+ return [
1073
+ HSBK(
1074
+ hue=c.hue,
1075
+ saturation=c.saturation,
1076
+ brightness=uplight_color.brightness,
1077
+ kelvin=c.kelvin,
1078
+ )
1079
+ for c in source_colors
1080
+ ]
839
1081
 
840
1082
  # 3. Hardcoded default (0.8)
841
1083
  return [
@@ -845,7 +1087,7 @@ class CeilingLight(MatrixLight):
845
1087
  brightness=0.8,
846
1088
  kelvin=c.kelvin,
847
1089
  )
848
- for c in current_downlight
1090
+ for c in source_colors
849
1091
  ]
850
1092
 
851
1093
  def _is_stored_state_valid(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lifx-async
3
- Version: 4.7.3
3
+ Version: 4.7.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>
@@ -6,7 +6,7 @@ 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=4b5QtO0EFWxIqN2lUYgM8uLjWyHI5hUcReiF9QCjCGw,1061
8
8
  lifx/devices/base.py,sha256=0G2PCJRNeIPkMCIw68x0ijn6gUIwh2jFlex8SN4Hs1Y,63530
9
- lifx/devices/ceiling.py,sha256=q1aVqnYA0C32-c2J6GaYriaqgam9pKkSvV8IVVsIOf0,35661
9
+ lifx/devices/ceiling.py,sha256=bLAurvqTNmhKMFUUJmLqn1vDFawapYju2i4G0pHOH_4,45790
10
10
  lifx/devices/hev.py,sha256=T5hvt2q_vdgPBvThx_-M7n5pZu9pL0y9Fs3Zz_KL0NM,15588
11
11
  lifx/devices/infrared.py,sha256=ePk9qxX_s-hv5gQMvio1Vv8FYiCd68HF0ySbWgSrvuU,8130
12
12
  lifx/devices/light.py,sha256=gk92lhViUWINGaxDWbs4qn8Stnn2fGCfRkC5Kk0Q-hI,34087
@@ -42,7 +42,7 @@ lifx/theme/canvas.py,sha256=4h7lgN8iu_OdchObGDgbxTqQLCb-FRKC-M-YCWef_i4,8048
42
42
  lifx/theme/generators.py,sha256=nq3Yvntq_h-eFHbmmow3LcAdA_hEbRRaP5mv9Bydrjk,6435
43
43
  lifx/theme/library.py,sha256=tKlKZNqJp8lRGDnilWyDm_Qr1vCRGGwuvWVS82anNpQ,21326
44
44
  lifx/theme/theme.py,sha256=qMEx_8E41C0Cc6f083XHiAXEglTv4YlXW0UFsG1rQKg,5521
45
- lifx_async-4.7.3.dist-info/METADATA,sha256=SruPlVJQdCTtWKsJHzQDYpgJooaDxgfP2KiVF1XyPZY,2609
46
- lifx_async-4.7.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
47
- lifx_async-4.7.3.dist-info/licenses/LICENSE,sha256=eBz48GRA3gSiWn3rYZAz2Ewp35snnhV9cSqkVBq7g3k,1832
48
- lifx_async-4.7.3.dist-info/RECORD,,
45
+ lifx_async-4.7.5.dist-info/METADATA,sha256=1SN5XqtWLHrF_JtLHyWGNSCCeoajcu32g0MWFC48VIM,2609
46
+ lifx_async-4.7.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
47
+ lifx_async-4.7.5.dist-info/licenses/LICENSE,sha256=eBz48GRA3gSiWn3rYZAz2Ewp35snnhV9cSqkVBq7g3k,1832
48
+ lifx_async-4.7.5.dist-info/RECORD,,