diff --git a/custom_components/versatile_thermostat/climate.py b/custom_components/versatile_thermostat/climate.py index a7bf6e1..479ec04 100644 --- a/custom_components/versatile_thermostat/climate.py +++ b/custom_components/versatile_thermostat/climate.py @@ -2,9 +2,10 @@ import math import logging from datetime import timedelta +from typing import Any, Mapping from homeassistant.core import ( - # HomeAssistant, + HomeAssistant, callback, CoreState, DOMAIN as HA_DOMAIN, @@ -35,10 +36,10 @@ from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, HVAC_MODE_OFF, PRESET_ACTIVITY, - # PRESET_AWAY, - # PRESET_BOOST, - # PRESET_COMFORT, - # PRESET_ECO, + PRESET_AWAY, + PRESET_BOOST, + PRESET_COMFORT, + PRESET_ECO, # PRESET_HOME, PRESET_NONE, # PRESET_SLEEP, @@ -94,7 +95,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( - _, # hass: HomeAssistant, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: @@ -134,6 +135,7 @@ async def async_setup_entry( async_add_entities( [ VersatileThermostat( + hass, unique_id, name, heater_entity_id, @@ -170,6 +172,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): def __init__( self, + hass, unique_id, name, heater_entity_id, @@ -195,6 +198,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): super().__init__() + self._hass = hass + self._attr_extra_state_attributes = {} + self._unique_id = unique_id self._name = name self._heater_entity_id = heater_entity_id @@ -360,6 +366,13 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): """Return the sensor temperature.""" return self._cur_temp + # @property + # def extra_state_attributes(self) -> Mapping[str, Any] | None: + # _LOGGER.debug( + # "Calling extra_state_attributes: %s", self._hass.custom_attributes + # ) + # return self._hass.custom_attributes + async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" _LOGGER.info("%s - Set hvac mode: %s", self, hvac_mode) @@ -655,6 +668,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): if not self._hvac_mode and old_state.state: self._hvac_mode = old_state.state + self._prop_algorithm.calculate( + self._target_temp, self._cur_temp, self._cur_ext_temp + ) + else: # No previous state, try and restore defaults if self._target_temp is None: @@ -1007,6 +1024,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): await self._async_heater_turn_on() + self.update_custom_attributes() + async def _turn_off(_): _LOGGER.info( "%s - stop heating for %d min %d sec", @@ -1017,6 +1036,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): await self._async_heater_turn_off() self._async_cancel_cycle() self._async_cancel_cycle = None + self.update_custom_attributes() # Program turn off self._async_cancel_cycle = async_call_later( @@ -1025,6 +1045,32 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): _turn_off, ) + def update_custom_attributes(self): + """Update the custom extra attributes for the entity""" + + self._attr_extra_state_attributes = { + "away_temp": self._presets[PRESET_AWAY], + "eco_temp": self._presets[PRESET_ECO], + "boost_temp": self._presets[PRESET_BOOST], + "comfort_temp": self._presets[PRESET_BOOST], + "power_temp": self._presets[PRESET_POWER], + "on_percent": self._prop_algorithm.on_percent, + "on_time_sec": self._prop_algorithm.on_time_sec, + "off_time_sec": self._prop_algorithm.off_time_sec, + "ext_current_temperature": self._cur_ext_temp, + "current_power": self._current_power, + "current_power_max": self._current_power_max, + "cycle_min": self._cycle_min, + "bias": self._proportional_bias, + "function": self._proportional_function, + "tpi_coefc": self._tpi_coefc, + "tpi_coeft": self._tpi_coeft, + "is_device_active": self._is_device_active, + } + _LOGGER.debug( + "Calling update_custom_attributes: %s", self._attr_extra_state_attributes + ) + @callback def async_registry_entry_updated(self): """update the entity if the config entry have been updated diff --git a/custom_components/versatile_thermostat/prop_algorithm.py b/custom_components/versatile_thermostat/prop_algorithm.py index c1d692d..776b1de 100644 --- a/custom_components/versatile_thermostat/prop_algorithm.py +++ b/custom_components/versatile_thermostat/prop_algorithm.py @@ -31,6 +31,7 @@ class PropAlgorithm: self._tpi_coefc = tpi_coefc self._tpi_coeft = tpi_coeft self._cycle_min = cycle_min + self._on_percent = 0 self._on_time_sec = 0 self._off_time_sec = self._cycle_min * 60 @@ -42,7 +43,7 @@ class PropAlgorithm: _LOGGER.warning( "Proportional algorithm: calculation is not possible cause target_temp or current_temp is null. Heating will be disabled" # pylint: disable=line-too-long ) - on_percent = 0 + self._on_percent = 0 else: delta_temp = target_temp - current_temp delta_ext_temp = ( @@ -50,11 +51,11 @@ class PropAlgorithm: ) if self._function == PROPORTIONAL_FUNCTION_LINEAR: - on_percent = 0.25 * delta_temp + self._bias + self._on_percent = 0.25 * delta_temp + self._bias elif self._function == PROPORTIONAL_FUNCTION_ATAN: - on_percent = math.atan(delta_temp + self._bias) / 1.4 + self._on_percent = math.atan(delta_temp + self._bias) / 1.4 elif self._function == PROPORTIONAL_FUNCTION_TPI: - on_percent = ( + self._on_percent = ( self._tpi_coefc * delta_temp + self._tpi_coeft * delta_ext_temp ) else: @@ -62,14 +63,14 @@ class PropAlgorithm: "Proportional algorithm: unknown %s function. Heating will be disabled", self._function, ) - on_percent = 0 + self._on_percent = 0 # calculated on_time duration in seconds - if on_percent > 1: - on_percent = 1 - if on_percent < 0: - on_percent = 0 - self._on_time_sec = on_percent * self._cycle_min * 60 + if self._on_percent > 1: + self._on_percent = 1 + if self._on_percent < 0: + self._on_percent = 0 + self._on_time_sec = self._on_percent * self._cycle_min * 60 # Do not heat for less than xx sec if self._on_time_sec < PROPORTIONAL_MIN_DURATION_SEC: @@ -80,18 +81,23 @@ class PropAlgorithm: ) self._on_time_sec = 0 - self._off_time_sec = (1.0 - on_percent) * self._cycle_min * 60 + self._off_time_sec = self._cycle_min * 60 - self._on_time_sec _LOGGER.debug( "heating percent calculated for current_temp %.1f, ext_current_temp %.1f and target_temp %.1f is %.2f, on_time is %d (sec), off_time is %d (sec)", # pylint: disable=line-too-long current_temp if current_temp else -9999.0, ext_current_temp if ext_current_temp else -9999.0, target_temp if target_temp else -9999.0, - on_percent, + self._on_percent, self.on_time_sec, self.off_time_sec, ) + @property + def on_percent(self) -> float: + """Returns the percentage the heater must be ON (1 means the heater will be always on, 0 never on)""" # pylint: disable=line-too-long + return round(self._on_percent, 2) + @property def on_time_sec(self) -> int: """Returns the calculated time in sec the heater must be ON"""