diff --git a/custom_components/versatile_thermostat/base_thermostat.py b/custom_components/versatile_thermostat/base_thermostat.py index 41a1c9d1..6c824a7f 100644 --- a/custom_components/versatile_thermostat/base_thermostat.py +++ b/custom_components/versatile_thermostat/base_thermostat.py @@ -138,6 +138,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): ClimateEntity._entity_component_unrecorded_attributes.union( frozenset( { + "is_on", "type", "eco_temp", "boost_temp", @@ -170,6 +171,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): "presence_sensor_entity_id", "power_sensor_entity_id", "max_power_sensor_entity_id", + "temperature_unit", } ) ) @@ -1032,6 +1034,11 @@ def nb_underlying_entities(self) -> int: """Returns the number of underlying entities""" return len(self._underlyings) + @property + def is_on(self) -> bool: + """True if the VTherm is on (! HVAC_OFF)""" + return self.hvac_mode and self.hvac_mode != HVACMode.OFF + def underlying_entity_id(self, index=0) -> str | None: """The climate_entity_id. Added for retrocompatibility reason""" if index < self.nb_underlying_entities: @@ -2107,6 +2114,7 @@ def update_custom_attributes(self): """Update the custom extra attributes for the entity""" self._attr_extra_state_attributes: dict(str, str) = { + "is_on": self.is_on, "hvac_action": self.hvac_action, "hvac_mode": self.hvac_mode, "preset_mode": self.preset_mode, @@ -2166,6 +2174,7 @@ def update_custom_attributes(self): "presence_sensor_entity_id": self._presence_sensor_entity_id, "power_sensor_entity_id": self._power_sensor_entity_id, "max_power_sensor_entity_id": self._max_power_sensor_entity_id, + "temperature_unit": self.temperature_unit, } @callback diff --git a/custom_components/versatile_thermostat/thermostat_climate.py b/custom_components/versatile_thermostat/thermostat_climate.py index 145ec3d3..451ffe3d 100644 --- a/custom_components/versatile_thermostat/thermostat_climate.py +++ b/custom_components/versatile_thermostat/thermostat_climate.py @@ -4,7 +4,10 @@ from datetime import timedelta, datetime from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.event import async_track_state_change_event, async_track_time_interval +from homeassistant.helpers.event import ( + async_track_state_change_event, + async_track_time_interval, +) from homeassistant.components.climate import HVACAction, HVACMode @@ -29,27 +32,40 @@ RegulationParamSlow, RegulationParamLight, RegulationParamMedium, - RegulationParamStrong + RegulationParamStrong, ) from .underlyings import UnderlyingClimate _LOGGER = logging.getLogger(__name__) + class ThermostatOverClimate(BaseThermostat): """Representation of a base class for a Versatile Thermostat over a climate""" - _auto_regulation_mode:str = None + + _auto_regulation_mode: str = None _regulation_algo = None _regulated_target_temp: float = None _auto_regulation_dtemp: float = None _auto_regulation_period_min: int = None _last_regulation_change: datetime = None - _entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union(frozenset( - { - "is_over_climate", "start_hvac_action_date", "underlying_climate_0", "underlying_climate_1", - "underlying_climate_2", "underlying_climate_3", "regulation_accumulated_error" - })) + _entity_component_unrecorded_attributes = ( + BaseThermostat._entity_component_unrecorded_attributes.union( + frozenset( + { + "is_over_climate", + "start_hvac_action_date", + "underlying_climate_0", + "underlying_climate_1", + "underlying_climate_2", + "underlying_climate_3", + "regulation_accumulated_error", + "auto_regulation_mode", + } + ) + ) + ) def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: """Initialize the thermostat over switch.""" @@ -60,12 +76,12 @@ def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: @property def is_over_climate(self) -> bool: - """ True if the Thermostat is over_climate""" + """True if the Thermostat is over_climate""" return True @property def hvac_action(self) -> HVACAction | None: - """ Returns the current hvac_action by checking all hvac_action of the underlyings """ + """Returns the current hvac_action by checking all hvac_action of the underlyings""" # if one not IDLE or OFF -> return it # else if one IDLE -> IDLE @@ -92,28 +108,44 @@ async def _async_internal_set_temperature(self, temperature): await self._send_regulated_temperature(force=True) async def _send_regulated_temperature(self, force=False): - """ Sends the regulated temperature to all underlying """ + """Sends the regulated temperature to all underlying""" if not self._regulated_target_temp: self._regulated_target_temp = self.target_temperature new_regulated_temp = round_to_nearest( - self._regulation_algo.calculate_regulated_temperature(self.current_temperature, self._cur_ext_temp), - self._auto_regulation_dtemp) + self._regulation_algo.calculate_regulated_temperature( + self.current_temperature, self._cur_ext_temp + ), + self._auto_regulation_dtemp, + ) dtemp = new_regulated_temp - self._regulated_target_temp if not force and abs(dtemp) < self._auto_regulation_dtemp: - _LOGGER.debug("%s - dtemp (%.1f) is < %.1f -> forget the regulation send", self, dtemp, self._auto_regulation_dtemp) + _LOGGER.debug( + "%s - dtemp (%.1f) is < %.1f -> forget the regulation send", + self, + dtemp, + self._auto_regulation_dtemp, + ) return - now:datetime = NowClass.get_now(self._hass) - period = float((now - self._last_regulation_change).total_seconds()) / 60. + now: datetime = NowClass.get_now(self._hass) + period = float((now - self._last_regulation_change).total_seconds()) / 60.0 if not force and period < self._auto_regulation_period_min: - _LOGGER.debug("%s - period (%.1f) is < %.0f -> forget the regulation send", self, period, self._auto_regulation_period_min) + _LOGGER.debug( + "%s - period (%.1f) is < %.0f -> forget the regulation send", + self, + period, + self._auto_regulation_period_min, + ) return - self._regulated_target_temp = new_regulated_temp - _LOGGER.info("%s - Regulated temp have changed to %.1f. Resend it to underlyings", self, new_regulated_temp) + _LOGGER.info( + "%s - Regulated temp have changed to %.1f. Resend it to underlyings", + self, + new_regulated_temp, + ) self._last_regulation_change = now for under in self._underlyings: @@ -123,7 +155,7 @@ async def _send_regulated_temperature(self, force=False): @overrides def post_init(self, entry_infos): - """ Initialize the Thermostat""" + """Initialize the Thermostat""" super().post_init(entry_infos) for climate in [ @@ -142,14 +174,24 @@ def post_init(self, entry_infos): ) self.choose_auto_regulation_mode( - entry_infos.get(CONF_AUTO_REGULATION_MODE) if entry_infos.get(CONF_AUTO_REGULATION_MODE) is not None else CONF_AUTO_REGULATION_NONE + entry_infos.get(CONF_AUTO_REGULATION_MODE) + if entry_infos.get(CONF_AUTO_REGULATION_MODE) is not None + else CONF_AUTO_REGULATION_NONE ) - self._auto_regulation_dtemp = entry_infos.get(CONF_AUTO_REGULATION_DTEMP) if entry_infos.get(CONF_AUTO_REGULATION_DTEMP) is not None else 0.5 - self._auto_regulation_period_min = entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN) if entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN) is not None else 5 + self._auto_regulation_dtemp = ( + entry_infos.get(CONF_AUTO_REGULATION_DTEMP) + if entry_infos.get(CONF_AUTO_REGULATION_DTEMP) is not None + else 0.5 + ) + self._auto_regulation_period_min = ( + entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN) + if entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN) is not None + else 5 + ) def choose_auto_regulation_mode(self, auto_regulation_mode): - """ Choose or change the regulation mode""" + """Choose or change the regulation mode""" self._auto_regulation_mode = auto_regulation_mode if self._auto_regulation_mode == CONF_AUTO_REGULATION_LIGHT: self._regulation_algo = PITemperatureRegulator( @@ -159,7 +201,8 @@ def choose_auto_regulation_mode(self, auto_regulation_mode): RegulationParamLight.k_ext, RegulationParamLight.offset_max, RegulationParamLight.stabilization_threshold, - RegulationParamLight.accumulated_error_threshold) + RegulationParamLight.accumulated_error_threshold, + ) elif self._auto_regulation_mode == CONF_AUTO_REGULATION_MEDIUM: self._regulation_algo = PITemperatureRegulator( self.target_temperature, @@ -168,7 +211,8 @@ def choose_auto_regulation_mode(self, auto_regulation_mode): RegulationParamMedium.k_ext, RegulationParamMedium.offset_max, RegulationParamMedium.stabilization_threshold, - RegulationParamMedium.accumulated_error_threshold) + RegulationParamMedium.accumulated_error_threshold, + ) elif self._auto_regulation_mode == CONF_AUTO_REGULATION_STRONG: self._regulation_algo = PITemperatureRegulator( self.target_temperature, @@ -177,7 +221,8 @@ def choose_auto_regulation_mode(self, auto_regulation_mode): RegulationParamStrong.k_ext, RegulationParamStrong.offset_max, RegulationParamStrong.stabilization_threshold, - RegulationParamStrong.accumulated_error_threshold) + RegulationParamStrong.accumulated_error_threshold, + ) elif self._auto_regulation_mode == CONF_AUTO_REGULATION_SLOW: self._regulation_algo = PITemperatureRegulator( self.target_temperature, @@ -186,11 +231,13 @@ def choose_auto_regulation_mode(self, auto_regulation_mode): RegulationParamSlow.k_ext, RegulationParamSlow.offset_max, RegulationParamSlow.stabilization_threshold, - RegulationParamSlow.accumulated_error_threshold) + RegulationParamSlow.accumulated_error_threshold, + ) else: # A default empty algo (which does nothing) self._regulation_algo = PITemperatureRegulator( - self.target_temperature, 0, 0, 0, 0, 0.1, 0) + self.target_temperature, 0, 0, 0, 0, 0.1, 0 + ) @overrides async def async_added_to_hass(self): @@ -219,27 +266,37 @@ async def async_added_to_hass(self): @overrides def update_custom_attributes(self): - """ Custom attributes """ + """Custom attributes""" super().update_custom_attributes() self._attr_extra_state_attributes["is_over_climate"] = self.is_over_climate - self._attr_extra_state_attributes["start_hvac_action_date"] = ( - self._underlying_climate_start_hvac_action_date) - self._attr_extra_state_attributes["underlying_climate_0"] = ( - self._underlyings[0].entity_id) + self._attr_extra_state_attributes[ + "start_hvac_action_date" + ] = self._underlying_climate_start_hvac_action_date + self._attr_extra_state_attributes["underlying_climate_0"] = self._underlyings[ + 0 + ].entity_id self._attr_extra_state_attributes["underlying_climate_1"] = ( - self._underlyings[1].entity_id if len(self._underlyings) > 1 else None - ) + self._underlyings[1].entity_id if len(self._underlyings) > 1 else None + ) self._attr_extra_state_attributes["underlying_climate_2"] = ( - self._underlyings[2].entity_id if len(self._underlyings) > 2 else None - ) + self._underlyings[2].entity_id if len(self._underlyings) > 2 else None + ) self._attr_extra_state_attributes["underlying_climate_3"] = ( - self._underlyings[3].entity_id if len(self._underlyings) > 3 else None - ) + self._underlyings[3].entity_id if len(self._underlyings) > 3 else None + ) if self.is_regulated: - self._attr_extra_state_attributes["regulated_target_temperature"] = self._regulated_target_temp - self._attr_extra_state_attributes["regulation_accumulated_error"] = self._regulation_algo.accumulated_error + self._attr_extra_state_attributes["is_regulated"] = self.is_regulated + self._attr_extra_state_attributes[ + "regulated_target_temperature" + ] = self._regulated_target_temp + self._attr_extra_state_attributes[ + "auto_regulation_mode" + ] = self.auto_regulation_mode + self._attr_extra_state_attributes[ + "regulation_accumulated_error" + ] = self._regulation_algo.accumulated_error self.async_write_ha_state() _LOGGER.debug( @@ -473,17 +530,17 @@ async def async_control_heating(self, force=False, _=None): @property def auto_regulation_mode(self): - """ Get the regulation mode """ + """Get the regulation mode""" return self._auto_regulation_mode @property def regulated_target_temp(self): - """ Get the regulated target temperature """ + """Get the regulated target temperature""" return self._regulated_target_temp @property def is_regulated(self): - """ Check if the ThermostatOverClimate is regulated """ + """Check if the ThermostatOverClimate is regulated""" return self.auto_regulation_mode != CONF_AUTO_REGULATION_NONE @property @@ -668,7 +725,11 @@ async def service_set_auto_regulation_mode(self, auto_regulation_mode): target: entity_id: climate.thermostat_1 """ - _LOGGER.info("%s - Calling service_set_auto_regulation_mode, auto_regulation_mode: %s", self, auto_regulation_mode) + _LOGGER.info( + "%s - Calling service_set_auto_regulation_mode, auto_regulation_mode: %s", + self, + auto_regulation_mode, + ) if auto_regulation_mode == "None": self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_NONE) elif auto_regulation_mode == "Light": diff --git a/custom_components/versatile_thermostat/thermostat_switch.py b/custom_components/versatile_thermostat/thermostat_switch.py index 9de4bb53..43c3e594 100644 --- a/custom_components/versatile_thermostat/thermostat_switch.py +++ b/custom_components/versatile_thermostat/thermostat_switch.py @@ -40,6 +40,7 @@ class ThermostatOverSwitch(BaseThermostat): "function", "tpi_coef_int", "tpi_coef_ext", + "power_percent", } ) ) @@ -144,6 +145,7 @@ def update_custom_attributes(self): self._attr_extra_state_attributes[ "on_percent" ] = self._prop_algorithm.on_percent + self._attr_extra_state_attributes["power_percent"] = self.power_percent self._attr_extra_state_attributes[ "on_time_sec" ] = self._prop_algorithm.on_time_sec