diff --git a/custom_components/localtuya/climate.py b/custom_components/localtuya/climate.py index 55bd303e7..f56f07680 100644 --- a/custom_components/localtuya/climate.py +++ b/custom_components/localtuya/climate.py @@ -73,8 +73,8 @@ HVACMode.AUTO: "Auto", }, "MANUAL/AUTO": { - HVAC_MODE_HEAT: "MANUAL", - HVAC_MODE_AUTO: "AUTO", + HVACMode.HEAT: "MANUAL", + HVACMode.AUTO: "AUTO", }, "Manual/Program": { HVACMode.HEAT: "Manual", @@ -117,8 +117,8 @@ HVACAction.IDLE: "Warming", }, "heating/warming": { - CURRENT_HVAC_HEAT: "heating", - CURRENT_HVAC_IDLE: "warming", + HVACAction.HEATING: "heating", + HVACAction.IDLE: "warming", }, } HVAC_FAN_MODE_SETS = { diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index 75f7bbd2a..6e7556b11 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -53,6 +53,7 @@ CONF_BRIGHTNESS_UPPER = "brightness_upper" CONF_COLOR = "color" CONF_COLOR_MODE = "color_mode" +CONF_COLOR_MODE_SET = "color_mode_set" CONF_COLOR_TEMP_MIN_KELVIN = "color_temp_min_kelvin" CONF_COLOR_TEMP_MAX_KELVIN = "color_temp_max_kelvin" CONF_COLOR_TEMP_REVERSE = "color_temp_reverse" diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 5e1a3c9c9..59c33acf0 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -9,7 +9,8 @@ DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, - FanEntity, FanEntityFeature, + FanEntityFeature, + FanEntity, ) from homeassistant.util.percentage import ( int_states_in_range, @@ -186,9 +187,9 @@ async def async_set_direction(self, direction): self.schedule_update_ha_state() @property - def supported_features(self) -> int: + def supported_features(self) -> FanEntityFeature: """Flag supported features.""" - features = 0 + features = FanEntityFeature(0) if self.has_config(CONF_FAN_OSCILLATING_CONTROL): features |= FanEntityFeature.OSCILLATE @@ -199,6 +200,9 @@ def supported_features(self) -> int: if self.has_config(CONF_FAN_DIRECTION): features |= FanEntityFeature.DIRECTION + features |= FanEntityFeature.TURN_OFF + features |= FanEntityFeature.TURN_ON + return features @property diff --git a/custom_components/localtuya/light.py b/custom_components/localtuya/light.py index 7c74e49f5..66773c216 100644 --- a/custom_components/localtuya/light.py +++ b/custom_components/localtuya/light.py @@ -1,21 +1,19 @@ """Platform to locally control Tuya-based light devices.""" import logging import textwrap +from dataclasses import dataclass from functools import partial import homeassistant.util.color as color_util import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, DOMAIN, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, - SUPPORT_EFFECT, LightEntity, + LightEntityFeature, + ColorMode, ) from homeassistant.const import CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_SCENE @@ -28,7 +26,7 @@ CONF_COLOR_TEMP_MAX_KELVIN, CONF_COLOR_TEMP_MIN_KELVIN, CONF_COLOR_TEMP_REVERSE, - CONF_MUSIC_MODE, + CONF_MUSIC_MODE, CONF_COLOR_MODE_SET, ) _LOGGER = logging.getLogger(__name__) @@ -41,6 +39,7 @@ DEFAULT_LOWER_BRIGHTNESS = 29 DEFAULT_UPPER_BRIGHTNESS = 1000 +MODE_MANUAL = "manual" MODE_COLOR = "colour" MODE_MUSIC = "music" MODE_SCENE = "scene" @@ -49,6 +48,8 @@ SCENE_CUSTOM = "Custom" SCENE_MUSIC = "Music" +MODES_SET = {"Colour, Music, Scene and White": 0, "Manual, Music, Scene and White": 1} + SCENE_LIST_RGBW_1000 = { "Night": "000e0d0000000000000000c80000", "Read": "010e0d0000000000000003e801f4", @@ -91,6 +92,22 @@ + "0000000", } +@dataclass(frozen=True) +class Mode: + color: str = MODE_COLOR + music: str = MODE_MUSIC + scene: str = MODE_SCENE + white: str = MODE_WHITE + + def as_list(self) -> list: + return [self.color, self.music, self.scene, self.white] + + def as_dict(self) -> dict[str, str]: + default = {"Default": self.white} + return {**default, "Mode Color": self.color, "Mode Scene": self.scene} + +MAP_MODE_SET = {0: Mode(), 1: Mode(color=MODE_MANUAL)} + def map_range(value, from_lower, from_upper, to_lower, to_upper): """Map a value in one range to another.""" @@ -162,10 +179,12 @@ def __init__( self._color_temp_reverse = self._config.get( CONF_COLOR_TEMP_REVERSE, DEFAULT_COLOR_TEMP_REVERSE ) + self._modes = MAP_MODE_SET[int(self._config.get(CONF_COLOR_MODE_SET, 0))] self._hs = None self._effect = None self._effect_list = [] - self._scenes = None + self._scenes = {} + if self.has_config(CONF_SCENE): if self._config.get(CONF_SCENE) < 20: self._scenes = SCENE_LIST_RGBW_255 @@ -174,6 +193,7 @@ def __init__( else: self._scenes = SCENE_LIST_RGBW_1000 self._effect_list = list(self._scenes.keys()) + if self._config.get(CONF_MUSIC_MODE): self._effect_list.append(SCENE_MUSIC) @@ -197,8 +217,8 @@ def hs_color(self): if self.is_color_mode: return self._hs if ( - self.supported_features & SUPPORT_COLOR - and not self.supported_features & SUPPORT_COLOR_TEMP + ColorMode.HS in self.supported_color_modes + and not ColorMode.COLOR_TEMP in self.supported_color_modes ): return [0, 0] return None @@ -241,45 +261,76 @@ def effect(self): @property def effect_list(self): """Return the list of supported effects for this light.""" - return self._effect_list + if self.is_scene_mode or self.is_music_mode: + return self._effect + elif (color_mode := self.__get_color_mode()) in self._scenes.values(): + return self.__find_scene_by_scene_data(color_mode) + return None @property - def supported_features(self): - """Flag supported features.""" - supports = 0 - if self.has_config(CONF_BRIGHTNESS): - supports |= SUPPORT_BRIGHTNESS + def supported_color_modes(self) -> set[ColorMode] | set[str] | None: + """Flag supported color modes.""" + color_modes: set[ColorMode] = set() + if self.has_config(CONF_COLOR_TEMP): - supports |= SUPPORT_COLOR_TEMP + color_modes.add(ColorMode.COLOR_TEMP) if self.has_config(CONF_COLOR): - supports |= SUPPORT_COLOR | SUPPORT_BRIGHTNESS + color_modes.add(ColorMode.HS) + + if not color_modes and self.has_config(CONF_BRIGHTNESS): + return {ColorMode.BRIGHTNESS} + + if not color_modes: + return {ColorMode.ONOFF} + + return color_modes + + @property + def supported_features(self) -> LightEntityFeature: + """Flag supported features.""" + supports = LightEntityFeature(0) if self.has_config(CONF_SCENE) or self.has_config(CONF_MUSIC_MODE): - supports |= SUPPORT_EFFECT + supports |= LightEntityFeature.EFFECT return supports + @property + def color_mode(self) -> ColorMode: + """Return the color_mode of the light.""" + if len(self.supported_color_modes) == 1: + return next(iter(self.supported_color_modes)) + + if self.is_color_mode: + return ColorMode.HS + if self.is_white_mode: + return ColorMode.COLOR_TEMP + if self._brightness: + return ColorMode.BRIGHTNESS + + return ColorMode.ONOFF + @property def is_white_mode(self): """Return true if the light is in white mode.""" color_mode = self.__get_color_mode() - return color_mode is None or color_mode == MODE_WHITE + return color_mode is None or color_mode == self._modes.white @property def is_color_mode(self): """Return true if the light is in color mode.""" color_mode = self.__get_color_mode() - return color_mode is not None and color_mode == MODE_COLOR + return color_mode is not None and color_mode == self._modes.color @property def is_scene_mode(self): """Return true if the light is in scene mode.""" color_mode = self.__get_color_mode() - return color_mode is not None and color_mode.startswith(MODE_SCENE) + return color_mode is not None and color_mode.startswith(self._modes.scene) @property def is_music_mode(self): """Return true if the light is in music mode.""" color_mode = self.__get_color_mode() - return color_mode is not None and color_mode == MODE_MUSIC + return color_mode is not None and color_mode == self._modes.music def __is_color_rgb_encoded(self): return len(self.dps_conf(CONF_COLOR)) > 12 @@ -294,7 +345,7 @@ def __get_color_mode(self): return ( self.dps_conf(CONF_COLOR_MODE) if self.has_config(CONF_COLOR_MODE) - else MODE_WHITE + else self._modes.white ) async def async_turn_on(self, **kwargs): @@ -304,7 +355,7 @@ async def async_turn_on(self, **kwargs): states[self._dp_id] = True features = self.supported_features brightness = None - if ATTR_EFFECT in kwargs and (features & SUPPORT_EFFECT): + if ATTR_EFFECT in kwargs and (features & LightEntityFeature.EFFECT): scene = self._scenes.get(kwargs[ATTR_EFFECT]) if scene is not None: if scene.startswith(MODE_SCENE): @@ -315,7 +366,11 @@ async def async_turn_on(self, **kwargs): elif kwargs[ATTR_EFFECT] == SCENE_MUSIC: states[self._config.get(CONF_COLOR_MODE)] = MODE_MUSIC - if ATTR_BRIGHTNESS in kwargs and (features & SUPPORT_BRIGHTNESS): + if ATTR_BRIGHTNESS in kwargs and ( + ColorMode.BRIGHTNESS in self.supported_color_modes + or self.has_config(CONF_BRIGHTNESS) + or self.has_config(CONF_COLOR) + ): brightness = map_range( int(kwargs[ATTR_BRIGHTNESS]), 0, @@ -347,7 +402,7 @@ async def async_turn_on(self, **kwargs): states[self._config.get(CONF_COLOR)] = color states[self._config.get(CONF_COLOR_MODE)] = MODE_COLOR - if ATTR_HS_COLOR in kwargs and (features & SUPPORT_COLOR): + if ATTR_HS_COLOR in kwargs and ColorMode.HS in self.supported_color_modes: if brightness is None: brightness = self._brightness hs = kwargs[ATTR_HS_COLOR] @@ -374,10 +429,10 @@ async def async_turn_on(self, **kwargs): states[self._config.get(CONF_COLOR)] = color states[self._config.get(CONF_COLOR_MODE)] = MODE_COLOR - if ATTR_COLOR_TEMP in kwargs and (features & SUPPORT_COLOR_TEMP): + if ColorMode.COLOR_TEMP in kwargs and ColorMode.COLOR_TEMP in self.supported_color_modes: if brightness is None: brightness = self._brightness - mired = int(kwargs[ATTR_COLOR_TEMP]) + mired = int(kwargs[ColorMode.COLOR_TEMP]) if self._color_temp_reverse: mired = self._max_mired - (mired - self._min_mired) if mired < self._min_mired: @@ -403,10 +458,14 @@ def status_updated(self): self._state = self.dps(self._dp_id) supported = self.supported_features self._effect = None - if supported & SUPPORT_BRIGHTNESS and self.has_config(CONF_BRIGHTNESS): + + if (ColorMode.BRIGHTNESS in self.supported_color_modes + or self.has_config(CONF_BRIGHTNESS) + or self.has_config(CONF_COLOR) + ): self._brightness = self.dps_conf(CONF_BRIGHTNESS) - if supported & SUPPORT_COLOR: + if ColorMode.HS in self.supported_color_modes: color = self.dps_conf(CONF_COLOR) if color is not None and not self.is_white_mode: if self.__is_color_rgb_encoded(): @@ -422,10 +481,10 @@ def status_updated(self): self._hs = [hue, sat / 10.0] self._brightness = value - if supported & SUPPORT_COLOR_TEMP: + if ColorMode.COLOR_TEMP in self.supported_color_modes: self._color_temp = self.dps_conf(CONF_COLOR_TEMP) - if self.is_scene_mode and supported & SUPPORT_EFFECT: + if self.is_scene_mode and supported & LightEntityFeature.EFFECT: if self.dps_conf(CONF_COLOR_MODE) != MODE_SCENE: self._effect = self.__find_scene_by_scene_data( self.dps_conf(CONF_COLOR_MODE) @@ -440,7 +499,7 @@ def status_updated(self): elif SCENE_CUSTOM in self._effect_list: self._effect_list.remove(SCENE_CUSTOM) - if self.is_music_mode and supported & SUPPORT_EFFECT: + if self.is_music_mode and supported & LightEntityFeature.EFFECT: self._effect = SCENE_MUSIC