Compare commits

..

3 Commits

Author SHA1 Message Date
Jean-Marc Collin
f547647e24 With enable + tests + hysteresis in calculation 2024-11-01 10:24:35 +00:00
Jean-Marc Collin
5063374b97 Allow calculation even if slope is None 2024-10-31 22:29:30 +00:00
Jean-Marc Collin
461db8d86c Change algo to take slop into account 2024-10-31 21:57:55 +00:00
5 changed files with 215 additions and 43 deletions

View File

@@ -3,7 +3,7 @@
""" """
import logging import logging
from datetime import datetime, timedelta from datetime import datetime
from typing import Literal from typing import Literal
from homeassistant.components.climate import HVACMode from homeassistant.components.climate import HVACMode
@@ -13,7 +13,6 @@ from .const import (
AUTO_START_STOP_LEVEL_FAST, AUTO_START_STOP_LEVEL_FAST,
AUTO_START_STOP_LEVEL_MEDIUM, AUTO_START_STOP_LEVEL_MEDIUM,
AUTO_START_STOP_LEVEL_SLOW, AUTO_START_STOP_LEVEL_SLOW,
CONF_AUTO_START_STOP_LEVELS,
TYPE_AUTO_START_STOP_LEVELS, TYPE_AUTO_START_STOP_LEVELS,
) )
@@ -31,6 +30,9 @@ DT_MIN = {
# the measurement cycle (2 min) # the measurement cycle (2 min)
CYCLE_SEC = 120 CYCLE_SEC = 120
# A temp hysteresis to avoid rapid OFF/ON
TEMP_HYSTERESIS = 0.5
ERROR_THRESHOLD = { ERROR_THRESHOLD = {
AUTO_START_STOP_LEVEL_NONE: 0, # Not used AUTO_START_STOP_LEVEL_NONE: 0, # Not used
AUTO_START_STOP_LEVEL_SLOW: 10, # 10 cycle above 1° or 5 cycle above 2°, ... AUTO_START_STOP_LEVEL_SLOW: 10, # 10 cycle above 1° or 5 cycle above 2°, ...
@@ -79,7 +81,7 @@ class AutoStartStopDetectionAlgorithm:
saved_hvac_mode: HVACMode | None, saved_hvac_mode: HVACMode | None,
target_temp: float, target_temp: float,
current_temp: float, current_temp: float,
slope_min: float, slope_min: float | None,
now: datetime, now: datetime,
) -> AUTO_START_STOP_ACTIONS: ) -> AUTO_START_STOP_ACTIONS:
"""Calculate an eventual action to do depending of the value in parameter""" """Calculate an eventual action to do depending of the value in parameter"""
@@ -101,12 +103,7 @@ class AutoStartStopDetectionAlgorithm:
now, now,
) )
if ( if hvac_mode is None or target_temp is None or current_temp is None:
hvac_mode is None
or target_temp is None
or current_temp is None
or slope_min is None
):
_LOGGER.debug( _LOGGER.debug(
"%s - No all mandatory parameters are set. Disable auto-start/stop", "%s - No all mandatory parameters are set. Disable auto-start/stop",
self, self,
@@ -119,8 +116,8 @@ class AutoStartStopDetectionAlgorithm:
# reduce the error considering the dt between the last measurement # reduce the error considering the dt between the last measurement
if self._last_calculation_date is not None: if self._last_calculation_date is not None:
dtmin = (now - self._last_calculation_date).total_seconds() / CYCLE_SEC dtmin = (now - self._last_calculation_date).total_seconds() / CYCLE_SEC
# ignore two calls too near (< 1 min) # ignore two calls too near (< 24 sec)
if dtmin <= 0.5: if dtmin <= 0.2:
_LOGGER.debug( _LOGGER.debug(
"%s - new calculation of auto_start_stop (%s) is too near of the last one (%s). Forget it", "%s - new calculation of auto_start_stop (%s) is too near of the last one (%s). Forget it",
self, self,
@@ -144,10 +141,15 @@ class AutoStartStopDetectionAlgorithm:
self._last_calculation_date = now self._last_calculation_date = now
temp_at_dt = current_temp + slope_min * self._dt
# Check to turn-off # Check to turn-off
# When we hit the threshold, that mean we can turn off # When we hit the threshold, that mean we can turn off
if hvac_mode == HVACMode.HEAT: if hvac_mode == HVACMode.HEAT:
if self._accumulated_error <= -self._error_threshold and slope_min >= 0: if (
self._accumulated_error <= -self._error_threshold
and temp_at_dt >= target_temp + TEMP_HYSTERESIS
):
_LOGGER.info( _LOGGER.info(
"%s - We need to stop, there is no need for heating for a long time.", "%s - We need to stop, there is no need for heating for a long time.",
self, self,
@@ -158,7 +160,10 @@ class AutoStartStopDetectionAlgorithm:
return AUTO_START_STOP_ACTION_NOTHING return AUTO_START_STOP_ACTION_NOTHING
if hvac_mode == HVACMode.COOL: if hvac_mode == HVACMode.COOL:
if self._accumulated_error >= self._error_threshold and slope_min <= 0: if (
self._accumulated_error >= self._error_threshold
and temp_at_dt <= target_temp - TEMP_HYSTERESIS
):
_LOGGER.info( _LOGGER.info(
"%s - We need to stop, there is no need for cooling for a long time.", "%s - We need to stop, there is no need for cooling for a long time.",
self, self,
@@ -173,7 +178,7 @@ class AutoStartStopDetectionAlgorithm:
# check to turn on # check to turn on
if hvac_mode == HVACMode.OFF and saved_hvac_mode == HVACMode.HEAT: if hvac_mode == HVACMode.OFF and saved_hvac_mode == HVACMode.HEAT:
if current_temp + slope_min * self._dt <= target_temp: if temp_at_dt <= target_temp - TEMP_HYSTERESIS:
_LOGGER.info( _LOGGER.info(
"%s - We need to start, because it will be time to heat", "%s - We need to start, because it will be time to heat",
self, self,
@@ -187,7 +192,7 @@ class AutoStartStopDetectionAlgorithm:
return AUTO_START_STOP_ACTION_NOTHING return AUTO_START_STOP_ACTION_NOTHING
if hvac_mode == HVACMode.OFF and saved_hvac_mode == HVACMode.COOL: if hvac_mode == HVACMode.OFF and saved_hvac_mode == HVACMode.COOL:
if current_temp + slope_min * self._dt >= target_temp: if temp_at_dt >= target_temp + TEMP_HYSTERESIS:
_LOGGER.info( _LOGGER.info(
"%s - We need to start, because it will be time to cool", "%s - We need to start, because it will be time to cool",
self, self,

View File

@@ -199,6 +199,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"is_device_active", "is_device_active",
"target_temperature_step", "target_temperature_step",
"is_used_by_central_boiler", "is_used_by_central_boiler",
"temperature_slope"
} }
) )
) )
@@ -2633,6 +2634,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"is_device_active": self.is_device_active, "is_device_active": self.is_device_active,
"ema_temp": self._ema_temp, "ema_temp": self._ema_temp,
"is_used_by_central_boiler": self.is_used_by_central_boiler, "is_used_by_central_boiler": self.is_used_by_central_boiler,
"temperature_slope": round(self.last_temperature_slope or 0, 3),
} }
@callback @callback

View File

@@ -8,7 +8,6 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.components.switch import SwitchEntity from homeassistant.components.switch import SwitchEntity
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -49,7 +48,7 @@ class AutoStartStopEnable(VersatileThermostatBaseEntity, SwitchEntity, RestoreEn
): ):
super().__init__(hass, unique_id, name) super().__init__(hass, unique_id, name)
self._attr_name = "Enable auto start/stop" self._attr_name = "Enable auto start/stop"
self._attr_unique_id = f"{self._device_name}_enbale_auto_start_stop" self._attr_unique_id = f"{self._device_name}_enable_auto_start_stop"
self._default_value = ( self._default_value = (
entry_infos.data.get(CONF_AUTO_START_STOP_LEVEL) entry_infos.data.get(CONF_AUTO_START_STOP_LEVEL)
!= AUTO_START_STOP_LEVEL_NONE != AUTO_START_STOP_LEVEL_NONE

View File

@@ -49,6 +49,7 @@ from .const import (
RegulationParamStrong, RegulationParamStrong,
AUTO_FAN_DTEMP_THRESHOLD, AUTO_FAN_DTEMP_THRESHOLD,
AUTO_FAN_DEACTIVATED_MODES, AUTO_FAN_DEACTIVATED_MODES,
CONF_USE_AUTO_START_STOP_FEATURE,
CONF_AUTO_START_STOP_LEVEL, CONF_AUTO_START_STOP_LEVEL,
AUTO_START_STOP_LEVEL_NONE, AUTO_START_STOP_LEVEL_NONE,
TYPE_AUTO_START_STOP_LEVELS, TYPE_AUTO_START_STOP_LEVELS,
@@ -176,9 +177,14 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
CONF_AUTO_REGULATION_USE_DEVICE_TEMP, False CONF_AUTO_REGULATION_USE_DEVICE_TEMP, False
) )
self._auto_start_stop_level = config_entry.get( use_auto_start_stop = config_entry.get(CONF_USE_AUTO_START_STOP_FEATURE, False)
CONF_AUTO_START_STOP_LEVEL, AUTO_START_STOP_LEVEL_NONE 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 # Instanciate the auto start stop algo
self._auto_start_stop_algo = AutoStartStopDetectionAlgorithm( self._auto_start_stop_algo = AutoStartStopDetectionAlgorithm(
self._auto_start_stop_level, self.name self._auto_start_stop_level, self.name
@@ -912,16 +918,16 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
ret = await super().async_control_heating(force, _) ret = await super().async_control_heating(force, _)
# Check if we need to auto start/stop the Vtherm # Check if we need to auto start/stop the Vtherm
if ( if self.auto_start_stop_enable:
self.auto_start_stop_enable slope = (
and self._window_auto_algo.last_slope is not None self.last_temperature_slope or 0
): ) / 60 # to have the slope in °/min
action = self._auto_start_stop_algo.calculate_action( action = self._auto_start_stop_algo.calculate_action(
self.hvac_mode, self.hvac_mode,
self._saved_hvac_mode, self._saved_hvac_mode,
self.target_temperature, self.target_temperature,
self.current_temperature, self.current_temperature,
self._window_auto_algo.last_slope / 60, # to have the slope in °/min slope,
self.now, self.now,
) )
_LOGGER.debug("%s - auto_start_stop action is %s", self, action) _LOGGER.debug("%s - auto_start_stop action is %s", self, action)
@@ -937,13 +943,13 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
event_type=EventType.AUTO_START_STOP_EVENT, event_type=EventType.AUTO_START_STOP_EVENT,
data={ data={
"type": "stop", "type": "stop",
"name:": self.name, "name": self.name,
"cause": "Auto stop conditions reached", "cause": "Auto stop conditions reached",
"hvac_mode": self.hvac_mode, "hvac_mode": self.hvac_mode,
"saved_hvac_mode": self._saved_hvac_mode, "saved_hvac_mode": self._saved_hvac_mode,
"target_temperature": self.target_temperature, "target_temperature": self.target_temperature,
"current_temperature": self.current_temperature, "current_temperature": self.current_temperature,
"temperature_slope": self._window_auto_algo.last_slope, "temperature_slope": round(slope, 3),
}, },
) )
@@ -960,13 +966,13 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
event_type=EventType.AUTO_START_STOP_EVENT, event_type=EventType.AUTO_START_STOP_EVENT,
data={ data={
"type": "start", "type": "start",
"name:": self.name, "name": self.name,
"cause": "Auto start conditions reached", "cause": "Auto start conditions reached",
"hvac_mode": self.hvac_mode, "hvac_mode": self.hvac_mode,
"saved_hvac_mode": self._saved_hvac_mode, "saved_hvac_mode": self._saved_hvac_mode,
"target_temperature": self.target_temperature, "target_temperature": self.target_temperature,
"current_temperature": self.current_temperature, "current_temperature": self.current_temperature,
"temperature_slope": self._window_auto_algo.last_slope, "temperature_slope": round(slope, 3),
}, },
) )
@@ -987,6 +993,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
def set_auto_start_stop_enable(self, is_enabled: bool): def set_auto_start_stop_enable(self, is_enabled: bool):
"""Enable/Disable the auto-start/stop feature""" """Enable/Disable the auto-start/stop feature"""
self._is_auto_start_stop_enabled = is_enabled self._is_auto_start_stop_enabled = is_enabled
self.update_custom_attributes()
@property @property
def auto_regulation_mode(self) -> str | None: def auto_regulation_mode(self) -> str | None:

View File

@@ -6,6 +6,7 @@ import logging
from unittest.mock import patch, call from unittest.mock import patch, call
from homeassistant.components.climate import HVACMode from homeassistant.components.climate import HVACMode
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from custom_components.versatile_thermostat.thermostat_climate import ( from custom_components.versatile_thermostat.thermostat_climate import (
ThermostatOverClimate, ThermostatOverClimate,
@@ -14,7 +15,6 @@ from custom_components.versatile_thermostat.auto_start_stop_algorithm import (
AutoStartStopDetectionAlgorithm, AutoStartStopDetectionAlgorithm,
AUTO_START_STOP_ACTION_NOTHING, AUTO_START_STOP_ACTION_NOTHING,
AUTO_START_STOP_ACTION_OFF, AUTO_START_STOP_ACTION_OFF,
AUTO_START_STOP_ACTION_ON,
) )
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
@@ -72,7 +72,7 @@ async def test_auto_start_stop_algo_slow_heat_off(hass: HomeAssistant):
assert ret == AUTO_START_STOP_ACTION_NOTHING assert ret == AUTO_START_STOP_ACTION_NOTHING
# 4 .No change on accumulated error because the new measure is too near the last one # 4 .No change on accumulated error because the new measure is too near the last one
now = now + timedelta(minutes=1) now = now + timedelta(seconds=11)
ret = algo.calculate_action( ret = algo.calculate_action(
hvac_mode=HVACMode.HEAT, hvac_mode=HVACMode.HEAT,
saved_hvac_mode=HVACMode.OFF, saved_hvac_mode=HVACMode.OFF,
@@ -225,6 +225,7 @@ async def test_auto_start_stop_none_vtherm(
CONF_USE_WINDOW_FEATURE: False, CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False, CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False, CONF_USE_POWER_FEATURE: False,
CONF_USE_AUTO_START_STOP_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: True, CONF_USE_PRESENCE_FEATURE: True,
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor", CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_CLIMATE: "climate.mock_climate", CONF_CLIMATE: "climate.mock_climate",
@@ -267,6 +268,12 @@ async def test_auto_start_stop_none_vtherm(
# 1. Vtherm auto-start/stop should be in NONE mode # 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_level == AUTO_START_STOP_LEVEL_NONE
# 2. We should not find any switch Enable entity
assert (
search_entity(hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN)
is None
)
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
@@ -310,6 +317,7 @@ async def test_auto_start_stop_medium_heat_vtherm(
CONF_USE_WINDOW_FEATURE: False, CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False, CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False, CONF_USE_POWER_FEATURE: False,
CONF_USE_AUTO_START_STOP_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True, CONF_USE_PRESENCE_FEATURE: True,
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor", CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_CLIMATE: "climate.mock_climate", CONF_CLIMATE: "climate.mock_climate",
@@ -350,8 +358,13 @@ async def test_auto_start_stop_medium_heat_vtherm(
assert vtherm._attr_extra_state_attributes["auto_start_stop_dtmin"] == 15 assert vtherm._attr_extra_state_attributes["auto_start_stop_dtmin"] == 15
# 1. Vtherm auto-start/stop should be in MEDIUM mode # 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_level == AUTO_START_STOP_LEVEL_MEDIUM
enable_entity = search_entity(
hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN
)
assert enable_entity is not None
assert enable_entity.state == STATE_ON
tz = get_tz(hass) # pylint: disable=invalid-name tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz) now: datetime = datetime.now(tz=tz)
@@ -369,6 +382,8 @@ async def test_auto_start_stop_medium_heat_vtherm(
# 3. Set current temperature to 19 5 min later # 3. Set current temperature to 19 5 min later
now = now + timedelta(minutes=5) now = now + timedelta(minutes=5)
# reset accumulated error (only for testing)
vtherm._auto_start_stop_algo._accumulated_error = 0
with patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event: ) as mock_send_event:
@@ -421,12 +436,13 @@ async def test_auto_start_stop_medium_heat_vtherm(
event_type=EventType.AUTO_START_STOP_EVENT, event_type=EventType.AUTO_START_STOP_EVENT,
data={ data={
"type": "stop", "type": "stop",
"name": "overClimate",
"cause": "Auto stop conditions reached", "cause": "Auto stop conditions reached",
"hvac_mode": HVACMode.OFF, "hvac_mode": HVACMode.OFF,
"saved_hvac_mode": HVACMode.HEAT, "saved_hvac_mode": HVACMode.HEAT,
"target_temperature": 19.0, "target_temperature": 19.0,
"current_temperature": 21.0, "current_temperature": 21.0,
"temperature_slope": 10.03, "temperature_slope": 0.167,
}, },
) )
] ]
@@ -480,12 +496,13 @@ async def test_auto_start_stop_medium_heat_vtherm(
event_type=EventType.AUTO_START_STOP_EVENT, event_type=EventType.AUTO_START_STOP_EVENT,
data={ data={
"type": "start", "type": "start",
"name": "overClimate",
"cause": "Auto start conditions reached", "cause": "Auto start conditions reached",
"hvac_mode": HVACMode.HEAT, "hvac_mode": HVACMode.HEAT,
"saved_hvac_mode": HVACMode.HEAT, # saved don't change "saved_hvac_mode": HVACMode.HEAT, # saved don't change
"target_temperature": 19.0, "target_temperature": 19.0,
"current_temperature": 18.0, "current_temperature": 18.0,
"temperature_slope": -2.06, "temperature_slope": -0.034,
}, },
) )
] ]
@@ -545,6 +562,7 @@ async def test_auto_start_stop_fast_ac_vtherm(
CONF_USE_WINDOW_FEATURE: False, CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False, CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False, CONF_USE_POWER_FEATURE: False,
CONF_USE_AUTO_START_STOP_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True, CONF_USE_PRESENCE_FEATURE: True,
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor", CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_CLIMATE: "climate.mock_climate", CONF_CLIMATE: "climate.mock_climate",
@@ -599,11 +617,13 @@ async def test_auto_start_stop_fast_ac_vtherm(
await hass.async_block_till_done() await hass.async_block_till_done()
assert vtherm.target_temperature == 25.0 assert vtherm.target_temperature == 25.0
# VTherm should be heating # VTherm should be cooling
assert vtherm.hvac_mode == HVACMode.COOL assert vtherm.hvac_mode == HVACMode.COOL
# 3. Set current temperature to 19 5 min later # 3. Set current temperature to 19 5 min later
now = now + timedelta(minutes=5) now = now + timedelta(minutes=5)
# reset accumulated error for test
vtherm._auto_start_stop_algo._accumulated_error = 0
with patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event: ) as mock_send_event:
@@ -611,7 +631,7 @@ async def test_auto_start_stop_fast_ac_vtherm(
await send_temperature_change_event(vtherm, 25, now, True) await send_temperature_change_event(vtherm, 25, now, True)
await hass.async_block_till_done() await hass.async_block_till_done()
# VTherm should still be heating # VTherm should still be cooling
assert vtherm.hvac_mode == HVACMode.COOL assert vtherm.hvac_mode == HVACMode.COOL
assert mock_send_event.call_count == 0 assert mock_send_event.call_count == 0
assert ( assert (
@@ -641,12 +661,13 @@ async def test_auto_start_stop_fast_ac_vtherm(
event_type=EventType.AUTO_START_STOP_EVENT, event_type=EventType.AUTO_START_STOP_EVENT,
data={ data={
"type": "stop", "type": "stop",
"name": "overClimate",
"cause": "Auto stop conditions reached", "cause": "Auto stop conditions reached",
"hvac_mode": HVACMode.OFF, "hvac_mode": HVACMode.OFF,
"saved_hvac_mode": HVACMode.COOL, "saved_hvac_mode": HVACMode.COOL,
"target_temperature": 25.0, "target_temperature": 25.0,
"current_temperature": 23.0, "current_temperature": 23.0,
"temperature_slope": -16.8, "temperature_slope": -0.28,
}, },
) )
] ]
@@ -664,18 +685,18 @@ async def test_auto_start_stop_fast_ac_vtherm(
) )
# 5. Set temperature to over the target, but slope is too low -> no change # 5. Set temperature to over the target, but slope is too low -> no change
now = now + timedelta(minutes=20) now = now + timedelta(minutes=30)
with patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event: ) as mock_send_event:
vtherm._set_now(now) vtherm._set_now(now)
await send_temperature_change_event(vtherm, 26, now, True) await send_temperature_change_event(vtherm, 25.5, now, True)
await hass.async_block_till_done() await hass.async_block_till_done()
# accumulated_error = 2/2 + target - current = -1 x 20 min / 2 capped to 2 # 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_algo.accumulated_error == -2
# VTherm should have been stopped # VTherm should stay stopped
assert vtherm.hvac_mode == HVACMode.OFF assert vtherm.hvac_mode == HVACMode.OFF
# a message should have been sent # a message should have been sent
assert mock_send_event.call_count == 0 assert mock_send_event.call_count == 0
@@ -702,12 +723,13 @@ async def test_auto_start_stop_fast_ac_vtherm(
event_type=EventType.AUTO_START_STOP_EVENT, event_type=EventType.AUTO_START_STOP_EVENT,
data={ data={
"type": "start", "type": "start",
"name": "overClimate",
"cause": "Auto start conditions reached", "cause": "Auto start conditions reached",
"hvac_mode": HVACMode.COOL, "hvac_mode": HVACMode.COOL,
"saved_hvac_mode": HVACMode.COOL, # saved don't change "saved_hvac_mode": HVACMode.COOL, # saved don't change
"target_temperature": 25.0, "target_temperature": 25.0,
"current_temperature": 26.5, "current_temperature": 26.5,
"temperature_slope": 5.74, "temperature_slope": 0.112,
}, },
) )
] ]
@@ -767,6 +789,7 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change(
CONF_USE_WINDOW_FEATURE: False, CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False, CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False, CONF_USE_POWER_FEATURE: False,
CONF_USE_AUTO_START_STOP_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True, CONF_USE_PRESENCE_FEATURE: True,
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor", CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_CLIMATE: "climate.mock_climate", CONF_CLIMATE: "climate.mock_climate",
@@ -826,6 +849,7 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change(
# 3. Set current temperature to 21 5 min later to auto-stop # 3. Set current temperature to 21 5 min later to auto-stop
now = now + timedelta(minutes=5) now = now + timedelta(minutes=5)
vtherm._auto_start_stop_algo._accumulated_error = 0
with patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event: ) as mock_send_event:
@@ -846,12 +870,13 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change(
event_type=EventType.AUTO_START_STOP_EVENT, event_type=EventType.AUTO_START_STOP_EVENT,
data={ data={
"type": "stop", "type": "stop",
"name": "overClimate",
"cause": "Auto stop conditions reached", "cause": "Auto stop conditions reached",
"hvac_mode": HVACMode.OFF, "hvac_mode": HVACMode.OFF,
"saved_hvac_mode": HVACMode.HEAT, "saved_hvac_mode": HVACMode.HEAT,
"target_temperature": 17.0, "target_temperature": 17.0,
"current_temperature": 19.0, "current_temperature": 19.0,
"temperature_slope": 18.0, "temperature_slope": 0.3,
}, },
) )
] ]
@@ -900,12 +925,13 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change(
event_type=EventType.AUTO_START_STOP_EVENT, event_type=EventType.AUTO_START_STOP_EVENT,
data={ data={
"type": "start", "type": "start",
"name": "overClimate",
"cause": "Auto start conditions reached", "cause": "Auto start conditions reached",
"hvac_mode": HVACMode.HEAT, "hvac_mode": HVACMode.HEAT,
"saved_hvac_mode": HVACMode.HEAT, # saved don't change "saved_hvac_mode": HVACMode.HEAT, # saved don't change
"target_temperature": 21.0, "target_temperature": 21.0,
"current_temperature": 17.0, "current_temperature": 17.0,
"temperature_slope": -5.19, "temperature_slope": -0.087,
}, },
) )
] ]
@@ -921,3 +947,136 @@ async def test_auto_start_stop_medium_heat_vtherm_preset_change(
) )
] ]
) )
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_auto_start_stop_medium_heat_vtherm_preset_change_enable_false(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test than auto-start/stop restart a VTherm stopped upon preset_change (in fast mode)"""
# vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
# The temperatures to set
temps = {
"frost": 7.0,
"eco": 17.0,
"comfort": 19.0,
"boost": 21.0,
"eco_ac": 27.0,
"comfort_ac": 25.0,
"boost_ac": 23.0,
"frost_away": 7.1,
"eco_away": 17.1,
"comfort_away": 19.1,
"boost_away": 21.1,
"eco_ac_away": 27.1,
"comfort_ac_away": 25.1,
"boost_ac_away": 23.1,
}
config_entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="overClimateUniqueId",
data={
CONF_NAME: "overClimate",
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_AUTO_START_STOP_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True,
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
CONF_AC_MODE: True,
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_FAST,
},
)
fake_underlying_climate = MockClimate(
hass=hass,
unique_id="mock_climate",
name="mock_climate",
hvac_modes=[HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT],
)
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
):
vtherm: ThermostatOverClimate = await create_thermostat(
hass, config_entry, "climate.overclimate"
)
assert vtherm is not None
# 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_FAST
)
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
enable_entity = search_entity(
hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN
)
assert enable_entity is not None
assert enable_entity.state == STATE_ON
assert vtherm._attr_extra_state_attributes.get("auto_start_stop_enable") is True
# 2. set enable to false
enable_entity.turn_off()
await hass.async_block_till_done()
assert enable_entity.state == STATE_OFF
assert vtherm._attr_extra_state_attributes.get("auto_start_stop_enable") is False
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
# 3. Set mode to Heat and preset to Comfort
await send_presence_change_event(vtherm, True, False, now)
await send_temperature_change_event(vtherm, 16, now, True)
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
await vtherm.async_set_preset_mode(PRESET_ECO)
await hass.async_block_till_done()
assert vtherm.target_temperature == 17.0
# VTherm should be heating
assert vtherm.hvac_mode == HVACMode.HEAT
# 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
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
vtherm._set_now(now)
await send_temperature_change_event(vtherm, 19, now, True)
await hass.async_block_till_done()
# VTherm should not have been stopped cause enable is false
assert vtherm.hvac_mode == HVACMode.HEAT
# Not calculated cause enable = false
assert vtherm._auto_start_stop_algo.accumulated_error == 0
# a message should have been sent
assert mock_send_event.call_count == 0