diff --git a/.coveragerc b/.coveragerc index 3bf6aa04d35..06a0f84e032 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1136,6 +1136,7 @@ omit = homeassistant/components/tuya/sensor.py homeassistant/components/tuya/siren.py homeassistant/components/tuya/switch.py + homeassistant/components/tuya/util.py homeassistant/components/tuya/vacuum.py homeassistant/components/twentemilieu/const.py homeassistant/components/twentemilieu/sensor.py diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index c5b2aac729d..d61c83b17ad 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -12,6 +12,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN, TUYA_HA_SIGNAL_UPDATE_ENTITY +from .util import remap_value _LOGGER = logging.getLogger(__name__) @@ -53,9 +54,7 @@ class IntegerTypeData: reverse: bool = False, ) -> float: """Remap a value from this range to a new range.""" - if reverse: - value = self.max - value + self.min - return ((value - self.min) / (self.max - self.min)) * (to_max - to_min) + to_min + return remap_value(value, self.min, self.max, to_min, to_max, reverse) def remap_value_from( self, @@ -65,11 +64,7 @@ class IntegerTypeData: reverse: bool = False, ) -> float: """Remap a value from its current range to this range.""" - if reverse: - value = from_max - value + from_min - return ((value - from_min) / (from_max - from_min)) * ( - self.max - self.min - ) + self.min + return remap_value(value, from_min, from_max, self.min, self.max, reverse) @classmethod def from_json(cls, data: str) -> IntegerTypeData: diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 2c6bb67fa16..276debe6b96 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -140,6 +140,12 @@ class DPCode(str, Enum): BRIGHT_VALUE_2 = "bright_value_2" BRIGHT_VALUE_3 = "bright_value_3" BRIGHT_VALUE_V2 = "bright_value_v2" + BRIGHTNESS_MAX_1 = "brightness_max_1" + BRIGHTNESS_MAX_2 = "brightness_max_2" + BRIGHTNESS_MAX_3 = "brightness_max_3" + BRIGHTNESS_MIN_1 = "brightness_min_1" + BRIGHTNESS_MIN_2 = "brightness_min_2" + BRIGHTNESS_MIN_3 = "brightness_min_3" C_F = "c_f" # Temperature unit switching CH2O_STATE = "ch2o_state" CH2O_VALUE = "ch2o_value" diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 308c0037d0b..6a194ed94b2 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -26,16 +26,19 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantTuyaData from .base import IntegerTypeData, TuyaEntity from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode, WorkMode +from .util import remap_value @dataclass class TuyaLightEntityDescription(LightEntityDescription): """Describe an Tuya light entity.""" - color_mode: DPCode | None = None + brightness_max: DPCode | None = None + brightness_min: DPCode | None = None brightness: DPCode | tuple[DPCode, ...] | None = None - color_temp: DPCode | tuple[DPCode, ...] | None = None color_data: DPCode | tuple[DPCode, ...] | None = None + color_mode: DPCode | None = None + color_temp: DPCode | tuple[DPCode, ...] | None = None LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { @@ -120,16 +123,22 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { key=DPCode.SWITCH_LED_1, name="Light", brightness=DPCode.BRIGHT_VALUE_1, + brightness_max=DPCode.BRIGHTNESS_MAX_1, + brightness_min=DPCode.BRIGHTNESS_MIN_1, ), TuyaLightEntityDescription( key=DPCode.SWITCH_LED_2, name="Light 2", brightness=DPCode.BRIGHT_VALUE_2, + brightness_max=DPCode.BRIGHTNESS_MAX_2, + brightness_min=DPCode.BRIGHTNESS_MIN_2, ), TuyaLightEntityDescription( key=DPCode.SWITCH_LED_3, name="Light 3", brightness=DPCode.BRIGHT_VALUE_3, + brightness_max=DPCode.BRIGHTNESS_MAX_3, + brightness_min=DPCode.BRIGHTNESS_MIN_3, ), ), # Dimmer @@ -276,6 +285,8 @@ class TuyaLightEntity(TuyaEntity, LightEntity): entity_description: TuyaLightEntityDescription _brightness_dpcode: DPCode | None = None + _brightness_max_type: IntegerTypeData | None = None + _brightness_min_type: IntegerTypeData | None = None _brightness_type: IntegerTypeData | None = None _color_data_dpcode: DPCode | None = None _color_data_type: ColorTypeData | None = None @@ -349,6 +360,20 @@ class TuyaLightEntity(TuyaEntity, LightEntity): device.status_range[self._brightness_dpcode].values ) + # Check if min/max capable + if ( + description.brightness_max is not None + and description.brightness_min is not None + and description.brightness_max in device.function + and description.brightness_min in device.function + ): + self._brightness_max_type = IntegerTypeData.from_json( + device.status_range[description.brightness_max].values + ) + self._brightness_min_type = IntegerTypeData.from_json( + device.status_range[description.brightness_min].values + ) + # Update internals based on found color temperature dpcode if self._color_temp_dpcode: self._attr_supported_color_modes.add(COLOR_MODE_COLOR_TEMP) @@ -456,12 +481,47 @@ class TuyaLightEntity(TuyaEntity, LightEntity): and self.color_mode != COLOR_MODE_HS and self._brightness_type ): + brightness = kwargs[ATTR_BRIGHTNESS] + + # If there is a min/max value, the brightness is actually limited. + # Meaning it is actually not on a 0-255 scale. + if ( + self._brightness_max_type is not None + and self._brightness_min_type is not None + and self.entity_description.brightness_max is not None + and self.entity_description.brightness_min is not None + and ( + brightness_max := self.device.status.get( + self.entity_description.brightness_max + ) + ) + is not None + and ( + brightness_min := self.device.status.get( + self.entity_description.brightness_min + ) + ) + is not None + ): + # Remap values onto our scale + brightness_max = self._brightness_max_type.remap_value_to( + brightness_max + ) + brightness_min = self._brightness_min_type.remap_value_to( + brightness_min + ) + + # Remap the brightness value from their min-max to our 0-255 scale + brightness = remap_value( + brightness, + to_min=brightness_min, + to_max=brightness_max, + ) + commands += [ { "code": self._brightness_dpcode, - "value": round( - self._brightness_type.remap_value_from(kwargs[ATTR_BRIGHTNESS]) - ), + "value": round(self._brightness_type.remap_value_from(brightness)), }, ] @@ -485,7 +545,41 @@ class TuyaLightEntity(TuyaEntity, LightEntity): if brightness is None: return None - return round(self._brightness_type.remap_value_to(brightness)) + # Remap value to our scale + brightness = self._brightness_type.remap_value_to(brightness) + + # If there is a min/max value, the brightness is actually limited. + # Meaning it is actually not on a 0-255 scale. + if ( + self._brightness_max_type is not None + and self._brightness_min_type is not None + and self.entity_description.brightness_max is not None + and self.entity_description.brightness_min is not None + and ( + brightness_max := self.device.status.get( + self.entity_description.brightness_max + ) + ) + is not None + and ( + brightness_min := self.device.status.get( + self.entity_description.brightness_min + ) + ) + is not None + ): + # Remap values onto our scale + brightness_max = self._brightness_max_type.remap_value_to(brightness_max) + brightness_min = self._brightness_min_type.remap_value_to(brightness_min) + + # Remap the brightness value from their min-max to our 0-255 scale + brightness = remap_value( + brightness, + from_min=brightness_min, + from_max=brightness_max, + ) + + return round(brightness) @property def color_temp(self) -> int | None: diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index 0c5aa288f29..922012412b7 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -78,6 +78,74 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_CONFIG, ), ), + # Dimmer Switch + # https://developer.tuya.com/en/docs/iot/categorytgkg?id=Kaiuz0ktx7m0o + "tgkg": ( + NumberEntityDescription( + key=DPCode.BRIGHTNESS_MIN_1, + name="Minimum Brightness", + icon="mdi:lightbulb-outline", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + NumberEntityDescription( + key=DPCode.BRIGHTNESS_MAX_1, + name="Maximum Brightness", + icon="mdi:lightbulb-on-outline", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + NumberEntityDescription( + key=DPCode.BRIGHTNESS_MIN_2, + name="Minimum Brightness 2", + icon="mdi:lightbulb-outline", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + NumberEntityDescription( + key=DPCode.BRIGHTNESS_MAX_2, + name="Maximum Brightness 2", + icon="mdi:lightbulb-on-outline", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + NumberEntityDescription( + key=DPCode.BRIGHTNESS_MIN_3, + name="Minimum Brightness 3", + icon="mdi:lightbulb-outline", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + NumberEntityDescription( + key=DPCode.BRIGHTNESS_MAX_3, + name="Maximum Brightness 3", + icon="mdi:lightbulb-on-outline", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), + # Dimmer Switch + # https://developer.tuya.com/en/docs/iot/categorytgkg?id=Kaiuz0ktx7m0o + "tgq": ( + NumberEntityDescription( + key=DPCode.BRIGHTNESS_MIN_1, + name="Minimum Brightness", + icon="mdi:lightbulb-outline", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + NumberEntityDescription( + key=DPCode.BRIGHTNESS_MAX_1, + name="Maximum Brightness", + icon="mdi:lightbulb-on-outline", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + NumberEntityDescription( + key=DPCode.BRIGHTNESS_MIN_2, + name="Minimum Brightness 2", + icon="mdi:lightbulb-outline", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + NumberEntityDescription( + key=DPCode.BRIGHTNESS_MAX_2, + name="Maximum Brightness 2", + icon="mdi:lightbulb-on-outline", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), # Vibration Sensor # https://developer.tuya.com/en/docs/iot/categoryzd?id=Kaiuz3a5vrzno "zd": ( diff --git a/homeassistant/components/tuya/util.py b/homeassistant/components/tuya/util.py new file mode 100644 index 00000000000..3b29a3e13cf --- /dev/null +++ b/homeassistant/components/tuya/util.py @@ -0,0 +1,16 @@ +"""Utility methods for the Tuya integration.""" +from __future__ import annotations + + +def remap_value( + value: float | int, + from_min: float | int = 0, + from_max: float | int = 255, + to_min: float | int = 0, + to_max: float | int = 255, + reverse: bool = False, +) -> float: + """Remap a value from its current range, to a new range.""" + if reverse: + value = from_max - value + from_min + return ((value - from_min) / (from_max - from_min)) * (to_max - to_min) + to_min