3232from .models import (
3333 DPCodeBooleanWrapper ,
3434 DPCodeEnumWrapper ,
35+ DPCodeIntegerWrapper ,
3536 IntegerTypeData ,
3637 find_dpcode ,
3738)
3839from .util import get_dpcode , get_dptype , remap_value
3940
4041
42+ class _BrightnessWrapper (DPCodeIntegerWrapper ):
43+ """Wrapper for brightness DP code.
44+
45+ Handles brightness value conversion between device scale and Home Assistant's
46+ 0-255 scale. Supports optional dynamic brightness_min and brightness_max
47+ wrappers that allow the device to specify runtime brightness range limits.
48+ """
49+
50+ brightness_min : DPCodeIntegerWrapper | None = None
51+ brightness_max : DPCodeIntegerWrapper | None = None
52+
53+ def read_device_status (self , device : CustomerDevice ) -> Any | None :
54+ """Return the brightness of this light between 0..255."""
55+ if (brightness := self ._read_device_status_raw (device )) is None :
56+ return None
57+
58+ # Remap value to our scale
59+ brightness = self .type_information .remap_value_to (brightness )
60+
61+ # If there is a min/max value, the brightness is actually limited.
62+ # Meaning it is actually not on a 0-255 scale.
63+ if (
64+ self .brightness_max is not None
65+ and self .brightness_min is not None
66+ and (brightness_max := device .status .get (self .brightness_max .dpcode ))
67+ is not None
68+ and (brightness_min := device .status .get (self .brightness_min .dpcode ))
69+ is not None
70+ ):
71+ # Remap values onto our scale
72+ brightness_max = self .brightness_max .type_information .remap_value_to (
73+ brightness_max
74+ )
75+ brightness_min = self .brightness_min .type_information .remap_value_to (
76+ brightness_min
77+ )
78+
79+ # Remap the brightness value from their min-max to our 0-255 scale
80+ brightness = remap_value (
81+ brightness , from_min = brightness_min , from_max = brightness_max
82+ )
83+
84+ return round (brightness )
85+
86+ def _convert_value_to_raw_value (self , device : CustomerDevice , value : Any ) -> Any :
87+ """Convert a Home Assistant value (0..255) back to a raw device value."""
88+ # If there is a min/max value, the brightness is actually limited.
89+ # Meaning it is actually not on a 0-255 scale.
90+ if (
91+ self .brightness_max is not None
92+ and self .brightness_min is not None
93+ and (brightness_max := device .status .get (self .brightness_max .dpcode ))
94+ is not None
95+ and (brightness_min := device .status .get (self .brightness_min .dpcode ))
96+ is not None
97+ ):
98+ # Remap values onto our scale
99+ brightness_max = self .brightness_max .type_information .remap_value_to (
100+ brightness_max
101+ )
102+ brightness_min = self .brightness_min .type_information .remap_value_to (
103+ brightness_min
104+ )
105+
106+ # Remap the brightness value from our 0-255 scale to their min-max
107+ value = remap_value (value , to_min = brightness_min , to_max = brightness_max )
108+ return round (self .type_information .remap_value_from (value ))
109+
110+
41111@dataclass
42112class ColorTypeData :
43113 """Color Type Data."""
@@ -417,6 +487,24 @@ def brightness(self) -> int:
417487 return round (self .type_data .v_type .remap_value_to (self .v_value , 0 , 255 ))
418488
419489
490+ def _get_brightness_wrapper (
491+ device : CustomerDevice , description : TuyaLightEntityDescription
492+ ) -> _BrightnessWrapper | None :
493+ if (
494+ brightness_wrapper := _BrightnessWrapper .find_dpcode (
495+ device , description .brightness , prefer_function = True
496+ )
497+ ) is None :
498+ return None
499+ brightness_wrapper .brightness_max = DPCodeIntegerWrapper .find_dpcode (
500+ device , description .brightness_max , prefer_function = True
501+ )
502+ brightness_wrapper .brightness_min = DPCodeIntegerWrapper .find_dpcode (
503+ device , description .brightness_min , prefer_function = True
504+ )
505+ return brightness_wrapper
506+
507+
420508async def async_setup_entry (
421509 hass : HomeAssistant ,
422510 entry : TuyaConfigEntry ,
@@ -437,6 +525,7 @@ def async_discover_device(device_ids: list[str]):
437525 device ,
438526 manager ,
439527 description ,
528+ brightness_wrapper = _get_brightness_wrapper (device , description ),
440529 color_mode_wrapper = DPCodeEnumWrapper .find_dpcode (
441530 device , description .color_mode , prefer_function = True
442531 ),
@@ -464,9 +553,6 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
464553
465554 entity_description : TuyaLightEntityDescription
466555
467- _brightness_max : IntegerTypeData | None = None
468- _brightness_min : IntegerTypeData | None = None
469- _brightness : IntegerTypeData | None = None
470556 _color_data_dpcode : DPCode | None = None
471557 _color_data_type : ColorTypeData | None = None
472558 _color_temp : IntegerTypeData | None = None
@@ -481,32 +567,22 @@ def __init__(
481567 device_manager : Manager ,
482568 description : TuyaLightEntityDescription ,
483569 * ,
570+ brightness_wrapper : DPCodeIntegerWrapper | None ,
484571 color_mode_wrapper : DPCodeEnumWrapper | None ,
485572 switch_wrapper : DPCodeBooleanWrapper ,
486573 ) -> None :
487574 """Init TuyaHaLight."""
488575 super ().__init__ (device , device_manager )
489576 self .entity_description = description
490577 self ._attr_unique_id = f"{ super ().unique_id } { description .key } "
578+ self ._brightness_wrapper = brightness_wrapper
491579 self ._color_mode_wrapper = color_mode_wrapper
492580 self ._switch_wrapper = switch_wrapper
493581
494582 color_modes : set [ColorMode ] = {ColorMode .ONOFF }
495583
496- if int_type := find_dpcode (
497- self .device ,
498- description .brightness ,
499- dptype = DPType .INTEGER ,
500- prefer_function = True ,
501- ):
502- self ._brightness = int_type
584+ if brightness_wrapper :
503585 color_modes .add (ColorMode .BRIGHTNESS )
504- self ._brightness_max = find_dpcode (
505- self .device , description .brightness_max , dptype = DPType .INTEGER
506- )
507- self ._brightness_min = find_dpcode (
508- self .device , description .brightness_min , dptype = DPType .INTEGER
509- )
510586
511587 if (dpcode := get_dpcode (self .device , description .color_data )) and (
512588 get_dptype (self .device , dpcode , prefer_function = True ) == DPType .JSON
@@ -529,7 +605,8 @@ def __init__(
529605 # If no type is found, use a default one
530606 self ._color_data_type = self .entity_description .default_color_type
531607 if self ._color_data_dpcode == DPCode .COLOUR_DATA_V2 or (
532- self ._brightness and self ._brightness .max > 255
608+ self ._brightness_wrapper
609+ and self ._brightness_wrapper .type_information .max > 255
533610 ):
534611 self ._color_data_type = DEFAULT_COLOR_TYPE_DATA_V2
535612
@@ -641,46 +718,16 @@ def turn_on(self, **kwargs: Any) -> None:
641718 },
642719 ]
643720
644- elif self ._brightness and (ATTR_BRIGHTNESS in kwargs or ATTR_WHITE in kwargs ):
721+ elif self ._brightness_wrapper and (
722+ ATTR_BRIGHTNESS in kwargs or ATTR_WHITE in kwargs
723+ ):
645724 if ATTR_BRIGHTNESS in kwargs :
646725 brightness = kwargs [ATTR_BRIGHTNESS ]
647726 else :
648727 brightness = kwargs [ATTR_WHITE ]
649728
650- # If there is a min/max value, the brightness is actually limited.
651- # Meaning it is actually not on a 0-255 scale.
652- if (
653- self ._brightness_max is not None
654- and self ._brightness_min is not None
655- and (
656- brightness_max := self .device .status .get (
657- self ._brightness_max .dpcode
658- )
659- )
660- is not None
661- and (
662- brightness_min := self .device .status .get (
663- self ._brightness_min .dpcode
664- )
665- )
666- is not None
667- ):
668- # Remap values onto our scale
669- brightness_max = self ._brightness_max .remap_value_to (brightness_max )
670- brightness_min = self ._brightness_min .remap_value_to (brightness_min )
671-
672- # Remap the brightness value from their min-max to our 0-255 scale
673- brightness = remap_value (
674- brightness ,
675- to_min = brightness_min ,
676- to_max = brightness_max ,
677- )
678-
679729 commands += [
680- {
681- "code" : self ._brightness .dpcode ,
682- "value" : round (self ._brightness .remap_value_from (brightness )),
683- },
730+ self ._brightness_wrapper .get_update_command (self .device , brightness ),
684731 ]
685732
686733 self ._send_command (commands )
@@ -691,43 +738,12 @@ async def async_turn_off(self, **kwargs: Any) -> None:
691738
692739 @property
693740 def brightness (self ) -> int | None :
694- """Return the brightness of the light."""
741+ """Return the brightness of this light between 0..255 ."""
695742 # If the light is currently in color mode, extract the brightness from the color data
696743 if self .color_mode == ColorMode .HS and (color_data := self ._get_color_data ()):
697744 return color_data .brightness
698745
699- if not self ._brightness :
700- return None
701-
702- brightness = self .device .status .get (self ._brightness .dpcode )
703- if brightness is None :
704- return None
705-
706- # Remap value to our scale
707- brightness = self ._brightness .remap_value_to (brightness )
708-
709- # If there is a min/max value, the brightness is actually limited.
710- # Meaning it is actually not on a 0-255 scale.
711- if (
712- self ._brightness_max is not None
713- and self ._brightness_min is not None
714- and (brightness_max := self .device .status .get (self ._brightness_max .dpcode ))
715- is not None
716- and (brightness_min := self .device .status .get (self ._brightness_min .dpcode ))
717- is not None
718- ):
719- # Remap values onto our scale
720- brightness_max = self ._brightness_max .remap_value_to (brightness_max )
721- brightness_min = self ._brightness_min .remap_value_to (brightness_min )
722-
723- # Remap the brightness value from their min-max to our 0-255 scale
724- brightness = remap_value (
725- brightness ,
726- from_min = brightness_min ,
727- from_max = brightness_max ,
728- )
729-
730- return round (brightness )
746+ return self ._read_wrapper (self ._brightness_wrapper )
731747
732748 @property
733749 def color_temp_kelvin (self ) -> int | None :
0 commit comments