From 38c4b067b1bc469b5f1b3c71fc44914b7b4757ef Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Thu, 31 Oct 2024 21:30:44 +0000 Subject: [PATCH] FIX too much start/stop --- .../auto_start_stop_algorithm.py | 45 +++--- .../versatile_thermostat/binary_sensor.py | 2 +- .../versatile_thermostat/const.py | 1 + .../versatile_thermostat/switch.py | 103 ++++++++++++++ .../thermostat_climate.py | 128 +++++++++++------- 5 files changed, 214 insertions(+), 65 deletions(-) create mode 100644 custom_components/versatile_thermostat/switch.py diff --git a/custom_components/versatile_thermostat/auto_start_stop_algorithm.py b/custom_components/versatile_thermostat/auto_start_stop_algorithm.py index 6249580..749c00b 100644 --- a/custom_components/versatile_thermostat/auto_start_stop_algorithm.py +++ b/custom_components/versatile_thermostat/auto_start_stop_algorithm.py @@ -90,6 +90,17 @@ class AutoStartStopDetectionAlgorithm: ) return AUTO_START_STOP_ACTION_NOTHING + _LOGGER.debug( + "%s - calculate_action: hvac_mode=%s, saved_hvac_mode=%s, target_temp=%s, current_temp=%s, slope_min=%s at %s", + self, + hvac_mode, + saved_hvac_mode, + target_temp, + current_temp, + slope_min, + now, + ) + if ( hvac_mode is None or target_temp is None @@ -102,24 +113,13 @@ class AutoStartStopDetectionAlgorithm: ) return AUTO_START_STOP_ACTION_NOTHING - _LOGGER.debug( - "%s - calculate_action: hvac_mode=%s, saved_hvac_mode=%s, target_temp=%s, current_temp=%s, slope_min=%s at %s", - self, - hvac_mode, - saved_hvac_mode, - target_temp, - current_temp, - slope_min, - now, - ) - # Calculate the error factor (P) error = target_temp - current_temp # reduce the error considering the dt between the last measurement if self._last_calculation_date is not None: dtmin = (now - self._last_calculation_date).total_seconds() / CYCLE_SEC - # ignore two calls too near (< 2,5 min) + # ignore two calls too near (< 1 min) if dtmin <= 0.5: _LOGGER.debug( "%s - new calculation of auto_start_stop (%s) is too near of the last one (%s). Forget it", @@ -147,26 +147,27 @@ class AutoStartStopDetectionAlgorithm: # Check to turn-off # When we hit the threshold, that mean we can turn off if hvac_mode == HVACMode.HEAT: - if self._accumulated_error <= -self._error_threshold: + if self._accumulated_error <= -self._error_threshold and slope_min >= 0: _LOGGER.info( "%s - We need to stop, there is no need for heating for a long time.", + self, ) return AUTO_START_STOP_ACTION_OFF else: - _LOGGER.debug( - "%s - nothing to do, we are heating", - ) + _LOGGER.debug("%s - nothing to do, we are heating", self) return AUTO_START_STOP_ACTION_NOTHING if hvac_mode == HVACMode.COOL: - if self._accumulated_error >= self._error_threshold: + if self._accumulated_error >= self._error_threshold and slope_min <= 0: _LOGGER.info( "%s - We need to stop, there is no need for cooling for a long time.", + self, ) return AUTO_START_STOP_ACTION_OFF else: _LOGGER.debug( "%s - nothing to do, we are cooling", + self, ) return AUTO_START_STOP_ACTION_NOTHING @@ -175,11 +176,13 @@ class AutoStartStopDetectionAlgorithm: if current_temp + slope_min * self._dt <= target_temp: _LOGGER.info( "%s - We need to start, because it will be time to heat", + self, ) return AUTO_START_STOP_ACTION_ON else: _LOGGER.debug( "%s - nothing to do, we don't need to heat soon", + self, ) return AUTO_START_STOP_ACTION_NOTHING @@ -187,16 +190,19 @@ class AutoStartStopDetectionAlgorithm: if current_temp + slope_min * self._dt >= target_temp: _LOGGER.info( "%s - We need to start, because it will be time to cool", + self, ) return AUTO_START_STOP_ACTION_ON else: _LOGGER.debug( "%s - nothing to do, we don't need to cool soon", + self, ) return AUTO_START_STOP_ACTION_NOTHING _LOGGER.debug( "%s - nothing to do, no conditions applied", + self, ) return AUTO_START_STOP_ACTION_NOTHING @@ -214,6 +220,11 @@ class AutoStartStopDetectionAlgorithm: """Get the accumulated error value""" return self._accumulated_error + @property + def accumulated_error_threshold(self) -> float: + """Get the accumulated error threshold value""" + return self._error_threshold + @property def level(self) -> TYPE_AUTO_START_STOP_LEVELS: """Get the level value""" diff --git a/custom_components/versatile_thermostat/binary_sensor.py b/custom_components/versatile_thermostat/binary_sensor.py index 73561b2..edd491d 100644 --- a/custom_components/versatile_thermostat/binary_sensor.py +++ b/custom_components/versatile_thermostat/binary_sensor.py @@ -100,7 +100,7 @@ class SecurityBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity): entry_infos, ) -> None: """Initialize the SecurityState Binary sensor""" - super().__init__(hass, unique_id, entry_infos.get(CONF_NAME)) + super().__init__(hass, unique_id, name) self._attr_name = "Security state" self._attr_unique_id = f"{self._device_name}_security_state" self._attr_is_on = False diff --git a/custom_components/versatile_thermostat/const.py b/custom_components/versatile_thermostat/const.py index b862636..c16bbe9 100644 --- a/custom_components/versatile_thermostat/const.py +++ b/custom_components/versatile_thermostat/const.py @@ -52,6 +52,7 @@ PLATFORMS: list[Platform] = [ # Number should be after CLIMATE Platform.NUMBER, Platform.BINARY_SENSOR, + Platform.SWITCH, ] CONF_HEATER = "heater_entity_id" diff --git a/custom_components/versatile_thermostat/switch.py b/custom_components/versatile_thermostat/switch.py new file mode 100644 index 0000000..7b8f617 --- /dev/null +++ b/custom_components/versatile_thermostat/switch.py @@ -0,0 +1,103 @@ +## pylint: disable=unused-argument + +""" Implements the VersatileThermostat select component """ +import logging +from typing import Any + +from homeassistant.core import HomeAssistant, callback + +from homeassistant.components.switch import SwitchEntity + +from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .commons import VersatileThermostatBaseEntity + +from .const import * # pylint: disable=unused-wildcard-import,wildcard-import + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the VersatileThermostat switches with config flow.""" + _LOGGER.debug( + "Calling async_setup_entry entry=%s, data=%s", entry.entry_id, entry.data + ) + + unique_id = entry.entry_id + name = entry.data.get(CONF_NAME) + vt_type = entry.data.get(CONF_THERMOSTAT_TYPE) + auto_start_stop_feature = entry.data.get(CONF_USE_AUTO_START_STOP_FEATURE) + + if vt_type == CONF_THERMOSTAT_CLIMATE and auto_start_stop_feature is True: + # Creates a switch to enable the auto-start/stop + enable_entity = AutoStartStopEnable(hass, unique_id, name, entry) + async_add_entities([enable_entity], True) + + +class AutoStartStopEnable(VersatileThermostatBaseEntity, SwitchEntity, RestoreEntity): + """The that enables the ManagedDevice optimisation with""" + + def __init__( + self, hass: HomeAssistant, unique_id: str, name: str, entry_infos: ConfigEntry + ): + super().__init__(hass, unique_id, name) + self._attr_name = "Enable auto start/stop" + self._attr_unique_id = f"{self._device_name}_enbale_auto_start_stop" + self._default_value = ( + entry_infos.data.get(CONF_AUTO_START_STOP_LEVEL) + != AUTO_START_STOP_LEVEL_NONE + ) + self._attr_is_on = self._default_value + + @property + def icon(self) -> str | None: + """The icon""" + return "mdi:power-settings" + + async def async_added_to_hass(self): + await super().async_added_to_hass() + + # Récupérer le dernier état sauvegardé de l'entité + last_state = await self.async_get_last_state() + + # Si l'état précédent existe, vous pouvez l'utiliser + if last_state is not None: + self._attr_is_on = last_state.state == "on" + else: + # If no previous state set it to false by default + self._attr_is_on = self._default_value + + self.update_my_state_and_vtherm() + + def update_my_state_and_vtherm(self): + """Update the auto_start_stop_enable flag in my VTherm""" + self.async_write_ha_state() + if self.my_climate is not None: + self.my_climate.set_auto_start_stop_enable(self._attr_is_on) + + @callback + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + self.turn_on() + + @callback + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + self.turn_off() + + @overrides + def turn_off(self, **kwargs: Any): + self._attr_is_on = False + self.update_my_state_and_vtherm() + + @overrides + def turn_on(self, **kwargs: Any): + self._attr_is_on = True + self.update_my_state_and_vtherm() diff --git a/custom_components/versatile_thermostat/thermostat_climate.py b/custom_components/versatile_thermostat/thermostat_climate.py index 5612f19..5e5ea59 100644 --- a/custom_components/versatile_thermostat/thermostat_climate.py +++ b/custom_components/versatile_thermostat/thermostat_climate.py @@ -91,6 +91,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]): _auto_deactivated_fan_mode: str | None = None _auto_start_stop_level: TYPE_AUTO_START_STOP_LEVELS = AUTO_START_STOP_LEVEL_NONE _auto_start_stop_algo: AutoStartStopDetectionAlgorithm | None = None + _is_auto_start_stop_enabled: bool = False _entity_component_unrecorded_attributes = ( BaseThermostat._entity_component_unrecorded_attributes.union( @@ -110,8 +111,10 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]): "auto_deactivated_fan_mode", "auto_regulation_use_device_temp", "auto_start_stop_level", - "auto_start_stop_dtemp", "auto_start_stop_dtmin", + "auto_start_stop_enable", + "auto_start_stop_accumulated_error", + "auto_start_stop_accumulated_error_threshold", } ) ) @@ -572,12 +575,23 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]): self.auto_regulation_use_device_temp ) + self._attr_extra_state_attributes["auto_start_stop_enable"] = ( + self.auto_start_stop_enable + ) + self._attr_extra_state_attributes["auto_start_stop_level"] = ( self._auto_start_stop_algo.level ) self._attr_extra_state_attributes["auto_start_stop_dtmin"] = ( self._auto_start_stop_algo.dt_min ) + self._attr_extra_state_attributes["auto_start_stop_accumulated_error"] = ( + self._auto_start_stop_algo.accumulated_error + ) + + self._attr_extra_state_attributes[ + "auto_start_stop_accumulated_error_threshold" + ] = self._auto_start_stop_algo.accumulated_error_threshold self.async_write_ha_state() _LOGGER.debug( @@ -898,56 +912,67 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]): ret = await super().async_control_heating(force, _) # Check if we need to auto start/stop the Vtherm - action = self._auto_start_stop_algo.calculate_action( - self.hvac_mode, - self._saved_hvac_mode, - self.target_temperature, - self.current_temperature, - self._window_auto_algo.last_slope, - self.now - ) - _LOGGER.debug("%s - auto_start_stop action is %s", self, action) - if action == AUTO_START_STOP_ACTION_OFF: - _LOGGER.info( - "%s - Turning OFF the Vtherm due to auto-start-stop conditions", self + if ( + self.auto_start_stop_enable + and self._window_auto_algo.last_slope is not None + ): + action = self._auto_start_stop_algo.calculate_action( + self.hvac_mode, + self._saved_hvac_mode, + self.target_temperature, + self.current_temperature, + self._window_auto_algo.last_slope / 60, # to have the slope in °/min + self.now, ) - await self.async_turn_off() + _LOGGER.debug("%s - auto_start_stop action is %s", self, action) + if action == AUTO_START_STOP_ACTION_OFF: + _LOGGER.info( + "%s - Turning OFF the Vtherm due to auto-start-stop conditions", + self, + ) + await self.async_turn_off() - # Send an event - self.send_event( - event_type=EventType.AUTO_START_STOP_EVENT, - data={ - "type": "stop", - "cause": "Auto stop conditions reached", - "hvac_mode": self.hvac_mode, - "saved_hvac_mode": self._saved_hvac_mode, - "target_temperature": self.target_temperature, - "current_temperature": self.current_temperature, - "temperature_slope": self._window_auto_algo.last_slope, - }, - ) + # Send an event + self.send_event( + event_type=EventType.AUTO_START_STOP_EVENT, + data={ + "type": "stop", + "name:": self.name, + "cause": "Auto stop conditions reached", + "hvac_mode": self.hvac_mode, + "saved_hvac_mode": self._saved_hvac_mode, + "target_temperature": self.target_temperature, + "current_temperature": self.current_temperature, + "temperature_slope": self._window_auto_algo.last_slope, + }, + ) - # Stop here - return ret - elif action == AUTO_START_STOP_ACTION_ON: - _LOGGER.info( - "%s - Turning ON the Vtherm due to auto-start-stop conditions", self - ) - await self.async_turn_on() + # Stop here + return ret + elif action == AUTO_START_STOP_ACTION_ON: + _LOGGER.info( + "%s - Turning ON the Vtherm due to auto-start-stop conditions", self + ) + await self.async_turn_on() - # Send an event - self.send_event( - event_type=EventType.AUTO_START_STOP_EVENT, - data={ - "type": "start", - "cause": "Auto start conditions reached", - "hvac_mode": self.hvac_mode, - "saved_hvac_mode": self._saved_hvac_mode, - "target_temperature": self.target_temperature, - "current_temperature": self.current_temperature, - "temperature_slope": self._window_auto_algo.last_slope, - }, - ) + # Send an event + self.send_event( + event_type=EventType.AUTO_START_STOP_EVENT, + data={ + "type": "start", + "name:": self.name, + "cause": "Auto start conditions reached", + "hvac_mode": self.hvac_mode, + "saved_hvac_mode": self._saved_hvac_mode, + "target_temperature": self.target_temperature, + "current_temperature": self.current_temperature, + "temperature_slope": self._window_auto_algo.last_slope, + }, + ) + + self.update_custom_attributes() + else: + _LOGGER.debug("%s - auto start/stop is disabled") # Continue the normal async_control_heating @@ -959,6 +984,10 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]): return ret + def set_auto_start_stop_enable(self, is_enabled: bool): + """Enable/Disable the auto-start/stop feature""" + self._is_auto_start_stop_enabled = is_enabled + @property def auto_regulation_mode(self) -> str | None: """Get the regulation mode""" @@ -1110,6 +1139,11 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]): """Return the auto start/stop level.""" return self._auto_start_stop_level + @property + def auto_start_stop_enable(self) -> bool: + """Returns the auto_start_stop_enable""" + return self._is_auto_start_stop_enabled + @overrides def init_underlyings(self): """Init the underlyings if not already done"""