Auto start/stop alog and testu + ConfigFlow

This commit is contained in:
Jean-Marc Collin
2024-10-27 10:04:14 +00:00
parent 17ebf629e6
commit db3afdf887
8 changed files with 837 additions and 11 deletions

View File

@@ -0,0 +1,153 @@
# pylint: disable=line-too-long
""" This file implements the Auto start/stop algorithm as described here: https://github.com/jmcollin78/versatile_thermostat/issues/585
"""
import logging
from homeassistant.components.climate import HVACMode
from .const import (
AUTO_START_STOP_LEVEL_NONE,
AUTO_START_STOP_LEVEL_FAST,
AUTO_START_STOP_LEVEL_MEDIUM,
AUTO_START_STOP_LEVEL_SLOW,
CONF_AUTO_START_STOP_LEVELS,
)
_LOGGER = logging.getLogger(__name__)
# attribute name should be equal to AUTO_START_STOP_LEVEL_xxx constants (in const.yaml)
DTEMP = {
AUTO_START_STOP_LEVEL_NONE: 99,
AUTO_START_STOP_LEVEL_SLOW: 3,
AUTO_START_STOP_LEVEL_MEDIUM: 2,
AUTO_START_STOP_LEVEL_FAST: 1,
}
DT_MIN = {
AUTO_START_STOP_LEVEL_NONE: 99,
AUTO_START_STOP_LEVEL_SLOW: 30,
AUTO_START_STOP_LEVEL_MEDIUM: 15,
AUTO_START_STOP_LEVEL_FAST: 7,
}
AUTO_START_STOP_ACTION_OFF = "turnOff"
AUTO_START_STOP_ACTION_ON = "turnOn"
AUTO_START_STOP_ACTION_NOTHING = "nothing"
AUTO_START_STOP_ACTIONS = [
AUTO_START_STOP_ACTION_OFF,
AUTO_START_STOP_ACTION_ON,
AUTO_START_STOP_ACTION_NOTHING,
]
class AutoStartStopDetectionAlgorithm:
"""The class that implements the algorithm listed above"""
_dt: float
_dtemp: float
_level: str
def __init__(self, level: CONF_AUTO_START_STOP_LEVELS, vtherm_name) -> None:
"""Initalize a new algorithm with the right constants"""
self._level = level
self._dt = DT_MIN[level]
self._dtemp = DTEMP[level]
self._vtherm_name = vtherm_name
def calculate_action(
self,
hvac_mode: HVACMode | None,
saved_hvac_mode: HVACMode | None,
regulated_temp: float,
target_temp: float,
current_temp: float,
slope_min: float,
) -> AUTO_START_STOP_ACTIONS:
"""Calculate an eventual action to do depending of the value in parameter"""
if self._level == AUTO_START_STOP_LEVEL_NONE:
_LOGGER.debug(
"%s - auto-start/stop is disabled",
self,
)
return AUTO_START_STOP_ACTION_NOTHING
if (
hvac_mode is None
or regulated_temp is None
or target_temp is None
or current_temp is None
or slope_min is None
):
_LOGGER.debug(
"%s - No all mandatory parameters are set. Disable auto-start/stop",
self,
)
return AUTO_START_STOP_ACTION_NOTHING
_LOGGER.debug(
"%s - calculate_action: hvac_mode=%s, saved_hvac_mode=%s, regulated_temp=%s, target_temp=%s, current_temp=%s, slope_min=%s",
self,
hvac_mode,
saved_hvac_mode,
regulated_temp,
target_temp,
current_temp,
slope_min,
)
if hvac_mode == HVACMode.HEAT:
if regulated_temp + self._dtemp <= target_temp and slope_min >= 0:
_LOGGER.info(
"%s - We need to stop, there is no need for heating for a long time.",
)
return AUTO_START_STOP_ACTION_OFF
else:
_LOGGER.debug(
"%s - nothing to do, we are heating",
)
return AUTO_START_STOP_ACTION_NOTHING
if hvac_mode == HVACMode.COOL:
if regulated_temp - self._dtemp >= target_temp and slope_min <= 0:
_LOGGER.info(
"%s - We need to stop, there is no need for cooling for a long time.",
)
return AUTO_START_STOP_ACTION_OFF
else:
_LOGGER.debug(
"%s - nothing to do, we are cooling",
)
return AUTO_START_STOP_ACTION_NOTHING
if hvac_mode == HVACMode.OFF and saved_hvac_mode == HVACMode.HEAT:
if current_temp + slope_min * self._dt <= target_temp:
_LOGGER.info(
"%s - We need to start, because it will be time to heat",
)
return AUTO_START_STOP_ACTION_ON
else:
_LOGGER.debug(
"%s - nothing to do, we don't need to heat soon",
)
return AUTO_START_STOP_ACTION_NOTHING
if hvac_mode == HVACMode.OFF and saved_hvac_mode == HVACMode.COOL:
if current_temp + slope_min * self._dt >= target_temp:
_LOGGER.info(
"%s - We need to start, because it will be time to cool",
)
return AUTO_START_STOP_ACTION_ON
else:
_LOGGER.debug(
"%s - nothing to do, we don't need to cool soon",
)
return AUTO_START_STOP_ACTION_NOTHING
_LOGGER.debug(
"%s - nothing to do, no conditions applied",
)
return AUTO_START_STOP_ACTION_NOTHING
def __str__(self) -> str:
return f"AutoStartStopDetectionAlgorithm-{self._vtherm_name}"

View File

@@ -413,6 +413,11 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
]:
menu_options.append("presets")
if self._infos[CONF_THERMOSTAT_TYPE] in [
CONF_THERMOSTAT_CLIMATE,
]:
menu_options.append("auto_start_stop")
if (
is_central_config
and self._infos.get(CONF_USE_CENTRAL_BOILER_FEATURE) is True
@@ -520,17 +525,29 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
"""Handle the Type flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_features user_input=%s", user_input)
schema = STEP_FEATURES_DATA_SCHEMA
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG:
schema = STEP_CENTRAL_FEATURES_DATA_SCHEMA
elif self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CLIMATE:
schema = STEP_CLIMATE_FEATURES_DATA_SCHEMA
return await self.generic_step(
"features",
(
STEP_CENTRAL_FEATURES_DATA_SCHEMA
if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG
else STEP_FEATURES_DATA_SCHEMA
),
schema,
user_input,
self.async_step_menu,
)
async def async_step_auto_start_stop(self, user_input: dict | None = None) -> FlowResult:
""" Handle the Auto start stop step"""
_LOGGER.debug("Into ConfigFlow.async_step_auto_start_stop user_input=%s", user_input)
schema = STEP_AUTO_START_STOP
self._infos[COMES_FROM] = None
next_step = self.async_step_menu
return await self.generic_step("auto_start_stop", schema, user_input, next_step)
async def async_step_tpi(self, user_input: dict | None = None) -> FlowResult:
"""Handle the TPI flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_tpi user_input=%s", user_input)

View File

@@ -68,6 +68,16 @@ STEP_FEATURES_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
}
)
STEP_CLIMATE_FEATURES_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Optional(CONF_USE_WINDOW_FEATURE, default=False): cv.boolean,
vol.Optional(CONF_USE_MOTION_FEATURE, default=False): cv.boolean,
vol.Optional(CONF_USE_POWER_FEATURE, default=False): cv.boolean,
vol.Optional(CONF_USE_PRESENCE_FEATURE, default=False): cv.boolean,
vol.Optional(CONF_USE_AUTO_START_STOP_FEATURE, default=False): cv.boolean,
}
)
STEP_CENTRAL_FEATURES_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Optional(CONF_USE_WINDOW_FEATURE, default=False): cv.boolean,
@@ -196,6 +206,20 @@ STEP_THERMOSTAT_VALVE = vol.Schema( # pylint: disable=invalid-name
}
)
STEP_AUTO_START_STOP = vol.Schema( # pylint: disable=invalid-name
{
vol.Optional(
CONF_AUTO_START_STOP_LEVEL, default=AUTO_START_STOP_LEVEL_NONE
): selector.SelectSelector(
selector.SelectSelectorConfig(
options=CONF_AUTO_START_STOP_LEVELS,
translation_key="auto_start_stop",
mode="dropdown",
)
),
}
)
STEP_TPI_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_USE_TPI_CENTRAL_CONFIG, default=True): cv.boolean,

View File

@@ -97,6 +97,7 @@ CONF_USE_MOTION_FEATURE = "use_motion_feature"
CONF_USE_PRESENCE_FEATURE = "use_presence_feature"
CONF_USE_POWER_FEATURE = "use_power_feature"
CONF_USE_CENTRAL_BOILER_FEATURE = "use_central_boiler_feature"
CONF_USE_AUTO_START_STOP_FEATURE = "use_auto_start_stop_feature"
CONF_AC_MODE = "ac_mode"
CONF_WINDOW_AUTO_OPEN_THRESHOLD = "window_auto_open_threshold"
CONF_WINDOW_AUTO_CLOSE_THRESHOLD = "window_auto_close_threshold"
@@ -145,6 +146,18 @@ CONF_CENTRAL_BOILER_DEACTIVATION_SRV = "central_boiler_deactivation_service"
CONF_USED_BY_CENTRAL_BOILER = "used_by_controls_central_boiler"
CONF_WINDOW_ACTION = "window_action"
CONF_AUTO_START_STOP_LEVEL = "auto_start_stop_level"
AUTO_START_STOP_LEVEL_NONE = "none"
AUTO_START_STOP_LEVEL_SLOW = "slow"
AUTO_START_STOP_LEVEL_MEDIUM = "medium"
AUTO_START_STOP_LEVEL_FAST = "fast"
CONF_AUTO_START_STOP_LEVELS = [
AUTO_START_STOP_LEVEL_NONE,
AUTO_START_STOP_LEVEL_SLOW,
AUTO_START_STOP_LEVEL_MEDIUM,
AUTO_START_STOP_LEVEL_FAST,
]
DEFAULT_SHORT_EMA_PARAMS = {
"max_alpha": 0.5,
# In sec

View File

@@ -27,6 +27,7 @@
"power": "Power management",
"presence": "Presence detection",
"advanced": "Advanced parameters",
"auto_start_stop": "Auto start and stop",
"finalize": "All done",
"configuration_not_complete": "Configuration not complete"
}
@@ -63,7 +64,8 @@
"use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management",
"use_presence_feature": "Use presence detection",
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page"
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page",
"use_auto_start_stop_feature": "Use the auto start and stop feature"
}
},
"type": {
@@ -262,6 +264,7 @@
"power": "Power management",
"presence": "Presence detection",
"advanced": "Advanced parameters",
"auto_start_stop": "Auto start and stop",
"finalize": "All done",
"configuration_not_complete": "Configuration not complete"
}
@@ -298,7 +301,8 @@
"use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management",
"use_presence_feature": "Use presence detection",
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page"
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page",
"use_auto_start_stop_feature": "Use the auto start and stop feature"
}
},
"type": {
@@ -514,6 +518,14 @@
"comfort": "Comfort",
"boost": "Boost"
}
},
"auto_start_stop": {
"options": {
"none": "No auto start/stop",
"slow": "Slow detection",
"medium": "Medium detection",
"fast": "Fast detection"
}
}
},
"entity": {

View File

@@ -27,6 +27,7 @@
"power": "Power management",
"presence": "Presence detection",
"advanced": "Advanced parameters",
"auto_start_stop": "Auto start and stop",
"finalize": "All done",
"configuration_not_complete": "Configuration not complete"
}
@@ -63,7 +64,8 @@
"use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management",
"use_presence_feature": "Use presence detection",
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page"
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page",
"use_auto_start_stop_feature": "Use the auto start and stop feature"
}
},
"type": {
@@ -262,6 +264,7 @@
"power": "Power management",
"presence": "Presence detection",
"advanced": "Advanced parameters",
"auto_start_stop": "Auto start and stop",
"finalize": "All done",
"configuration_not_complete": "Configuration not complete"
}
@@ -298,7 +301,8 @@
"use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management",
"use_presence_feature": "Use presence detection",
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page"
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page",
"use_auto_start_stop_feature": "Use the auto start and stop feature"
}
},
"type": {
@@ -514,6 +518,14 @@
"comfort": "Comfort",
"boost": "Boost"
}
},
"auto_start_stop": {
"options": {
"none": "No auto start/stop",
"slow": "Slow detection",
"medium": "Medium detection",
"fast": "Fast detection"
}
}
},
"entity": {

View File

@@ -27,6 +27,7 @@
"power": "Gestion de la puissance",
"presence": "Détection de présence",
"advanced": "Paramètres avancés",
"auto_start_stop": "Allumage/extinction automatique",
"finalize": "Finaliser la création",
"configuration_not_complete": "Configuration incomplète"
}
@@ -63,7 +64,8 @@
"use_motion_feature": "Avec détection de mouvement",
"use_power_feature": "Avec gestion de la puissance",
"use_presence_feature": "Avec détection de présence",
"use_central_boiler_feature": "Ajouter une chaudière centrale. Cochez pour ajouter un controle sur une chaudière centrale. Vous devrez ensuite configurer les VTherms qui commande la chaudière centrale pour que cette option prenne effet. Si au moins un des VTherm a besoin de chauffer, la chaudière centrale sera activée. Si aucun VTherm n'a besoin de chauffer, elle sera éteinte. Les commandes pour allumer/éteindre la chaudière centrale sont données dans la page de configuration suivante."
"use_central_boiler_feature": "Ajouter une chaudière centrale. Cochez pour ajouter un controle sur une chaudière centrale. Vous devrez ensuite configurer les VTherms qui commande la chaudière centrale pour que cette option prenne effet. Si au moins un des VTherm a besoin de chauffer, la chaudière centrale sera activée. Si aucun VTherm n'a besoin de chauffer, elle sera éteinte. Les commandes pour allumer/éteindre la chaudière centrale sont données dans la page de configuration suivante.",
"use_auto_start_stop_feature": "Avec démarrage et extinction automatique"
}
},
"type": {
@@ -274,6 +276,7 @@
"power": "Gestion de la puissance",
"presence": "Détection de présence",
"advanced": "Paramètres avancés",
"auto_start_stop": "Allumage/extinction automatique",
"finalize": "Finaliser les modifications",
"configuration_not_complete": "Configuration incomplète"
}
@@ -310,7 +313,8 @@
"use_motion_feature": "Avec détection de mouvement",
"use_power_feature": "Avec gestion de la puissance",
"use_presence_feature": "Avec détection de présence",
"use_central_boiler_feature": "Ajouter une chaudière centrale. Cochez pour ajouter un controle sur une chaudière centrale. Vous devrez ensuite configurer les VTherms qui commande la chaudière centrale pour que cette option prenne effet. Si au moins un des VTherm a besoin de chauffer, la chaudière centrale sera activée. Si aucun VTherm n'a besoin de chauffer, elle sera éteinte. Les commandes pour allumer/éteindre la chaudière centrale sont données dans la page de configuration suivante."
"use_central_boiler_feature": "Ajouter une chaudière centrale. Cochez pour ajouter un controle sur une chaudière centrale. Vous devrez ensuite configurer les VTherms qui commande la chaudière centrale pour que cette option prenne effet. Si au moins un des VTherm a besoin de chauffer, la chaudière centrale sera activée. Si aucun VTherm n'a besoin de chauffer, elle sera éteinte. Les commandes pour allumer/éteindre la chaudière centrale sont données dans la page de configuration suivante.",
"use_auto_start_stop_feature": "Avec démarrage et extinction automatique"
}
},
"type": {
@@ -532,6 +536,14 @@
"comfort": "Confort",
"boost": "Renforcé (boost)"
}
},
"auto_start_stop": {
"options": {
"none": "No auto start/stop",
"slow": "Slow detection",
"medium": "Medium detection",
"fast": "Fast detection"
}
}
},
"entity": {

View File

@@ -0,0 +1,583 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, unused-variable
""" Test the Auto Start Stop algorithm management """
from datetime import datetime, timedelta
import logging
from unittest.mock import patch
from homeassistant.components.climate import HVACMode
from custom_components.versatile_thermostat.thermostat_climate import (
ThermostatOverClimate,
)
from custom_components.versatile_thermostat.auto_start_stop_algorithm import (
AutoStartStopDetectionAlgorithm,
AUTO_START_STOP_ACTION_NOTHING,
AUTO_START_STOP_ACTION_OFF,
AUTO_START_STOP_ACTION_ON,
)
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
logging.getLogger().setLevel(logging.DEBUG)
async def test_auto_start_stop_algo_slow(hass: HomeAssistant):
"""Testing directly the algorithm in Slow level"""
algo: AutoStartStopDetectionAlgorithm = AutoStartStopDetectionAlgorithm(
AUTO_START_STOP_LEVEL_SLOW, "testu"
)
assert algo._dtemp == 3
assert algo._dt == 30
assert algo._vtherm_name == "testu"
# 1. In heating we should stop
ret = algo.calculate_action(
hvac_mode=HVACMode.HEAT,
saved_hvac_mode=HVACMode.OFF,
regulated_temp=18,
target_temp=21,
current_temp=22,
slope_min=0.1,
)
assert ret == AUTO_START_STOP_ACTION_OFF
# 2. In heating we should do nothing
ret = algo.calculate_action(
hvac_mode=HVACMode.HEAT,
saved_hvac_mode=HVACMode.OFF,
regulated_temp=20,
target_temp=21,
current_temp=21,
slope_min=0.0,
)
assert ret == AUTO_START_STOP_ACTION_NOTHING
# 3. In Cooling we should stop
ret = algo.calculate_action(
hvac_mode=HVACMode.COOL,
saved_hvac_mode=HVACMode.OFF,
regulated_temp=24,
target_temp=21,
current_temp=22,
slope_min=-0.1,
)
assert ret == AUTO_START_STOP_ACTION_OFF
# 4. In Colling we should do nothing
ret = algo.calculate_action(
hvac_mode=HVACMode.COOL,
saved_hvac_mode=HVACMode.OFF,
regulated_temp=22,
target_temp=21,
current_temp=22,
slope_min=0.0,
)
assert ret == AUTO_START_STOP_ACTION_NOTHING
# 5. In Off, we should start heating
ret = algo.calculate_action(
hvac_mode=HVACMode.OFF,
saved_hvac_mode=HVACMode.HEAT,
regulated_temp=22,
target_temp=21,
current_temp=22,
slope_min=-0.1,
)
assert ret == AUTO_START_STOP_ACTION_ON
# 6. In Off we should not heat
ret = algo.calculate_action(
hvac_mode=HVACMode.OFF,
saved_hvac_mode=HVACMode.HEAT,
regulated_temp=23,
target_temp=21,
current_temp=24,
slope_min=0.5,
)
assert ret == AUTO_START_STOP_ACTION_NOTHING
# 7. In Off we still should not heat (slope too low)
ret = algo.calculate_action(
hvac_mode=HVACMode.OFF,
saved_hvac_mode=HVACMode.HEAT,
regulated_temp=22,
target_temp=21,
current_temp=22,
slope_min=-0.01,
)
assert ret == AUTO_START_STOP_ACTION_NOTHING
# 8. In Off, we should start cooling
ret = algo.calculate_action(
hvac_mode=HVACMode.OFF,
saved_hvac_mode=HVACMode.COOL,
regulated_temp=25,
target_temp=24,
current_temp=25,
slope_min=0.1,
)
assert ret == AUTO_START_STOP_ACTION_ON
# 9. In Off we should not cool
ret = algo.calculate_action(
hvac_mode=HVACMode.OFF,
saved_hvac_mode=HVACMode.COOL,
regulated_temp=20,
target_temp=24,
current_temp=21,
slope_min=0.01,
)
assert ret == AUTO_START_STOP_ACTION_NOTHING
# 9.1 In Off and slow we should cool
ret = algo.calculate_action(
hvac_mode=HVACMode.OFF,
saved_hvac_mode=HVACMode.COOL,
regulated_temp=20,
target_temp=24,
current_temp=21,
slope_min=0.1,
)
assert ret == AUTO_START_STOP_ACTION_ON
# 10. In Off we still should not cool (slope too low)
ret = algo.calculate_action(
hvac_mode=HVACMode.OFF,
saved_hvac_mode=HVACMode.COOL,
regulated_temp=25,
target_temp=24,
current_temp=23,
slope_min=0.01,
)
assert ret == AUTO_START_STOP_ACTION_NOTHING
async def test_auto_start_stop_algo_medium(hass: HomeAssistant):
"""Testing directly the algorithm in Slow level"""
algo: AutoStartStopDetectionAlgorithm = AutoStartStopDetectionAlgorithm(
AUTO_START_STOP_LEVEL_MEDIUM, "testu"
)
assert algo._dtemp == 2
assert algo._dt == 15
assert algo._vtherm_name == "testu"
# 1. In heating we should stop
ret = algo.calculate_action(
hvac_mode=HVACMode.HEAT,
saved_hvac_mode=HVACMode.OFF,
regulated_temp=18,
target_temp=21,
current_temp=22,
slope_min=0.1,
)
assert ret == AUTO_START_STOP_ACTION_OFF
# 2. In heating we should do nothing
ret = algo.calculate_action(
hvac_mode=HVACMode.HEAT,
saved_hvac_mode=HVACMode.OFF,
regulated_temp=20,
target_temp=21,
current_temp=21,
slope_min=0.0,
)
assert ret == AUTO_START_STOP_ACTION_NOTHING
# 3. In Cooling we should stop
ret = algo.calculate_action(
hvac_mode=HVACMode.COOL,
saved_hvac_mode=HVACMode.OFF,
regulated_temp=24,
target_temp=21,
current_temp=22,
slope_min=-0.1,
)
assert ret == AUTO_START_STOP_ACTION_OFF
# 4. In Colling we should do nothing
ret = algo.calculate_action(
hvac_mode=HVACMode.COOL,
saved_hvac_mode=HVACMode.OFF,
regulated_temp=22,
target_temp=21,
current_temp=22,
slope_min=0.0,
)
assert ret == AUTO_START_STOP_ACTION_NOTHING
# 5. In Off, we should start heating
ret = algo.calculate_action(
hvac_mode=HVACMode.OFF,
saved_hvac_mode=HVACMode.HEAT,
regulated_temp=22,
target_temp=21,
current_temp=22,
slope_min=-0.1,
)
assert ret == AUTO_START_STOP_ACTION_ON
# 6. In Off we should not heat
ret = algo.calculate_action(
hvac_mode=HVACMode.OFF,
saved_hvac_mode=HVACMode.HEAT,
regulated_temp=23,
target_temp=21,
current_temp=24,
slope_min=0.5,
)
assert ret == AUTO_START_STOP_ACTION_NOTHING
# 7. In Off we still should not heat (slope too low)
ret = algo.calculate_action(
hvac_mode=HVACMode.OFF,
saved_hvac_mode=HVACMode.HEAT,
regulated_temp=22,
target_temp=21,
current_temp=22,
slope_min=-0.01,
)
assert ret == AUTO_START_STOP_ACTION_NOTHING
# 8. In Off, we should start cooling
ret = algo.calculate_action(
hvac_mode=HVACMode.OFF,
saved_hvac_mode=HVACMode.COOL,
regulated_temp=25,
target_temp=24,
current_temp=25,
slope_min=0.1,
)
assert ret == AUTO_START_STOP_ACTION_ON
# 9. In Off we should not cool
ret = algo.calculate_action(
hvac_mode=HVACMode.OFF,
saved_hvac_mode=HVACMode.COOL,
regulated_temp=20,
target_temp=24,
current_temp=21,
slope_min=0.1,
)
assert ret == AUTO_START_STOP_ACTION_NOTHING
# 10. In Off we still should not cool (slope too low)
ret = algo.calculate_action(
hvac_mode=HVACMode.OFF,
saved_hvac_mode=HVACMode.COOL,
regulated_temp=25,
target_temp=24,
current_temp=23,
slope_min=0.01,
)
assert ret == AUTO_START_STOP_ACTION_NOTHING
async def test_auto_start_stop_algo_high(hass: HomeAssistant):
"""Testing directly the algorithm in Slow level"""
algo: AutoStartStopDetectionAlgorithm = AutoStartStopDetectionAlgorithm(
AUTO_START_STOP_LEVEL_FAST, "testu"
)
assert algo._dtemp == 1
assert algo._dt == 7
assert algo._vtherm_name == "testu"
# 1. In heating we should stop
ret = algo.calculate_action(
hvac_mode=HVACMode.HEAT,
saved_hvac_mode=HVACMode.OFF,
regulated_temp=18,
target_temp=21,
current_temp=22,
slope_min=0.1,
)
assert ret == AUTO_START_STOP_ACTION_OFF
# 2. In heating and fast we should turn off
ret = algo.calculate_action(
hvac_mode=HVACMode.HEAT,
saved_hvac_mode=HVACMode.OFF,
regulated_temp=20,
target_temp=21,
current_temp=21,
slope_min=0.0,
)
assert ret == AUTO_START_STOP_ACTION_OFF
# 3. In Cooling we should stop
ret = algo.calculate_action(
hvac_mode=HVACMode.COOL,
saved_hvac_mode=HVACMode.OFF,
regulated_temp=24,
target_temp=21,
current_temp=22,
slope_min=-0.1,
)
assert ret == AUTO_START_STOP_ACTION_OFF
# 4. In Cooling and fast we should turn off
ret = algo.calculate_action(
hvac_mode=HVACMode.COOL,
saved_hvac_mode=HVACMode.OFF,
regulated_temp=22,
target_temp=21,
current_temp=22,
slope_min=0.0,
)
assert ret == AUTO_START_STOP_ACTION_OFF
# 5. In Off and fast , we should do nothing
ret = algo.calculate_action(
hvac_mode=HVACMode.OFF,
saved_hvac_mode=HVACMode.HEAT,
regulated_temp=22,
target_temp=21,
current_temp=22,
slope_min=-0.1,
)
assert ret == AUTO_START_STOP_ACTION_NOTHING
# 6. In Off we should not heat
ret = algo.calculate_action(
hvac_mode=HVACMode.OFF,
saved_hvac_mode=HVACMode.HEAT,
regulated_temp=23,
target_temp=21,
current_temp=24,
slope_min=0.5,
)
assert ret == AUTO_START_STOP_ACTION_NOTHING
# 7. In Off we still should not heat (slope too low)
ret = algo.calculate_action(
hvac_mode=HVACMode.OFF,
saved_hvac_mode=HVACMode.HEAT,
regulated_temp=22,
target_temp=21,
current_temp=22,
slope_min=-0.01,
)
assert ret == AUTO_START_STOP_ACTION_NOTHING
# 8. In Off, we should start cooling
ret = algo.calculate_action(
hvac_mode=HVACMode.OFF,
saved_hvac_mode=HVACMode.COOL,
regulated_temp=25,
target_temp=24,
current_temp=25,
slope_min=0.1,
)
assert ret == AUTO_START_STOP_ACTION_ON
# 9. In Off we should not cool
ret = algo.calculate_action(
hvac_mode=HVACMode.OFF,
saved_hvac_mode=HVACMode.COOL,
regulated_temp=20,
target_temp=24,
current_temp=21,
slope_min=0.1,
)
assert ret == AUTO_START_STOP_ACTION_NOTHING
# 10. In Off we still should not cool (slope too low)
ret = algo.calculate_action(
hvac_mode=HVACMode.OFF,
saved_hvac_mode=HVACMode.COOL,
regulated_temp=25,
target_temp=24,
current_temp=23,
slope_min=0.01,
)
assert ret == AUTO_START_STOP_ACTION_NOTHING
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_auto_start_stop_none_vtherm(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test than auto-start/stop is disabled with a real over_climate VTherm in NONE level"""
# 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_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_NONE,
},
)
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")
# 1. Vtherm auto-start/stop should be in MEDIUM mode
assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_NONE
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_auto_start_stop_medium_vtherm(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test than auto-start/stop works with a real over_climate VTherm in MEDIUM level"""
# 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_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_MEDIUM,
},
)
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")
# 1. Vtherm auto-start/stop should be in MEDIUM mode
assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_MEDIUM
# 1. Set mode to Heat and preset to Comfort
await send_presence_change_event(vtherm, True, False, datetime.now())
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
await vtherm.async_set_preset_mode(PRESET_COMFORT)
await hass.async_block_till_done()
assert vtherm.target_temperature == 19.0
# 2. Only change the HVAC_MODE (and keep preset to comfort)
await vtherm.async_set_hvac_mode(HVACMode.COOL)
await hass.async_block_till_done()
assert vtherm.target_temperature == 25.0
# 3. Only change the HVAC_MODE (and keep preset to comfort)
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
await hass.async_block_till_done()
assert vtherm.target_temperature == 19.0
# 4. Change presence to off
await send_presence_change_event(vtherm, False, True, datetime.now())
await hass.async_block_till_done()
assert vtherm.target_temperature == 19.1
# 5. Change hvac_mode to AC
await vtherm.async_set_hvac_mode(HVACMode.COOL)
await hass.async_block_till_done()
assert vtherm.target_temperature == 25.1
# 6. Change presence to on
await send_presence_change_event(vtherm, True, False, datetime.now())
await hass.async_block_till_done()
assert vtherm.target_temperature == 25