diff --git a/custom_components/versatile_thermostat/base_thermostat.py b/custom_components/versatile_thermostat/base_thermostat.py index 51f793e..2e25ec4 100644 --- a/custom_components/versatile_thermostat/base_thermostat.py +++ b/custom_components/versatile_thermostat/base_thermostat.py @@ -125,6 +125,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): .union(FeaturePresenceManager.unrecorded_attributes) .union(FeaturePowerManager.unrecorded_attributes) .union(FeatureMotionManager.unrecorded_attributes) + .union(FeatureWindowManager.unrecorded_attributes) ) def __init__( @@ -216,6 +217,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): # Instanciate all features manager self._managers: list[BaseFeatureManager] = [] + self._presence_manager: FeaturePresenceManager = FeaturePresenceManager( self, hass ) @@ -1299,7 +1301,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN): return - # TODO ce code avec du dearm est curieux. A voir après refacto dearm_window_auto = await self._async_update_temp(new_state) self.recalculate() await self.async_control_heating(force=False) @@ -1866,7 +1867,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): ), } - self._presence_manager.add_custom_attributes(self._attr_extra_state_attributes) + for manager in self._managers: + manager.add_custom_attributes(self._attr_extra_state_attributes) @overrides def async_write_ha_state(self): diff --git a/custom_components/versatile_thermostat/feature_auto_start_stop_manager.py b/custom_components/versatile_thermostat/feature_auto_start_stop_manager.py new file mode 100644 index 0000000..b601745 --- /dev/null +++ b/custom_components/versatile_thermostat/feature_auto_start_stop_manager.py @@ -0,0 +1,236 @@ +""" Implements the Auto-start/stop Feature Manager """ + +# pylint: disable=line-too-long + +import logging +from typing import Any + +from homeassistant.core import ( + HomeAssistant, +) +from homeassistant.components.climate import HVACMode + +from .const import * # pylint: disable=wildcard-import, unused-wildcard-import +from .commons import ConfigData + +from .base_manager import BaseFeatureManager + +from .auto_start_stop_algorithm import ( + AutoStartStopDetectionAlgorithm, + AUTO_START_STOP_ACTION_OFF, + AUTO_START_STOP_ACTION_ON, +) + + +_LOGGER = logging.getLogger(__name__) + + +class FeatureAutoStartStopManager(BaseFeatureManager): + """The implementation of the AutoStartStop feature""" + + unrecorded_attributes = frozenset( + { + "auto_start_stop_level", + "auto_start_stop_dtmin", + "auto_start_stop_enable", + "auto_start_stop_accumulated_error", + "auto_start_stop_accumulated_error_threshold", + "auto_start_stop_last_switch_date", + } + ) + + def __init__(self, vtherm: Any, hass: HomeAssistant): + """Init of a featureManager""" + super().__init__(vtherm, hass) + + self._auto_start_stop_level: TYPE_AUTO_START_STOP_LEVELS = ( + AUTO_START_STOP_LEVEL_NONE + ) + self._auto_start_stop_algo: AutoStartStopDetectionAlgorithm | None = None + self._is_configured: bool = False + self._is_auto_start_stop_enabled: bool = False + + @overrides + def post_init(self, entry_infos: ConfigData): + """Reinit of the manager""" + + use_auto_start_stop = entry_infos.get(CONF_USE_AUTO_START_STOP_FEATURE, False) + if use_auto_start_stop: + self._auto_start_stop_level = ( + entry_infos.get(CONF_AUTO_START_STOP_LEVEL, None) + or AUTO_START_STOP_LEVEL_NONE + ) + self._is_configured = True + else: + self._auto_start_stop_level = AUTO_START_STOP_LEVEL_NONE + self._is_configured = False + + # Instanciate the auto start stop algo + self._auto_start_stop_algo = AutoStartStopDetectionAlgorithm( + self._auto_start_stop_level, self.name + ) + + @overrides + def start_listening(self): + """Start listening the underlying entity""" + + @overrides + def stop_listening(self): + """Stop listening and remove the eventual timer still running""" + + @overrides + async def refresh_state(self) -> bool: + """Check the auto-start-stop and an eventual action + Return False if we should stop the control_heating method""" + + if not self._is_configured or not self._is_auto_start_stop_enabled: + _LOGGER.debug("%s - auto start/stop is disabled (or not configured)", self) + return True + + slope = ( + self._vtherm.last_temperature_slope or 0 + ) / 60 # to have the slope in °/min + action = self._auto_start_stop_algo.calculate_action( + self._vtherm.hvac_mode, + self._vtherm.saved_hvac_mode, + self._vtherm.target_temperature, + self._vtherm.current_temperature, + slope, + self._vtherm.now, + ) + _LOGGER.debug("%s - auto_start_stop action is %s", self, action) + if action == AUTO_START_STOP_ACTION_OFF and self._vtherm.is_on: + _LOGGER.info( + "%s - Turning OFF the Vtherm due to auto-start-stop conditions", + self, + ) + self._vtherm.set_hvac_off_reason(HVAC_OFF_REASON_AUTO_START_STOP) + await self._vtherm.async_turn_off() + + # Send an event + self._vtherm.send_event( + event_type=EventType.AUTO_START_STOP_EVENT, + data={ + "type": "stop", + "name": self.name, + "cause": "Auto stop conditions reached", + "hvac_mode": self._vtherm.hvac_mode, + "saved_hvac_mode": self._vtherm.saved_hvac_mode, + "target_temperature": self._vtherm.target_temperature, + "current_temperature": self._vtherm.current_temperature, + "temperature_slope": round(slope, 3), + "accumulated_error": self._auto_start_stop_algo.accumulated_error, + "accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold, + }, + ) + + # Stop here + return False + elif ( + action == AUTO_START_STOP_ACTION_ON + and self._vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP + ): + _LOGGER.info( + "%s - Turning ON the Vtherm due to auto-start-stop conditions", self + ) + await self._vtherm.async_turn_on() + + # Send an event + self._vtherm.send_event( + event_type=EventType.AUTO_START_STOP_EVENT, + data={ + "type": "start", + "name": self.name, + "cause": "Auto start conditions reached", + "hvac_mode": self._vtherm.hvac_mode, + "saved_hvac_mode": self._vtherm.saved_hvac_mode, + "target_temperature": self._vtherm.target_temperature, + "current_temperature": self._vtherm.current_temperature, + "temperature_slope": round(slope, 3), + "accumulated_error": self._auto_start_stop_algo.accumulated_error, + "accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold, + }, + ) + + self._vtherm.update_custom_attributes() + + return True + + 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 + if ( + self._vtherm.hvac_mode == HVACMode.OFF + and self._vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP + ): + _LOGGER.debug( + "%s - the vtherm is off cause auto-start/stop and enable have been set to false -> starts the VTherm" + ) + self.hass.create_task(self._vtherm.async_turn_on()) + + # Send an event + self._vtherm.send_event( + event_type=EventType.AUTO_START_STOP_EVENT, + data={ + "type": "start", + "name": self.name, + "cause": "Auto start stop disabled", + "hvac_mode": self._vtherm.hvac_mode, + "saved_hvac_mode": self._vtherm.saved_hvac_mode, + "target_temperature": self._vtherm.target_temperature, + "current_temperature": self._vtherm.current_temperature, + "temperature_slope": round( + self._vtherm.last_temperature_slope or 0, 3 + ), + "accumulated_error": self._auto_start_stop_algo.accumulated_error, + "accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold, + }, + ) + + self._vtherm.update_custom_attributes() + + def add_custom_attributes(self, extra_state_attributes: dict[str, Any]): + """Add some custom attributes""" + extra_state_attributes.update( + { + "is_auto_start_stop_configured": self.is_configured, + } + ) + if self.is_configured: + extra_state_attributes.update( + { + "auto_start_stop_enable": self.auto_start_stop_enable, + "auto_start_stop_level": self._auto_start_stop_algo.level, + "auto_start_stop_dtmin": self._auto_start_stop_algo.dt_min, + "auto_start_stop_accumulated_error": self._auto_start_stop_algo.accumulated_error, + "auto_start_stop_accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold, + "auto_start_stop_last_switch_date": self._auto_start_stop_algo.last_switch_date, + } + ) + + @overrides + @property + def is_configured(self) -> bool: + """Return True of the window feature is configured""" + return self._is_configured + + @property + def auto_start_stop_level(self) -> TYPE_AUTO_START_STOP_LEVELS: + """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 + + @property + def is_auto_stopped(self) -> bool: + """Returns the is vtherm is stopped and reason is AUTO_START_STOP""" + return ( + self._vtherm.hvac_mode == HVACMode.OFF + and self._vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP + ) + + def __str__(self): + return f"AutoStartStopManager-{self.name}" diff --git a/custom_components/versatile_thermostat/switch.py b/custom_components/versatile_thermostat/switch.py index 6b49705..9801f98 100644 --- a/custom_components/versatile_thermostat/switch.py +++ b/custom_components/versatile_thermostat/switch.py @@ -84,8 +84,13 @@ class AutoStartStopEnable(VersatileThermostatBaseEntity, SwitchEntity, RestoreEn 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) + if ( + self.my_climate is not None + and self.my_climate.auto_start_stop_manager is not None + ): + self.my_climate.auto_start_stop_manager.set_auto_start_stop_enable( + self._attr_is_on + ) @callback async def async_turn_on(self, **kwargs: Any) -> None: diff --git a/custom_components/versatile_thermostat/thermostat_climate.py b/custom_components/versatile_thermostat/thermostat_climate.py index 373c4ea..5473f44 100644 --- a/custom_components/versatile_thermostat/thermostat_climate.py +++ b/custom_components/versatile_thermostat/thermostat_climate.py @@ -24,11 +24,7 @@ from .const import * # pylint: disable=wildcard-import, unused-wildcard-import from .vtherm_api import VersatileThermostatAPI from .underlyings import UnderlyingClimate -from .auto_start_stop_algorithm import ( - AutoStartStopDetectionAlgorithm, - AUTO_START_STOP_ACTION_OFF, - AUTO_START_STOP_ACTION_ON, -) +from .feature_auto_start_stop_manager import FeatureAutoStartStopManager _LOGGER = logging.getLogger(__name__) @@ -55,15 +51,9 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]): "auto_activated_fan_mode", "auto_deactivated_fan_mode", "auto_regulation_use_device_temp", - "auto_start_stop_level", - "auto_start_stop_dtmin", - "auto_start_stop_enable", - "auto_start_stop_accumulated_error", - "auto_start_stop_accumulated_error_threshold", - "auto_start_stop_last_switch_date", "follow_underlying_temp_change", } - ) + ).union(FeatureAutoStartStopManager.unrecorded_attributes) ) def __init__( @@ -83,11 +73,6 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]): # The fan_mode name depending of the current_mode self._auto_activated_fan_mode: str | None = None self._auto_deactivated_fan_mode: str | None = None - self._auto_start_stop_level: TYPE_AUTO_START_STOP_LEVELS = ( - AUTO_START_STOP_LEVEL_NONE - ) - self._auto_start_stop_algo: AutoStartStopDetectionAlgorithm | None = None - self._is_auto_start_stop_enabled: bool = False self._follow_underlying_temp_change: bool = False self._last_regulation_change = None # NowClass.get_now(hass) @@ -99,6 +84,12 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]): def post_init(self, config_entry: ConfigData): """Initialize the Thermostat""" + self._auto_start_stop_manager: FeatureAutoStartStopManager = ( + FeatureAutoStartStopManager(self, self._hass) + ) + + self.register_manager(self._auto_start_stop_manager) + super().post_init(config_entry) for climate in config_entry.get(CONF_UNDERLYING_LIST): @@ -136,19 +127,6 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]): CONF_AUTO_REGULATION_USE_DEVICE_TEMP, False ) - use_auto_start_stop = config_entry.get(CONF_USE_AUTO_START_STOP_FEATURE, False) - if use_auto_start_stop: - self._auto_start_stop_level = config_entry.get( - CONF_AUTO_START_STOP_LEVEL, AUTO_START_STOP_LEVEL_NONE - ) - else: - self._auto_start_stop_level = AUTO_START_STOP_LEVEL_NONE - - # Instanciate the auto start stop algo - self._auto_start_stop_algo = AutoStartStopDetectionAlgorithm( - self._auto_start_stop_level, self.name - ) - @property def is_over_climate(self) -> bool: """True if the Thermostat is over_climate""" @@ -538,28 +516,6 @@ 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._attr_extra_state_attributes["auto_start_stop_last_switch_date"] = ( - self._auto_start_stop_algo.last_switch_date - ) - self._attr_extra_state_attributes["follow_underlying_temp_change"] = ( self._follow_underlying_temp_change ) @@ -899,90 +855,17 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]): await end_climate_changed(changes) - async def check_auto_start_stop(self): - """Check the auto-start-stop and an eventual action - Return False if we should stop the control_heating method""" - slope = (self.last_temperature_slope or 0) / 60 # to have the slope in °/min - action = self._auto_start_stop_algo.calculate_action( - self.hvac_mode, - self._saved_hvac_mode, - self.target_temperature, - self.current_temperature, - slope, - self.now, - ) - _LOGGER.debug("%s - auto_start_stop action is %s", self, action) - if action == AUTO_START_STOP_ACTION_OFF and self.is_on: - _LOGGER.info( - "%s - Turning OFF the Vtherm due to auto-start-stop conditions", - self, - ) - self.set_hvac_off_reason(HVAC_OFF_REASON_AUTO_START_STOP) - await self.async_turn_off() - - # 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": round(slope, 3), - "accumulated_error": self._auto_start_stop_algo.accumulated_error, - "accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold, - }, - ) - - # Stop here - return False - elif ( - action == AUTO_START_STOP_ACTION_ON - and self.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP - ): - _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", - "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": round(slope, 3), - "accumulated_error": self._auto_start_stop_algo.accumulated_error, - "accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold, - }, - ) - - self.update_custom_attributes() - - return True - @overrides async def async_control_heating(self, force=False, _=None) -> bool: """The main function used to run the calculation at each cycle""" ret = await super().async_control_heating(force, _) # Check if we need to auto start/stop the Vtherm - if self.auto_start_stop_enable: - continu = await self.check_auto_start_stop() - if not continu: - return ret - else: - _LOGGER.debug("%s - auto start/stop is disabled", self) + continu = await self.auto_start_stop_manager.refresh_state() + if not continu: + return ret - # Continue the normal async_control_heating + # Continue the normal async_control_heating # Send the regulated temperature to the underlyings await self._send_regulated_temperature() @@ -992,37 +875,6 @@ 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 - if ( - self.hvac_mode == HVACMode.OFF - and self.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP - ): - _LOGGER.debug( - "%s - the vtherm is off cause auto-start/stop and enable have been set to false -> starts the VTherm" - ) - self.hass.create_task(self.async_turn_on()) - - # Send an event - self.send_event( - event_type=EventType.AUTO_START_STOP_EVENT, - data={ - "type": "start", - "name": self.name, - "cause": "Auto start stop disabled", - "hvac_mode": self.hvac_mode, - "saved_hvac_mode": self._saved_hvac_mode, - "target_temperature": self.target_temperature, - "current_temperature": self.current_temperature, - "temperature_slope": round(self.last_temperature_slope or 0, 3), - "accumulated_error": self._auto_start_stop_algo.accumulated_error, - "accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold, - }, - ) - - self.update_custom_attributes() - def set_follow_underlying_temp_change(self, follow: bool): """Set the flaf follow the underlying temperature changes""" self._follow_underlying_temp_change = follow @@ -1173,21 +1025,16 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]): return False return True - @property - def auto_start_stop_level(self) -> TYPE_AUTO_START_STOP_LEVELS: - """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 - @property def follow_underlying_temp_change(self) -> bool: """Get the follow underlying temp change flag""" return self._follow_underlying_temp_change + @property + def auto_start_stop_manager(self) -> FeatureAutoStartStopManager: + """Return the auto-start-stop Manager""" + return self._auto_start_stop_manager + @overrides def init_underlyings(self): """Init the underlyings if not already done""" diff --git a/tests/test_auto_start_stop.py b/tests/test_auto_start_stop.py index d52ecba..4d5e274 100644 --- a/tests/test_auto_start_stop.py +++ b/tests/test_auto_start_stop.py @@ -1,4 +1,4 @@ -# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, unused-variable +# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, unused-variable, too-many-lines """ Test the Auto Start Stop algorithm management """ from datetime import datetime, timedelta @@ -363,15 +363,14 @@ async def test_auto_start_stop_none_vtherm( # Initialize all temps await set_all_climate_preset_temp(hass, vtherm, temps, "overclimate") # Check correct initialization of auto_start_stop attributes - assert ( - vtherm._attr_extra_state_attributes["auto_start_stop_level"] - == AUTO_START_STOP_LEVEL_NONE - ) - - assert vtherm._attr_extra_state_attributes["auto_start_stop_dtmin"] is None + assert vtherm._attr_extra_state_attributes.get("auto_start_stop_level") is None + assert vtherm._attr_extra_state_attributes.get("auto_start_stop_dtmin") is None # 1. Vtherm auto-start/stop should be in NONE mode - assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_NONE + assert ( + vtherm.auto_start_stop_manager.auto_start_stop_level + == AUTO_START_STOP_LEVEL_NONE + ) # 2. We should not find any switch Enable entity assert ( @@ -464,7 +463,10 @@ async def test_auto_start_stop_medium_heat_vtherm( assert vtherm._attr_extra_state_attributes["auto_start_stop_dtmin"] == 15 # 1. Vtherm auto-start/stop should be in MEDIUM mode and an enable entity should exists - assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_MEDIUM + assert ( + vtherm.auto_start_stop_manager.auto_start_stop_level + == AUTO_START_STOP_LEVEL_MEDIUM + ) enable_entity = search_entity( hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN ) @@ -488,7 +490,7 @@ async def test_auto_start_stop_medium_heat_vtherm( # 3. Set current temperature to 19 5 min later now = now + timedelta(minutes=5) # reset accumulated error (only for testing) - vtherm._auto_start_stop_algo._accumulated_error = 0 + vtherm.auto_start_stop_manager._auto_start_stop_algo._accumulated_error = 0 with patch( "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" ) as mock_send_event: @@ -500,7 +502,7 @@ async def test_auto_start_stop_medium_heat_vtherm( assert vtherm.hvac_mode == HVACMode.HEAT assert mock_send_event.call_count == 0 assert ( - vtherm._auto_start_stop_algo.accumulated_error == 0 + vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == 0 ) # target = current = 19 # 4. Set current temperature to 20 5 min later @@ -516,7 +518,10 @@ async def test_auto_start_stop_medium_heat_vtherm( assert vtherm.hvac_mode == HVACMode.HEAT assert mock_send_event.call_count == 0 # accumulated_error = target - current = -1 x 5 min / 2 - assert vtherm._auto_start_stop_algo.accumulated_error == -2.5 + assert ( + vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error + == -2.5 + ) # 5. Set current temperature to 21 5 min later -> should turn off now = now + timedelta(minutes=5) @@ -532,7 +537,9 @@ async def test_auto_start_stop_medium_heat_vtherm( assert vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP # accumulated_error = -2.5 + target - current = -2 x 5 min / 2 capped to 5 - assert vtherm._auto_start_stop_algo.accumulated_error == -5 + assert ( + vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == -5 + ) # a message should have been sent assert mock_send_event.call_count >= 1 @@ -577,7 +584,9 @@ async def test_auto_start_stop_medium_heat_vtherm( await hass.async_block_till_done() # accumulated_error = .... capped to -5 - assert vtherm._auto_start_stop_algo.accumulated_error == -5 + assert ( + vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == -5 + ) # VTherm should stay stopped cause slope is too low to allow the turn to On assert vtherm.hvac_mode == HVACMode.OFF @@ -593,7 +602,9 @@ async def test_auto_start_stop_medium_heat_vtherm( await hass.async_block_till_done() # accumulated_error = -5/2 + target - current = 1 x 20 min / 2 capped to 5 - assert vtherm._auto_start_stop_algo.accumulated_error == 5 + assert ( + vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == 5 + ) # VTherm should have been stopped assert vtherm.hvac_mode == HVACMode.HEAT @@ -717,7 +728,10 @@ async def test_auto_start_stop_fast_ac_vtherm( assert vtherm._attr_extra_state_attributes["auto_start_stop_dtmin"] == 7 # 1. Vtherm auto-start/stop should be in MEDIUM mode - assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST + assert ( + vtherm.auto_start_stop_manager.auto_start_stop_level + == AUTO_START_STOP_LEVEL_FAST + ) tz = get_tz(hass) # pylint: disable=invalid-name now: datetime = datetime.now(tz=tz) @@ -736,7 +750,7 @@ async def test_auto_start_stop_fast_ac_vtherm( # 3. Set current temperature to 19 5 min later now = now + timedelta(minutes=5) # reset accumulated error for test - vtherm._auto_start_stop_algo._accumulated_error = 0 + vtherm.auto_start_stop_manager._auto_start_stop_algo._accumulated_error = 0 with patch( "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" ) as mock_send_event: @@ -748,7 +762,8 @@ async def test_auto_start_stop_fast_ac_vtherm( assert vtherm.hvac_mode == HVACMode.COOL assert mock_send_event.call_count == 0 assert ( - vtherm._auto_start_stop_algo.accumulated_error == 0 # target = current = 25 + vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error + == 0 # target = current = 25 ) # 4. Set current temperature to 23 5 min later -> should turn off @@ -764,7 +779,9 @@ async def test_auto_start_stop_fast_ac_vtherm( assert vtherm.hvac_mode == HVACMode.OFF # accumulated_error = target - current = 2 x 5 min / 2 capped to 2 - assert vtherm._auto_start_stop_algo.accumulated_error == 2 + assert ( + vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == 2 + ) # a message should have been sent assert mock_send_event.call_count >= 1 @@ -809,7 +826,9 @@ async def test_auto_start_stop_fast_ac_vtherm( await hass.async_block_till_done() # accumulated_error = 2/2 + target - current = -1 x 20 min / 2 capped to 2 - assert vtherm._auto_start_stop_algo.accumulated_error == -2 + assert ( + vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == -2 + ) # VTherm should stay stopped assert vtherm.hvac_mode == HVACMode.OFF @@ -826,7 +845,9 @@ async def test_auto_start_stop_fast_ac_vtherm( await hass.async_block_till_done() # accumulated_error = 2/2 + target - current = -1 x 20 min / 2 capped to 2 - assert vtherm._auto_start_stop_algo.accumulated_error == -2 + assert ( + vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == -2 + ) # VTherm should have been stopped assert vtherm.hvac_mode == HVACMode.COOL @@ -948,7 +969,10 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change( assert vtherm._attr_extra_state_attributes["auto_start_stop_dtmin"] == 7 # 1. Vtherm auto-start/stop should be in MEDIUM mode - assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST + assert ( + vtherm.auto_start_stop_manager.auto_start_stop_level + == AUTO_START_STOP_LEVEL_FAST + ) tz = get_tz(hass) # pylint: disable=invalid-name now: datetime = datetime.now(tz=tz) @@ -966,7 +990,7 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change( # 3. Set current temperature to 21 5 min later to auto-stop now = now + timedelta(minutes=5) - vtherm._auto_start_stop_algo._accumulated_error = 0 + vtherm.auto_start_stop_manager._auto_start_stop_algo._accumulated_error = 0 with patch( "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" ) as mock_send_event: @@ -977,7 +1001,9 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change( # VTherm should have been stopped assert vtherm.hvac_mode == HVACMode.OFF - assert vtherm._auto_start_stop_algo.accumulated_error == -2 + assert ( + vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == -2 + ) # a message should have been sent assert mock_send_event.call_count >= 1 @@ -1032,7 +1058,9 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change( await hass.async_block_till_done() assert vtherm.target_temperature == 21 - assert vtherm._auto_start_stop_algo.accumulated_error == 2 + assert ( + vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == 2 + ) # VTherm should have been restarted assert vtherm.hvac_mode == HVACMode.HEAT @@ -1154,7 +1182,10 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change_enable_false( assert vtherm._attr_extra_state_attributes["auto_start_stop_dtmin"] == 7 # 1. Vtherm auto-start/stop should be in FAST mode and enable should be on - assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST + assert ( + vtherm.auto_start_stop_manager.auto_start_stop_level + == AUTO_START_STOP_LEVEL_FAST + ) enable_entity = search_entity( hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN ) @@ -1185,7 +1216,7 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change_enable_false( # 3. Set current temperature to 21 5 min later to auto-stop now = now + timedelta(minutes=5) - vtherm._auto_start_stop_algo._accumulated_error = 0 + vtherm.auto_start_stop_manager._auto_start_stop_algo._accumulated_error = 0 with patch( "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" ) as mock_send_event: @@ -1197,7 +1228,9 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change_enable_false( assert vtherm.hvac_mode == HVACMode.HEAT # Not calculated cause enable = false - assert vtherm._auto_start_stop_algo.accumulated_error == 0 + assert ( + vtherm.auto_start_stop_manager._auto_start_stop_algo.accumulated_error == 0 + ) # a message should have been sent assert mock_send_event.call_count == 0 @@ -1288,7 +1321,10 @@ async def test_auto_start_stop_fast_heat_window( assert vtherm._attr_extra_state_attributes["auto_start_stop_dtmin"] == 7 # 1. Vtherm auto-start/stop should be in MEDIUM mode and an enable entity should exists - assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST + assert ( + vtherm.auto_start_stop_manager.auto_start_stop_level + == AUTO_START_STOP_LEVEL_FAST + ) enable_entity = search_entity( hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN ) @@ -1315,7 +1351,7 @@ async def test_auto_start_stop_fast_heat_window( # 3. Set current temperature to 21 5 min later -> should turn off VTherm now = now + timedelta(minutes=5) # reset accumulated error (only for testing) - vtherm._auto_start_stop_algo._accumulated_error = 0 + vtherm.auto_start_stop_manager._auto_start_stop_algo._accumulated_error = 0 with patch( "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" ) as mock_send_event: @@ -1463,7 +1499,10 @@ async def test_auto_start_stop_fast_heat_window_mixed( assert vtherm._attr_extra_state_attributes["auto_start_stop_dtmin"] == 7 # 1. Vtherm auto-start/stop should be in MEDIUM mode and an enable entity should exists - assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST + assert ( + vtherm.auto_start_stop_manager.auto_start_stop_level + == AUTO_START_STOP_LEVEL_FAST + ) enable_entity = search_entity( hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN ) @@ -1513,7 +1552,7 @@ async def test_auto_start_stop_fast_heat_window_mixed( # 4. Set current temperature to 21 5 min later -> should turn off VTherm now = now + timedelta(minutes=5) # reset accumulated error (only for testing) - vtherm._auto_start_stop_algo._accumulated_error = 0 + vtherm.auto_start_stop_manager._auto_start_stop_algo._accumulated_error = 0 with patch( "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" ) as mock_send_event: @@ -1637,6 +1676,9 @@ async def test_auto_start_stop_disable_vtherm_off( await set_all_climate_preset_temp(hass, vtherm, temps, "overclimate") # Check correct initialization of auto_start_stop attributes + assert ( + vtherm._attr_extra_state_attributes["is_auto_start_stop_configured"] is True + ) assert ( vtherm._attr_extra_state_attributes["auto_start_stop_level"] == AUTO_START_STOP_LEVEL_FAST @@ -1646,7 +1688,10 @@ async def test_auto_start_stop_disable_vtherm_off( # 1. Vtherm auto-start/stop should be in FAST mode and enable should be on vtherm._set_now(now) - assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST + assert ( + vtherm.auto_start_stop_manager.auto_start_stop_level + == AUTO_START_STOP_LEVEL_FAST + ) enable_entity = search_entity( hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN ) diff --git a/tests/test_auto_start_stop_feature_manager.py b/tests/test_auto_start_stop_feature_manager.py new file mode 100644 index 0000000..7c3c661 --- /dev/null +++ b/tests/test_auto_start_stop_feature_manager.py @@ -0,0 +1,121 @@ +# pylint: disable=unused-argument, line-too-long, protected-access, too-many-lines +""" Test the Window management """ +import logging +from datetime import datetime, timedelta +from unittest.mock import patch, call, PropertyMock, AsyncMock, MagicMock + +from custom_components.versatile_thermostat.base_thermostat import BaseThermostat + +from custom_components.versatile_thermostat.feature_auto_start_stop_manager import ( + FeatureAutoStartStopManager, +) +from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import + +logging.getLogger().setLevel(logging.DEBUG) + + +async def test_auto_start_stop_feature_manager_create( + hass: HomeAssistant, +): + """Test the FeatureMotionManager class direclty""" + + fake_vtherm = MagicMock(spec=BaseThermostat) + type(fake_vtherm).name = PropertyMock(return_value="the name") + + # 1. creation + auto_start_stop_manager = FeatureAutoStartStopManager(fake_vtherm, hass) + + assert auto_start_stop_manager is not None + assert auto_start_stop_manager.is_configured is False + assert auto_start_stop_manager.is_auto_stopped is False + assert auto_start_stop_manager.auto_start_stop_enable is False + assert auto_start_stop_manager.name == "the name" + + assert len(auto_start_stop_manager._active_listener) == 0 + + custom_attributes = {} + auto_start_stop_manager.add_custom_attributes(custom_attributes) + assert custom_attributes["is_auto_start_stop_configured"] is False + # assert custom_attributes["auto_start_stop_enable"] is False + # assert custom_attributes["auto_start_stop_level"] == AUTO_START_STOP_LEVEL_NONE + # assert custom_attributes["auto_start_stop_dtmin"] is None + # assert custom_attributes["auto_start_stop_accumulated_error"] is None + # assert custom_attributes["auto_start_stop_accumulated_error_threshold"] is None + # assert custom_attributes["auto_start_stop_last_switch_date"] is None + + +@pytest.mark.parametrize( + "use_auto_start_stop_feature, level, is_configured", + [ + # fmt: off + ( True, AUTO_START_STOP_LEVEL_NONE, True), + ( True, AUTO_START_STOP_LEVEL_SLOW, True), + ( True, AUTO_START_STOP_LEVEL_MEDIUM, True), + ( True, AUTO_START_STOP_LEVEL_FAST, True), + # Level is missing , will be set to None + ( True, None, True), + ( False, AUTO_START_STOP_LEVEL_NONE, False), + ( False, AUTO_START_STOP_LEVEL_SLOW, False), + ( False, AUTO_START_STOP_LEVEL_MEDIUM, False), + ( False, AUTO_START_STOP_LEVEL_FAST, False), + # Level is missing , will be set to None + ( False, None, False), + # fmt: on + ], +) +async def test_auto_start_stop_feature_manager_post_init( + hass: HomeAssistant, use_auto_start_stop_feature, level, is_configured +): + """Test the FeatureMotionManager class direclty""" + + fake_vtherm = MagicMock(spec=BaseThermostat) + type(fake_vtherm).name = PropertyMock(return_value="the name") + + # 1. creation + auto_start_stop_manager = FeatureAutoStartStopManager(fake_vtherm, hass) + assert auto_start_stop_manager is not None + + # 2. post_init + auto_start_stop_manager.post_init( + { + CONF_USE_AUTO_START_STOP_FEATURE: use_auto_start_stop_feature, + CONF_AUTO_START_STOP_LEVEL: level, + } + ) + + assert auto_start_stop_manager.is_configured is is_configured + assert ( + auto_start_stop_manager.auto_start_stop_level == level + if level and is_configured + else AUTO_START_STOP_LEVEL_NONE + ) + assert auto_start_stop_manager.auto_start_stop_enable is False + assert auto_start_stop_manager._auto_start_stop_algo is not None + + custom_attributes = {} + auto_start_stop_manager.add_custom_attributes(custom_attributes) + assert custom_attributes["is_auto_start_stop_configured"] is is_configured + + if auto_start_stop_manager.is_configured: + assert custom_attributes["auto_start_stop_enable"] is False + assert ( + custom_attributes["auto_start_stop_level"] == level + if level and is_configured + else AUTO_START_STOP_LEVEL_NONE + ) + assert ( + custom_attributes["auto_start_stop_dtmin"] + == auto_start_stop_manager._auto_start_stop_algo.dt_min + ) + assert ( + custom_attributes["auto_start_stop_accumulated_error"] + == auto_start_stop_manager._auto_start_stop_algo.accumulated_error + ) + assert ( + custom_attributes["auto_start_stop_accumulated_error_threshold"] + == auto_start_stop_manager._auto_start_stop_algo.accumulated_error_threshold + ) + assert ( + custom_attributes["auto_start_stop_last_switch_date"] + == auto_start_stop_manager._auto_start_stop_algo.last_switch_date + ) diff --git a/tests/test_overclimate.py b/tests/test_overclimate.py index 30581a2..dd7898a 100644 --- a/tests/test_overclimate.py +++ b/tests/test_overclimate.py @@ -938,7 +938,10 @@ async def test_manual_hvac_off_should_take_the_lead_over_window( == AUTO_START_STOP_LEVEL_FAST ) - assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST + assert ( + vtherm.auto_start_stop_manager.auto_start_stop_level + == AUTO_START_STOP_LEVEL_FAST + ) enable_entity = search_entity( hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN ) @@ -1112,7 +1115,10 @@ async def test_manual_hvac_off_should_take_the_lead_over_auto_start_stop( == AUTO_START_STOP_LEVEL_FAST ) - assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST + assert ( + vtherm.auto_start_stop_manager.auto_start_stop_level + == AUTO_START_STOP_LEVEL_FAST + ) enable_entity = search_entity( hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN ) @@ -1138,7 +1144,7 @@ async def test_manual_hvac_off_should_take_the_lead_over_auto_start_stop( now = now + timedelta(minutes=5) vtherm._set_now(now) # reset accumulated error (only for testing) - vtherm._auto_start_stop_algo._accumulated_error = 0 + vtherm.auto_start_stop_manager._auto_start_stop_algo._accumulated_error = 0 with patch( "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" ) as mock_send_event: