"""Platform to locally control Tuya-based climate devices.""" import asyncio import logging from functools import partial 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, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, CURRENT_HVAC_IDLE, CURRENT_HVAC_HEAT, PRESET_NONE, PRESET_ECO, PRESET_AWAY, PRESET_HOME, ) 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_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_HEURISTIC_ACTION, CONF_HVAC_ACTION_DP, CONF_HVAC_ACTION_SET, CONF_ECO_DP, CONF_ECO_VALUE, CONF_PRESET_DP, CONF_PRESET_SET, ) _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", }, "Manual/Program": { HVAC_MODE_HEAT: "Manual", HVAC_MODE_AUTO: "Program", }, "True/False": { HVAC_MODE_HEAT: True, }, } HVAC_ACTION_SETS = { "True/False": { CURRENT_HVAC_HEAT: True, CURRENT_HVAC_IDLE: False, }, "open/close": { CURRENT_HVAC_HEAT: "open", CURRENT_HVAC_IDLE: "close", }, "heating/no_heating": { CURRENT_HVAC_HEAT: "heating", CURRENT_HVAC_IDLE: "no_heating", }, "Heat/Warming": { CURRENT_HVAC_HEAT: "Heat", CURRENT_HVAC_IDLE: "Warming", }, } PRESET_SETS = { "Manual/Holiday/Program": { PRESET_AWAY: "Holiday", PRESET_HOME: "Program", PRESET_NONE: "Manual", }, } TEMPERATURE_CELSIUS = "celsius" TEMPERATURE_FAHRENHEIT = "fahrenheit" DEFAULT_TEMPERATURE_UNIT = TEMPERATURE_CELSIUS DEFAULT_PRECISION = PRECISION_TENTHS DEFAULT_TEMPERATURE_STEP = PRECISION_HALVES # Empirically tested to work for AVATTO thermostat 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_HEURISTIC_ACTION): bool, } 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_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.get(CONF_HEURISTIC_ACTION, False): 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_IDLE: self._hvac_action = CURRENT_HVAC_IDLE if ( self._current_temperature + self._precision ) > self._target_temperature: self._hvac_action = CURRENT_HVAC_IDLE 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 and self._conf_hvac_mode_dp != self._dp_id: await self._device.set_dp(True, self._dp_id) # Some thermostats need a small wait before sending another update 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_turn_on(self) -> None: """Turn the entity on.""" await self._device.set_dp(True, self._dp_id) async def async_turn_off(self) -> None: """Turn the entity off.""" await self._device.set_dp(False, self._dp_id) 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 ) 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)