From 87d61eea6f4189bf1dc01614534e1fff84db4a7f Mon Sep 17 00:00:00 2001 From: Esteban Zapata Rojas <2417465+estebanz01@users.noreply.github.com> Date: Fri, 5 Jul 2024 15:58:39 -0500 Subject: [PATCH 01/16] at init: Replace deprecated async entry setup call with suggestion. Based on https://developers.home-assistant.io/blog/2024/06/12/async_forward_entry_setups/ it is possible to replace it with `async_forward_entry_setups` instead, which accepts multiple platforms and it should be more efficient. --- custom_components/localtuya/__init__.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/custom_components/localtuya/__init__.py b/custom_components/localtuya/__init__.py index b126c2f..35a2adb 100644 --- a/custom_components/localtuya/__init__.py +++ b/custom_components/localtuya/__init__.py @@ -139,7 +139,7 @@ async def async_setup(hass: HomeAssistant, config: dict): ) new_data[ATTR_UPDATED_AT] = str(int(time.time() * 1000)) hass.config_entries.async_update_entry(entry, data=new_data) - + elif device_id in hass.data[DOMAIN][TUYA_DEVICES]: _LOGGER.debug("Device %s found with IP %s", device_id, device_ip) @@ -270,12 +270,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) hass.data[DOMAIN][TUYA_DEVICES][dev_id] = TuyaDevice(hass, entry, dev_id) - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in platforms - ] - ) + # Setup all platforms at once, letting HA handling each platform and avoiding + # potential integration restarts while elements are still initialising. + await hass.config_entries.async_forward_entry_setups(entry, platforms) for dev_id in device_ids: hass.data[DOMAIN][TUYA_DEVICES][dev_id].async_connect() From 1da7b313d44315382e0e198b1833e2f74eac9f77 Mon Sep 17 00:00:00 2001 From: Andy Barratt Date: Fri, 13 Sep 2024 14:04:02 +0100 Subject: [PATCH 02/16] Add discovered HVAC and PRESET options (#1177) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add discovered HVAC and PRESET options This commit adds the HVAC Action Set and Preset Set that I've discovered my Bödenwarme Underfloor Heating thermostat to be using as detailed here: https://github.com/rospogrigio/localtuya/issues/1175 * Add HVAC mode and action sets for climate entities These are the actions and modes as reported by the Magnum smart wifi thermostat: https://www.magnumheating.com/product/remote-control/ --------- Co-authored-by: Rutger Kerkhoff --- custom_components/localtuya/climate.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/custom_components/localtuya/climate.py b/custom_components/localtuya/climate.py index da23cfa..55bd303 100644 --- a/custom_components/localtuya/climate.py +++ b/custom_components/localtuya/climate.py @@ -72,6 +72,10 @@ HVAC_MODE_SETS = { HVACMode.HEAT: "Manual", HVACMode.AUTO: "Auto", }, + "MANUAL/AUTO": { + HVAC_MODE_HEAT: "MANUAL", + HVAC_MODE_AUTO: "AUTO", + }, "Manual/Program": { HVACMode.HEAT: "Manual", HVACMode.AUTO: "Program", @@ -112,6 +116,10 @@ HVAC_ACTION_SETS = { HVACAction.HEATING: "Heat", HVACAction.IDLE: "Warming", }, + "heating/warming": { + CURRENT_HVAC_HEAT: "heating", + CURRENT_HVAC_IDLE: "warming", + }, } HVAC_FAN_MODE_SETS = { "Auto/Low/Middle/High/Strong": { @@ -134,6 +142,11 @@ PRESET_SETS = { PRESET_HOME: "Program", PRESET_NONE: "Manual", }, + "smart/holiday/hold": { + PRESET_AWAY: "holiday", + PRESET_HOME: "smart", + PRESET_NONE: "hold", + }, } TEMPERATURE_CELSIUS = "celsius" From e1fb0f2ccb6a237a732b6d78d03f796b92737c0b Mon Sep 17 00:00:00 2001 From: dim145 Date: Sat, 4 Jan 2025 04:06:43 +0100 Subject: [PATCH 03/16] fix: do not use deprecated features --- custom_components/localtuya/const.py | 1 + custom_components/localtuya/light.py | 91 ++++++++++++++++++++-------- 2 files changed, 67 insertions(+), 25 deletions(-) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index 75f7bbd..6e7556b 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -53,6 +53,7 @@ CONF_BRIGHTNESS_LOWER = "brightness_lower" 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/light.py b/custom_components/localtuya/light.py index 7c74e49..bc70594 100644 --- a/custom_components/localtuya/light.py +++ b/custom_components/localtuya/light.py @@ -1,21 +1,20 @@ """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.ecobee.humidifier import MODE_MANUAL 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 +27,7 @@ from .const import ( 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__) @@ -49,6 +48,8 @@ MODE_WHITE = "white" 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 @@ SCENE_LIST_RGB_1000 = { + "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,6 +179,7 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): 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 = [] @@ -197,8 +215,8 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): 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 @@ -244,24 +262,36 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): return self._effect_list @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 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): @@ -304,7 +334,7 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): 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 +345,11 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): 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 +381,7 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): 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,7 +408,7 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): 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 ATTR_COLOR_TEMP in kwargs and (features & ColorMode.RGB): if brightness is None: brightness = self._brightness mired = int(kwargs[ATTR_COLOR_TEMP]) @@ -403,10 +437,17 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): 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 brightness_dp_value := self.dp_value(CONF_BRIGHTNESS, None): + self._brightness = brightness_dp_value + + 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 +463,10 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): 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 +481,7 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): 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 From d57932e8e40c687e10e51b533f685d7760302c2d Mon Sep 17 00:00:00 2001 From: dim145 Date: Sat, 4 Jan 2025 04:13:47 +0100 Subject: [PATCH 04/16] fix: do not use deprecated attr in light entity --- custom_components/localtuya/light.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/custom_components/localtuya/light.py b/custom_components/localtuya/light.py index bc70594..bad9d72 100644 --- a/custom_components/localtuya/light.py +++ b/custom_components/localtuya/light.py @@ -9,12 +9,12 @@ import voluptuous as vol from homeassistant.components.ecobee.humidifier import MODE_MANUAL from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, DOMAIN, LightEntity, - LightEntityFeature, ColorMode, + LightEntityFeature, + ColorMode, ) from homeassistant.const import CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_SCENE @@ -408,10 +408,10 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): states[self._config.get(CONF_COLOR)] = color states[self._config.get(CONF_COLOR_MODE)] = MODE_COLOR - if ATTR_COLOR_TEMP in kwargs and (features & ColorMode.RGB): + 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: From 42b6eb1fc1b0d353c414e16441ed98dc2eb7aae4 Mon Sep 17 00:00:00 2001 From: dim145 Date: Sat, 4 Jan 2025 11:52:40 +0100 Subject: [PATCH 05/16] refactor: define MODE_MANUAL --- custom_components/localtuya/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/light.py b/custom_components/localtuya/light.py index bad9d72..e3188b1 100644 --- a/custom_components/localtuya/light.py +++ b/custom_components/localtuya/light.py @@ -6,7 +6,6 @@ from functools import partial import homeassistant.util.color as color_util import voluptuous as vol -from homeassistant.components.ecobee.humidifier import MODE_MANUAL from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_EFFECT, @@ -40,6 +39,7 @@ DEFAULT_COLOR_TEMP_REVERSE = False DEFAULT_LOWER_BRIGHTNESS = 29 DEFAULT_UPPER_BRIGHTNESS = 1000 +MODE_MANUAL = "manual" MODE_COLOR = "colour" MODE_MUSIC = "music" MODE_SCENE = "scene" From b01861d53ea8c06419688d8f2ba79c0f2aa295c6 Mon Sep 17 00:00:00 2001 From: dim145 Date: Sat, 4 Jan 2025 12:10:52 +0100 Subject: [PATCH 06/16] fix: add FanEntityFeature.TURN_OFF/ON to fans --- custom_components/localtuya/fan.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 5e1a3c9..59c33ac 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -9,7 +9,8 @@ from homeassistant.components.fan import ( DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, - FanEntity, FanEntityFeature, + FanEntityFeature, + FanEntity, ) from homeassistant.util.percentage import ( int_states_in_range, @@ -186,9 +187,9 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): 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 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): if self.has_config(CONF_FAN_DIRECTION): features |= FanEntityFeature.DIRECTION + features |= FanEntityFeature.TURN_OFF + features |= FanEntityFeature.TURN_ON + return features @property From 65f3dafd046f1959170b2d70e578d9e7dbe957cb Mon Sep 17 00:00:00 2001 From: dim145 Date: Sat, 4 Jan 2025 12:38:41 +0100 Subject: [PATCH 07/16] fix: add color_mode property & adapt color mode method (is_xxx_mode) --- custom_components/localtuya/light.py | 33 +++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/custom_components/localtuya/light.py b/custom_components/localtuya/light.py index e3188b1..fd286db 100644 --- a/custom_components/localtuya/light.py +++ b/custom_components/localtuya/light.py @@ -183,7 +183,8 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): 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 @@ -192,6 +193,7 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): 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) @@ -259,7 +261,11 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): @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_color_modes(self) -> set[ColorMode] | set[str] | None: @@ -287,6 +293,21 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): 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.""" @@ -297,19 +318,19 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): 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 @@ -324,7 +345,7 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): 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): From 096568bd20fb0a91da0cf066a61378c3bd6b611b Mon Sep 17 00:00:00 2001 From: dim145 Date: Sat, 4 Jan 2025 12:44:04 +0100 Subject: [PATCH 08/16] fix: remove useless test line in status_updated of light --- custom_components/localtuya/light.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/custom_components/localtuya/light.py b/custom_components/localtuya/light.py index fd286db..66773c2 100644 --- a/custom_components/localtuya/light.py +++ b/custom_components/localtuya/light.py @@ -465,9 +465,6 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): ): self._brightness = self.dps_conf(CONF_BRIGHTNESS) - if brightness_dp_value := self.dp_value(CONF_BRIGHTNESS, None): - self._brightness = brightness_dp_value - 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: From 1369245b1597b9b277a27a96681434af93212003 Mon Sep 17 00:00:00 2001 From: dim145 Date: Sat, 4 Jan 2025 13:42:44 +0100 Subject: [PATCH 09/16] fix: climate deprecated attributes --- custom_components/localtuya/climate.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/custom_components/localtuya/climate.py b/custom_components/localtuya/climate.py index 55bd303..f56f076 100644 --- a/custom_components/localtuya/climate.py +++ b/custom_components/localtuya/climate.py @@ -73,8 +73,8 @@ HVAC_MODE_SETS = { 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 @@ HVAC_ACTION_SETS = { HVACAction.IDLE: "Warming", }, "heating/warming": { - CURRENT_HVAC_HEAT: "heating", - CURRENT_HVAC_IDLE: "warming", + HVACAction.HEATING: "heating", + HVACAction.IDLE: "warming", }, } HVAC_FAN_MODE_SETS = { From 6202c74e3922af7f6d75d5a7e84ba6b54a3527a3 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 14 Jan 2025 10:13:07 +0000 Subject: [PATCH 10/16] add a subset of HVAC_MODE_SETS (#1875) iPAC-40 Portable Air Conditioning Heat Pump with Wifi and Remote Control. These devices have a limited option of modes. The available modes are Hot, Cold and Dry. Co-authored-by: rospogrigio <49229287+rospogrigio@users.noreply.github.com> --- custom_components/localtuya/climate.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/custom_components/localtuya/climate.py b/custom_components/localtuya/climate.py index f56f076..1bf3b9b 100644 --- a/custom_components/localtuya/climate.py +++ b/custom_components/localtuya/climate.py @@ -94,6 +94,11 @@ HVAC_MODE_SETS = { HVACMode.COOL: "cold", HVACMode.AUTO: "auto", }, + "Cold/Dehumidify/Hot": { + HVACMode.HEAT: "hot", + HVACMode.DRY: "dehumidify", + HVACMode.COOL: "cold", + }, "1/0": { HVACMode.HEAT: "1", HVACMode.AUTO: "0", From 12d40f81afcc9e309f56f7c0da6becf2c41d7c42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20D=C3=B6rfler?= <30753608+0Styless@users.noreply.github.com> Date: Tue, 14 Jan 2025 11:26:26 +0100 Subject: [PATCH 11/16] fix(common.py) - Change loglevel for missing device configuration from WARNING to DEBUG. (#1816) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Loglevel warning produces a huge amount of log messages (around 500000 a day - depends on how much tuya devices are in your local network). Co-authored-by: Tobias Dörfler Co-authored-by: rospogrigio <49229287+rospogrigio@users.noreply.github.com> --- custom_components/localtuya/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/common.py b/custom_components/localtuya/common.py index 4ad46f6..fdb3503 100644 --- a/custom_components/localtuya/common.py +++ b/custom_components/localtuya/common.py @@ -127,7 +127,7 @@ def async_config_entry_by_device_id(hass, device_id): if device_id in entry.data.get(CONF_DEVICES, []): return entry else: - _LOGGER.warning(f"Missing device configuration for device_id {device_id}") + _LOGGER.debug(f"Missing device configuration for device_id {device_id}") return None From 01a6f0fa155875a4df4fb02044ab699f3194b6e4 Mon Sep 17 00:00:00 2001 From: Handrail9 <86343914+Handrail9@users.noreply.github.com> Date: Tue, 14 Jan 2025 14:03:16 +0000 Subject: [PATCH 12/16] Update README.md (#1808) Tuya integration page shows new information and no longer includes how to get Client ID & Secret. Added those here. Co-authored-by: rospogrigio <49229287+rospogrigio@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e54de7d..8f5211e 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ The Cloud API configuration page will appear, requesting to input your Tuya IoT To setup a Tuya IoT Platform account and setup a project in it, refer to the instructions for the official Tuya integration: https://www.home-assistant.io/integrations/tuya/ -The place to find the Client ID and Secret is described in this link (in the ["Get Authorization Key"](https://www.home-assistant.io/integrations/tuya/#get-authorization-key) paragraph), while the User ID can be found in the "Link Tuya App Account" subtab within the Cloud project: +The Client ID and Secret can be found at `Cloud > Development > Overview` and the User ID can be found in the "Link Tuya App Account" subtab within the Cloud project: ![user_id.png](https://github.com/rospogrigio/localtuya-homeassistant/blob/master/img/8-user_id.png) From 5f2c027c1e9421a93dcc937bf151b9456add04c6 Mon Sep 17 00:00:00 2001 From: rospogrigio <49229287+rospogrigio@users.noreply.github.com> Date: Tue, 14 Jan 2025 15:05:27 +0100 Subject: [PATCH 13/16] Update manifest.json --- custom_components/localtuya/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/manifest.json b/custom_components/localtuya/manifest.json index 28e36fa..95f34fb 100644 --- a/custom_components/localtuya/manifest.json +++ b/custom_components/localtuya/manifest.json @@ -10,5 +10,5 @@ "iot_class": "local_push", "issue_tracker": "https://github.com/rospogrigio/localtuya/issues", "requirements": [], - "version": "5.2.1" + "version": "5.2.3" } From e6bcaa112748d72283a0fe8c8f0dacb803ae5003 Mon Sep 17 00:00:00 2001 From: Michiel De Wilde Date: Tue, 22 Apr 2025 18:11:23 +0200 Subject: [PATCH 14/16] Deprecation fix: use async_register_admin_service This fixes the following reported issue: Detected that custom integration 'localtuya' accesses hass.helpers.service, which should be updated to import functions used from service directly at custom_components/localtuya/__init__.py, line 165: hass.helpers.service.async_register_admin_service(. This will stop working in Home Assistant 2025.5, please create a bug report at https://github.com/rospogrigio/localtuya/issues --- custom_components/localtuya/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/custom_components/localtuya/__init__.py b/custom_components/localtuya/__init__.py index 35a2adb..2f0e0fc 100644 --- a/custom_components/localtuya/__init__.py +++ b/custom_components/localtuya/__init__.py @@ -26,6 +26,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.service import async_register_admin_service from .cloud_api import TuyaCloudApi from .common import TuyaDevice, async_config_entry_by_device_id @@ -162,7 +163,8 @@ async def async_setup(hass: HomeAssistant, config: dict): async_track_time_interval(hass, _async_reconnect, RECONNECT_INTERVAL) - hass.helpers.service.async_register_admin_service( + async_register_admin_service( + hass, DOMAIN, SERVICE_RELOAD, _handle_reload, From 2217b09286c3cfbb010bb15ac7c5bc8ed0e6c5e6 Mon Sep 17 00:00:00 2001 From: Michiel De Wilde Date: Tue, 22 Apr 2025 18:13:46 +0200 Subject: [PATCH 15/16] Deprecation fix: do not call async_forward_entry_setups in an async task This fixes the following reported issue: Detected that custom integration 'localtuya' calls async_forward_entry_setups for integration localtuya with title: *** and entry_id: ***, during setup without awaiting async_forward_entry_setups, which can cause the setup lock to be released before the setup is done at custom_components/localtuya/__init__.py, line 275: await hass.config_entries.async_forward_entry_setups(entry, platforms). This will stop working in Home Assistant 2025.1, please create a bug report at https://github.com/rospogrigio/localtuya/issues --- custom_components/localtuya/__init__.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/custom_components/localtuya/__init__.py b/custom_components/localtuya/__init__.py index 2f0e0fc..a54656d 100644 --- a/custom_components/localtuya/__init__.py +++ b/custom_components/localtuya/__init__.py @@ -263,19 +263,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): res = await tuya_api.async_get_devices_list() hass.data[DOMAIN][DATA_CLOUD] = tuya_api + platforms = set() + for dev_id in entry.data[CONF_DEVICES].keys(): + entities = entry.data[CONF_DEVICES][dev_id][CONF_ENTITIES] + platforms = platforms.union( + set(entity[CONF_PLATFORM] for entity in entities) + ) + hass.data[DOMAIN][TUYA_DEVICES][dev_id] = TuyaDevice(hass, entry, dev_id) + + # Setup all platforms at once, letting HA handling each platform and avoiding + # potential integration restarts while elements are still initialising. + await hass.config_entries.async_forward_entry_setups(entry, platforms) + async def setup_entities(device_ids): - platforms = set() - for dev_id in device_ids: - entities = entry.data[CONF_DEVICES][dev_id][CONF_ENTITIES] - platforms = platforms.union( - set(entity[CONF_PLATFORM] for entity in entities) - ) - hass.data[DOMAIN][TUYA_DEVICES][dev_id] = TuyaDevice(hass, entry, dev_id) - - # Setup all platforms at once, letting HA handling each platform and avoiding - # potential integration restarts while elements are still initialising. - await hass.config_entries.async_forward_entry_setups(entry, platforms) - for dev_id in device_ids: hass.data[DOMAIN][TUYA_DEVICES][dev_id].async_connect() From 9eabc43da2414fbb96904fa3a2f4c922fae68e21 Mon Sep 17 00:00:00 2001 From: Michiel De Wilde Date: Tue, 22 Apr 2025 18:14:40 +0200 Subject: [PATCH 16/16] Deprecation fix: use VacuumActivity This fixes the following reported issue: STATE_DOCKED was used from localtuya, this is a deprecated constant which will be removed in HA Core 2026.1. Use VacuumActivity.DOCKED instead, please report it to the author of the 'localtuya' custom integration STATE_ERROR was used from localtuya, this is a deprecated constant which will be removed in HA Core 2026.1. Use VacuumActivity.ERROR instead, please report it to the author of the 'localtuya' custom integration STATE_IDLE was used from localtuya, this is a deprecated constant which will be removed in HA Core 2026.1. Use VacuumActivity.IDLE instead, please report it to the author of the 'localtuya' custom integration STATE_PAUSED was used from localtuya, this is a deprecated constant which will be removed in HA Core 2026.1. Use VacuumActivity.PAUSED instead, please report it to the author of the 'localtuya' custom integration STATE_RETURNING was used from localtuya, this is a deprecated constant which will be removed in HA Core 2026.1. Use VacuumActivity.RETURNING instead, please report it to the author of the 'localtuya' custom integration --- custom_components/localtuya/vacuum.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/custom_components/localtuya/vacuum.py b/custom_components/localtuya/vacuum.py index 11e7a61..0ac0b1e 100644 --- a/custom_components/localtuya/vacuum.py +++ b/custom_components/localtuya/vacuum.py @@ -5,13 +5,7 @@ from functools import partial import voluptuous as vol from homeassistant.components.vacuum import ( DOMAIN, - STATE_CLEANING, - STATE_DOCKED, - STATE_ERROR, - STATE_IDLE, - STATE_PAUSED, - STATE_RETURNING, - StateVacuumEntity, VacuumEntityFeature, + StateVacuumEntity, VacuumActivity, VacuumEntityFeature, ) from .common import LocalTuyaEntity, async_setup_entry @@ -207,15 +201,15 @@ class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity): state_value = str(self.dps(self._dp_id)) if state_value in self._idle_status_list: - self._state = STATE_IDLE + self._state = VacuumActivity.IDLE elif state_value in self._docked_status_list: - self._state = STATE_DOCKED + self._state = VacuumActivity.DOCKED elif state_value == self._config[CONF_RETURNING_STATUS_VALUE]: - self._state = STATE_RETURNING + self._state = VacuumActivity.RETURNING elif state_value == self._config[CONF_PAUSED_STATE]: - self._state = STATE_PAUSED + self._state = VacuumActivity.PAUSED else: - self._state = STATE_CLEANING + self._state = VacuumActivity.CLEANING if self.has_config(CONF_BATTERY_DP): self._battery_level = self.dps_conf(CONF_BATTERY_DP) @@ -241,7 +235,7 @@ class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity): if self.has_config(CONF_FAULT_DP): self._attrs[FAULT] = self.dps_conf(CONF_FAULT_DP) if self._attrs[FAULT] != 0: - self._state = STATE_ERROR + self._state = VacuumActivity.ERROR async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaVacuum, flow_schema)