From 0985ad64f29dbf5b526b28415d5a9bed5c7de4d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Tue, 10 Aug 2021 19:17:56 +0100 Subject: [PATCH] add climate implementation --- custom_components/localtuya/climate.py | 371 ++++++++++++++++++ custom_components/localtuya/const.py | 21 +- .../localtuya/translations/en.json | 40 +- 3 files changed, 429 insertions(+), 3 deletions(-) create mode 100644 custom_components/localtuya/climate.py diff --git a/custom_components/localtuya/climate.py b/custom_components/localtuya/climate.py new file mode 100644 index 0000000..fe4eab3 --- /dev/null +++ b/custom_components/localtuya/climate.py @@ -0,0 +1,371 @@ +"""Platform to locally control Tuya-based climate devices.""" +import asyncio +import logging +import json +from functools import partial +import json + +import voluptuous as vol +from homeassistant.components.climate import ( + DEFAULT_MAX_TEMP, + DEFAULT_MIN_TEMP, + DOMAIN, + ClimateEntity, +) +from homeassistant.components.climate.const import ( + HVAC_MODE_AUTO, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, + CURRENT_HVAC_OFF, + CURRENT_HVAC_HEAT, + PRESET_NONE, + PRESET_ECO, + PRESET_AWAY, + PRESET_BOOST, + PRESET_COMFORT, + PRESET_HOME, + PRESET_SLEEP, + PRESET_ACTIVITY, +) +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_TEMPERATURE_UNIT, + PRECISION_HALVES, + PRECISION_TENTHS, + PRECISION_WHOLE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) + +from .common import LocalTuyaEntity, async_setup_entry +from .const import ( + CONF_CURRENT_TEMPERATURE_DP, + CONF_FAN_MODE_DP, + CONF_MAX_TEMP_DP, + CONF_MIN_TEMP_DP, + CONF_PRECISION, + CONF_TARGET_PRECISION, + CONF_TARGET_TEMPERATURE_DP, + CONF_TEMPERATURE_STEP, + CONF_HVAC_MODE_DP, + CONF_HVAC_MODE_SET, + CONF_EURISTIC_ACTION, + CONF_HVAC_ACTION_DP, + CONF_HVAC_ACTION_SET, + CONF_ECO_DP, + CONF_ECO_VALUE, + CONF_PRESET_DP, + CONF_PRESET_SET, +) + +from . import pytuya + +_LOGGER = logging.getLogger(__name__) + +HVAC_MODE_SETS = { + "manual/auto": { + HVAC_MODE_HEAT: "manual", + HVAC_MODE_AUTO: "auto", + }, + "Manual/Auto": { + HVAC_MODE_HEAT: "Manual", + HVAC_MODE_AUTO: "Auto", + }, +} +HVAC_ACTION_SETS = { + "True/False": { + CURRENT_HVAC_HEAT: True, + CURRENT_HVAC_OFF: False, + }, + "open/close": { + CURRENT_HVAC_HEAT: "open", + CURRENT_HVAC_OFF: "close", + }, + "heating/no_heating": { + CURRENT_HVAC_HEAT: "heating", + CURRENT_HVAC_OFF: "no_heating", + }, +} +PRESET_SETS = { + "Manual/Holiday/Program": { + PRESET_NONE: "Manual", + PRESET_AWAY: "Holiday", + PRESET_HOME: "Program", + }, +} + +TEMPERATURE_CELSIUS = "celsius" +TEMPERATURE_FAHRENHEIT = "fahrenheit" +DEFAULT_TEMPERATURE_UNIT = TEMPERATURE_CELSIUS +DEFAULT_PRECISION = PRECISION_TENTHS +DEFAULT_TEMPERATURE_STEP = PRECISION_HALVES +MODE_WAIT = 0.1 + +def flow_schema(dps): + """Return schema used in config flow.""" + return { + vol.Optional(CONF_TARGET_TEMPERATURE_DP): vol.In(dps), + vol.Optional(CONF_CURRENT_TEMPERATURE_DP): vol.In(dps), + vol.Optional(CONF_TEMPERATURE_STEP): vol.In( + [PRECISION_WHOLE, PRECISION_HALVES, PRECISION_TENTHS] + ), + vol.Optional(CONF_MAX_TEMP_DP): vol.In(dps), + vol.Optional(CONF_MIN_TEMP_DP): vol.In(dps), + vol.Optional(CONF_PRECISION): vol.In( + [PRECISION_WHOLE, PRECISION_HALVES, PRECISION_TENTHS] + ), + vol.Optional(CONF_HVAC_MODE_DP): vol.In(dps), + vol.Optional(CONF_HVAC_MODE_SET): vol.In( + list(HVAC_MODE_SETS.keys()) + ), + vol.Optional(CONF_HVAC_ACTION_DP): vol.In(dps), + vol.Optional(CONF_HVAC_ACTION_SET): vol.In( + list(HVAC_ACTION_SETS.keys()) + ), + vol.Optional(CONF_ECO_DP): vol.In(dps), + vol.Optional(CONF_ECO_VALUE): str, + vol.Optional(CONF_PRESET_DP): vol.In(dps), + vol.Optional(CONF_PRESET_SET): vol.In( + list(PRESET_SETS.keys()) + ), + vol.Optional(CONF_TEMPERATURE_UNIT): vol.In( + [TEMPERATURE_CELSIUS, TEMPERATURE_FAHRENHEIT] + ), + vol.Optional(CONF_TARGET_PRECISION): vol.In( + [PRECISION_WHOLE, PRECISION_HALVES, PRECISION_TENTHS] + ), + vol.Optional(CONF_EURISTIC_ACTION, default=False): bool, + vol.Optional(CONF_FAN_MODE_DP): vol.In(dps), + } + + +class LocaltuyaClimate(LocalTuyaEntity, ClimateEntity): + """Tuya climate device.""" + + def __init__( + self, + device, + config_entry, + switchid, + **kwargs, + ): + """Initialize a new LocaltuyaClimate.""" + super().__init__(device, config_entry, switchid, _LOGGER, **kwargs) + self._state = None + self._target_temperature = None + self._current_temperature = None + self._hvac_mode = None + self._preset_mode = None + self._hvac_action = None + self._precision = self._config.get(CONF_PRECISION, DEFAULT_PRECISION) + self._target_precision = self._config.get(CONF_TARGET_PRECISION, self._precision) + self._conf_hvac_mode_dp = self._config.get(CONF_HVAC_MODE_DP) + self._conf_hvac_mode_set = HVAC_MODE_SETS.get(self._config.get(CONF_HVAC_MODE_SET), {}) + self._conf_preset_dp = self._config.get(CONF_PRESET_DP) + self._conf_preset_set = PRESET_SETS.get(self._config.get(CONF_PRESET_SET), {}) + self._conf_hvac_action_dp = self._config.get(CONF_HVAC_ACTION_DP) + self._conf_hvac_action_set = HVAC_ACTION_SETS.get(self._config.get(CONF_HVAC_ACTION_SET), {}) + self._conf_eco_dp = self._config.get(CONF_ECO_DP) + self._conf_eco_value = self._config.get(CONF_ECO_VALUE, "ECO") + self._has_presets = self.has_config(CONF_ECO_DP) or self.has_config(CONF_PRESET_DP) + print("Initialized climate [{}]".format(self.name)) + + @property + def supported_features(self): + """Flag supported features.""" + supported_features = 0 + if self.has_config(CONF_TARGET_TEMPERATURE_DP): + supported_features = supported_features | SUPPORT_TARGET_TEMPERATURE + if self.has_config(CONF_MAX_TEMP_DP): + supported_features = supported_features | SUPPORT_TARGET_TEMPERATURE_RANGE + if self.has_config(CONF_FAN_MODE_DP): + supported_features = supported_features | SUPPORT_FAN_MODE + if self.has_config(CONF_PRESET_DP) or self.has_config(CONF_ECO_DP): + supported_features = supported_features | SUPPORT_PRESET_MODE + return supported_features + + @property + def precision(self): + """Return the precision of the system.""" + return self._precision + + @property + def target_recision(self): + """Return the precision of the target.""" + return self._target_precision + + @property + def temperature_unit(self): + """Return the unit of measurement used by the platform.""" + if ( + self._config.get(CONF_TEMPERATURE_UNIT, DEFAULT_TEMPERATURE_UNIT) + == TEMPERATURE_FAHRENHEIT + ): + return TEMP_FAHRENHEIT + return TEMP_CELSIUS + + @property + def hvac_mode(self): + """Return current operation ie. heat, cool, idle.""" + return self._hvac_mode + + @property + def hvac_modes(self): + """Return the list of available operation modes.""" + if not self.has_config(CONF_HVAC_MODE_DP): + return None + return list(self._conf_hvac_mode_set) + [HVAC_MODE_OFF] + + @property + def hvac_action(self): + """Return the current running hvac operation if supported. + Need to be one of CURRENT_HVAC_*. + """ + if self._config[CONF_EURISTIC_ACTION]: + if self._hvac_mode == HVAC_MODE_HEAT: + if self._current_temperature < (self._target_temperature - self._precision): + self._hvac_action = CURRENT_HVAC_HEAT + if self._current_temperature == (self._target_temperature - self._precision): + if self._hvac_action == CURRENT_HVAC_HEAT: + self._hvac_action = CURRENT_HVAC_HEAT + if self._hvac_action == CURRENT_HVAC_OFF: + self._hvac_action = CURRENT_HVAC_OFF + if (self._current_temperature + self._precision) > self._target_temperature: + self._hvac_action = CURRENT_HVAC_OFF + return self._hvac_action + return self._hvac_action + + @property + def preset_mode(self): + """Return current preset""" + return self._preset_mode + + @property + def preset_modes(self): + """Return the list of available presets modes.""" + if not self._has_presets: + return None + presets = list(self._conf_preset_set) + if self._conf_eco_dp: + presets.append(PRESET_ECO) + return presets + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._current_temperature + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._target_temperature + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return self._config.get(CONF_TEMPERATURE_STEP, DEFAULT_TEMPERATURE_STEP) + + @property + def fan_mode(self): + """Return the fan setting.""" + return NotImplementedError() + + @property + def fan_modes(self): + """Return the list of available fan modes.""" + return NotImplementedError() + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + if ATTR_TEMPERATURE in kwargs and self.has_config(CONF_TARGET_TEMPERATURE_DP): + temperature = round(kwargs[ATTR_TEMPERATURE] / self._target_precision) + await self._device.set_dp(temperature, self._config[CONF_TARGET_TEMPERATURE_DP]) + + def set_fan_mode(self, fan_mode): + """Set new target fan mode.""" + return NotImplementedError() + + async def async_set_hvac_mode(self, hvac_mode): + """Set new target operation mode.""" + if hvac_mode == HVAC_MODE_OFF: + await self._device.set_dp(False, self._dp_id) + return + if not self._state: + await self._device.set_dp(True, self._dp_id) + await asyncio.sleep(MODE_WAIT) + await self._device.set_dp(self._conf_hvac_mode_set[hvac_mode], self._conf_hvac_mode_dp) + + async def async_set_preset_mode(self, preset_mode): + """Set new target preset mode.""" + if preset_mode == PRESET_ECO: + await self._device.set_dp(self._conf_eco_value, self._conf_eco_dp) + return + await self._device.set_dp(self._conf_preset_set[preset_mode], self._conf_preset_dp) + + @property + def min_temp(self): + """Return the minimum temperature.""" + if self.has_config(CONF_MIN_TEMP_DP): + return self.dps_conf(CONF_MIN_TEMP_DP) + return DEFAULT_MIN_TEMP + + @property + def max_temp(self): + """Return the maximum temperature.""" + if self.has_config(CONF_MAX_TEMP_DP): + return self.dps_conf(CONF_MAX_TEMP_DP) + return DEFAULT_MAX_TEMP + + def status_updated(self): + """Device status was updated.""" + self._state = self.dps(self._dp_id) + + if self.has_config(CONF_TARGET_TEMPERATURE_DP): + self._target_temperature = ( + self.dps_conf(CONF_TARGET_TEMPERATURE_DP) * self._target_precision + ) + + if self.has_config(CONF_CURRENT_TEMPERATURE_DP): + self._current_temperature = ( + self.dps_conf(CONF_CURRENT_TEMPERATURE_DP) * self._precision + ) + + #_LOGGER.debug("the test is %s", test)he preset status""" + if self._has_presets: + if self.has_config(CONF_ECO_DP) and self.dps_conf(CONF_ECO_DP) == self._conf_eco_value: + self._preset_mode = PRESET_ECO + else: + for preset,value in self._conf_preset_set.items(): # todo remove + if self.dps_conf(CONF_PRESET_DP) == value: + self._preset_mode = preset + break + else: + self._preset_mode = PRESET_NONE + + """Update the HVAC status""" + if self.has_config(CONF_HVAC_MODE_DP): + if not self._state: + self._hvac_mode = HVAC_MODE_OFF + else: + for mode,value in self._conf_hvac_mode_set.items(): + if self.dps_conf(CONF_HVAC_MODE_DP) == value: + self._hvac_mode = mode + break + else: + # in case hvac mode and preset share the same dp + self._hvac_mode = HVAC_MODE_AUTO + + """Update the current action""" + for action,value in self._conf_hvac_action_set.items(): + if self.dps_conf(CONF_HVAC_ACTION_DP) == value: + self._hvac_action = action + +async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaClimate, flow_schema) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index bd8a5d3..6c03420 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -41,11 +41,30 @@ CONF_FAN_SPEED_HIGH = "fan_speed_high" # sensor CONF_SCALING = "scaling" +# climate +CONF_TARGET_TEMPERATURE_DP = "target_temperature_dp" +CONF_CURRENT_TEMPERATURE_DP = "current_temperature_dp" +CONF_TEMPERATURE_STEP = "temperature_step" +CONF_MAX_TEMP_DP = "max_temperature_dp" +CONF_MIN_TEMP_DP = "min_temperature_dp" +CONF_FAN_MODE_DP = "fan_mode_dp" +CONF_PRECISION = "precision" +CONF_TARGET_PRECISION = "target_precision" +CONF_HVAC_MODE_DP = "hvac_mode_dp" +CONF_HVAC_MODE_SET = "hvac_mode_set" +CONF_PRESET_DP = "preset_dp" +CONF_PRESET_SET = "preset_set" +CONF_EURISTIC_ACTION = "euristic_action" +CONF_HVAC_ACTION_DP = "hvac_action_dp" +CONF_HVAC_ACTION_SET = "hvac_action_set" +CONF_ECO_DP = "eco_dp" +CONF_ECO_VALUE = "eco_value" + DATA_DISCOVERY = "discovery" DOMAIN = "localtuya" # Platforms in this list must support config flows -PLATFORMS = ["binary_sensor", "cover", "fan", "light", "sensor", "switch"] +PLATFORMS = ["binary_sensor", "cover", "climate", "fan", "light", "sensor", "switch"] TUYA_DEVICE = "tuya_device" diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index 6ce233a..8ede6fe 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -74,7 +74,25 @@ "fan_oscillating_control": "Fan Oscillating Control", "fan_speed_low": "Fan Low Speed Setting", "fan_speed_medium": "Fan Medium Speed Setting", - "fan_speed_high": "Fan High Speed Setting" + "fan_speed_high": "Fan High Speed Setting", + "current_temperature_dp": "Current Temperature", + "target_temperature_dp": "Target Temperature", + "fan_mode_dp": "Fan Mode (optional)", + "temperature_step": "Temperature Step (optional)", + "max_temperature_dp": "Max Temperature (optional)", + "min_temperature_dp": "Min Temperature (optional)", + "precision": "Precision (optional, for DPs values)", + "target_precision": "Target Precision (optional, for DPs values)", + "temperature_unit": "Temperature Unit (optional)", + "hvac_mode_dp": "HVAC Mode DP (optional)", + "hvac_mode_set": "HVAC Mode Set (optional)", + "hvac_action_dp": "HVAC Current Action DP (optional)", + "hvac_action_set": "HVAC Current Action Set (optional)", + "preset_dp": "Presets DP (optional)", + "preset_set": "Presets Set (optional)", + "eco_dp": "Eco DP (optional)", + "eco_value": "Eco value (optional)", + "euristic_action": "Enable euristic action (optional)" } } } @@ -126,7 +144,25 @@ "fan_oscillating_control": "Fan Oscillating Control", "fan_speed_low": "Fan Low Speed Setting", "fan_speed_medium": "Fan Medium Speed Setting", - "fan_speed_high": "Fan High Speed Setting" + "fan_speed_high": "Fan High Speed Setting", + "current_temperature_dp": "Current Temperature", + "target_temperature_dp": "Target Temperature", + "fan_mode_dp": "Fan Mode (optional)", + "temperature_step": "Temperature Step (optional)", + "max_temperature_dp": "Max Temperature (optional)", + "min_temperature_dp": "Min Temperature (optional)", + "precision": "Precision (optional, for DPs values)", + "target_precision": "Target Precision (optional, for DPs values)", + "temperature_unit": "Temperature Unit (optional)", + "hvac_mode_dp": "HVAC Mode DP (optional)", + "hvac_mode_set": "HVAC Mode Set (optional)", + "hvac_action_dp": "HVAC Current Action DP (optional)", + "hvac_action_set": "HVAC Current Action Set (optional)", + "preset_dp": "Presets DP (optional)", + "preset_set": "Presets Set (optional)", + "eco_dp": "Eco DP (optional)", + "eco_value": "Eco value (optional)", + "euristic_action": "Enable euristic action (optional)" } }, "yaml_import": {