Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
This commit is contained in:
@@ -57,10 +57,13 @@ class AutoStartStopDetectionAlgorithm:
|
|||||||
_accumulated_error: float = 0
|
_accumulated_error: float = 0
|
||||||
_error_threshold: float | None = None
|
_error_threshold: float | None = None
|
||||||
_last_calculation_date: datetime | None = None
|
_last_calculation_date: datetime | None = None
|
||||||
|
_last_switch_date: datetime | None = None
|
||||||
|
|
||||||
def __init__(self, level: TYPE_AUTO_START_STOP_LEVELS, vtherm_name) -> None:
|
def __init__(self, level: TYPE_AUTO_START_STOP_LEVELS, vtherm_name) -> None:
|
||||||
"""Initalize a new algorithm with the right constants"""
|
"""Initalize a new algorithm with the right constants"""
|
||||||
self._vtherm_name = vtherm_name
|
self._vtherm_name = vtherm_name
|
||||||
|
self._last_calculation_date = None
|
||||||
|
self._last_switch_date = None
|
||||||
self._init_level(level)
|
self._init_level(level)
|
||||||
|
|
||||||
def _init_level(self, level: TYPE_AUTO_START_STOP_LEVELS):
|
def _init_level(self, level: TYPE_AUTO_START_STOP_LEVELS):
|
||||||
@@ -143,17 +146,26 @@ class AutoStartStopDetectionAlgorithm:
|
|||||||
|
|
||||||
temp_at_dt = current_temp + slope_min * self._dt
|
temp_at_dt = current_temp + slope_min * self._dt
|
||||||
|
|
||||||
|
# Calculate the number of minute from last_switch
|
||||||
|
nb_minutes_since_last_switch = 999
|
||||||
|
if self._last_switch_date is not None:
|
||||||
|
nb_minutes_since_last_switch = (
|
||||||
|
now - self._last_switch_date
|
||||||
|
).total_seconds() / 60
|
||||||
|
|
||||||
# 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 (
|
if (
|
||||||
self._accumulated_error <= -self._error_threshold
|
self._accumulated_error <= -self._error_threshold
|
||||||
and temp_at_dt >= target_temp + TEMP_HYSTERESIS
|
and temp_at_dt >= target_temp + TEMP_HYSTERESIS
|
||||||
|
and nb_minutes_since_last_switch >= self._dt
|
||||||
):
|
):
|
||||||
_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,
|
||||||
)
|
)
|
||||||
|
self._last_switch_date = now
|
||||||
return AUTO_START_STOP_ACTION_OFF
|
return AUTO_START_STOP_ACTION_OFF
|
||||||
else:
|
else:
|
||||||
_LOGGER.debug("%s - nothing to do, we are heating", self)
|
_LOGGER.debug("%s - nothing to do, we are heating", self)
|
||||||
@@ -163,11 +175,13 @@ class AutoStartStopDetectionAlgorithm:
|
|||||||
if (
|
if (
|
||||||
self._accumulated_error >= self._error_threshold
|
self._accumulated_error >= self._error_threshold
|
||||||
and temp_at_dt <= target_temp - TEMP_HYSTERESIS
|
and temp_at_dt <= target_temp - TEMP_HYSTERESIS
|
||||||
|
and nb_minutes_since_last_switch >= self._dt
|
||||||
):
|
):
|
||||||
_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,
|
||||||
)
|
)
|
||||||
|
self._last_switch_date = now
|
||||||
return AUTO_START_STOP_ACTION_OFF
|
return AUTO_START_STOP_ACTION_OFF
|
||||||
else:
|
else:
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
@@ -178,11 +192,15 @@ 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 temp_at_dt <= target_temp - TEMP_HYSTERESIS:
|
if (
|
||||||
|
temp_at_dt <= target_temp - TEMP_HYSTERESIS
|
||||||
|
and nb_minutes_since_last_switch >= self._dt
|
||||||
|
):
|
||||||
_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,
|
||||||
)
|
)
|
||||||
|
self._last_switch_date = now
|
||||||
return AUTO_START_STOP_ACTION_ON
|
return AUTO_START_STOP_ACTION_ON
|
||||||
else:
|
else:
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
@@ -192,11 +210,15 @@ 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 temp_at_dt >= target_temp + TEMP_HYSTERESIS:
|
if (
|
||||||
|
temp_at_dt >= target_temp + TEMP_HYSTERESIS
|
||||||
|
and nb_minutes_since_last_switch >= self._dt
|
||||||
|
):
|
||||||
_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,
|
||||||
)
|
)
|
||||||
|
self._last_switch_date = now
|
||||||
return AUTO_START_STOP_ACTION_ON
|
return AUTO_START_STOP_ACTION_ON
|
||||||
else:
|
else:
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
@@ -235,5 +257,10 @@ class AutoStartStopDetectionAlgorithm:
|
|||||||
"""Get the level value"""
|
"""Get the level value"""
|
||||||
return self._level
|
return self._level
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_switch_date(self) -> datetime | None:
|
||||||
|
"""Get the last of the last switch"""
|
||||||
|
return self._last_switch_date
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"AutoStartStopDetectionAlgorithm-{self._vtherm_name}"
|
return f"AutoStartStopDetectionAlgorithm-{self._vtherm_name}"
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
|||||||
"auto_start_stop_enable",
|
"auto_start_stop_enable",
|
||||||
"auto_start_stop_accumulated_error",
|
"auto_start_stop_accumulated_error",
|
||||||
"auto_start_stop_accumulated_error_threshold",
|
"auto_start_stop_accumulated_error_threshold",
|
||||||
|
"auto_start_stop_last_switch_date",
|
||||||
"follow_underlying_temp_change",
|
"follow_underlying_temp_change",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -555,6 +556,10 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
|||||||
"auto_start_stop_accumulated_error_threshold"
|
"auto_start_stop_accumulated_error_threshold"
|
||||||
] = self._auto_start_stop_algo.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._attr_extra_state_attributes["follow_underlying_temp_change"] = (
|
||||||
self._follow_underlying_temp_change
|
self._follow_underlying_temp_change
|
||||||
)
|
)
|
||||||
@@ -1114,15 +1119,6 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
|||||||
|
|
||||||
return self._support_flags
|
return self._support_flags
|
||||||
|
|
||||||
# We keep the step configured for the VTherm and not the step of the underlying
|
|
||||||
# @property
|
|
||||||
# def target_temperature_step(self) -> float | None:
|
|
||||||
# """Return the supported step of target temperature."""
|
|
||||||
# if self.underlying_entity(0):
|
|
||||||
# return self.underlying_entity(0).target_temperature_step
|
|
||||||
#
|
|
||||||
# return None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_temperature_high(self) -> float | None:
|
def target_temperature_high(self) -> float | None:
|
||||||
"""Return the highbound target temperature we try to reach.
|
"""Return the highbound target temperature we try to reach.
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ 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
|
||||||
|
|
||||||
@@ -44,6 +45,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
|
||||||
assert algo.accumulated_error == -1
|
assert algo.accumulated_error == -1
|
||||||
|
assert algo.last_switch_date is None
|
||||||
|
|
||||||
# 2. should not stop (accumulated_error too low)
|
# 2. should not stop (accumulated_error too low)
|
||||||
now = now + timedelta(minutes=5)
|
now = now + timedelta(minutes=5)
|
||||||
@@ -57,6 +59,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
|
||||||
assert algo.accumulated_error == -6
|
assert algo.accumulated_error == -6
|
||||||
|
assert algo.last_switch_date is None
|
||||||
|
|
||||||
# 3. should not stop (accumulated_error too low)
|
# 3. should not stop (accumulated_error too low)
|
||||||
now = now + timedelta(minutes=2)
|
now = now + timedelta(minutes=2)
|
||||||
@@ -70,6 +73,7 @@ async def test_auto_start_stop_algo_slow_heat_off(hass: HomeAssistant):
|
|||||||
)
|
)
|
||||||
assert algo.accumulated_error == -8
|
assert algo.accumulated_error == -8
|
||||||
assert ret == AUTO_START_STOP_ACTION_NOTHING
|
assert ret == AUTO_START_STOP_ACTION_NOTHING
|
||||||
|
assert algo.last_switch_date is None
|
||||||
|
|
||||||
# 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(seconds=11)
|
now = now + timedelta(seconds=11)
|
||||||
@@ -83,6 +87,7 @@ async def test_auto_start_stop_algo_slow_heat_off(hass: HomeAssistant):
|
|||||||
)
|
)
|
||||||
assert algo.accumulated_error == -8
|
assert algo.accumulated_error == -8
|
||||||
assert ret == AUTO_START_STOP_ACTION_NOTHING
|
assert ret == AUTO_START_STOP_ACTION_NOTHING
|
||||||
|
assert algo.last_switch_date is None
|
||||||
|
|
||||||
# 5. should stop now because accumulated_error is > ERROR_THRESHOLD for slow (10)
|
# 5. should stop now because accumulated_error is > ERROR_THRESHOLD for slow (10)
|
||||||
now = now + timedelta(minutes=4)
|
now = now + timedelta(minutes=4)
|
||||||
@@ -96,6 +101,9 @@ async def test_auto_start_stop_algo_slow_heat_off(hass: HomeAssistant):
|
|||||||
)
|
)
|
||||||
assert algo.accumulated_error == -10
|
assert algo.accumulated_error == -10
|
||||||
assert ret == AUTO_START_STOP_ACTION_OFF
|
assert ret == AUTO_START_STOP_ACTION_OFF
|
||||||
|
assert algo.last_switch_date is not None
|
||||||
|
assert algo.last_switch_date == now
|
||||||
|
last_now = now
|
||||||
|
|
||||||
# 6. inverse the temperature (target > current) -> accumulated_error should be divided by 2
|
# 6. inverse the temperature (target > current) -> accumulated_error should be divided by 2
|
||||||
now = now + timedelta(minutes=2)
|
now = now + timedelta(minutes=2)
|
||||||
@@ -109,14 +117,111 @@ async def test_auto_start_stop_algo_slow_heat_off(hass: HomeAssistant):
|
|||||||
)
|
)
|
||||||
assert algo.accumulated_error == -4 # -10/2 + 1
|
assert algo.accumulated_error == -4 # -10/2 + 1
|
||||||
assert ret == AUTO_START_STOP_ACTION_NOTHING
|
assert ret == AUTO_START_STOP_ACTION_NOTHING
|
||||||
|
assert algo.last_switch_date == last_now
|
||||||
|
|
||||||
# 7. change level to slow (no real change) -> error_accumulated should not reset to 0
|
# 7. change level to slow (no real change) -> error_accumulated should not reset to 0
|
||||||
algo.set_level(AUTO_START_STOP_LEVEL_SLOW)
|
algo.set_level(AUTO_START_STOP_LEVEL_SLOW)
|
||||||
assert algo.accumulated_error == -4
|
assert algo.accumulated_error == -4
|
||||||
|
assert algo.last_switch_date == last_now
|
||||||
|
|
||||||
# 8. change level -> error_accumulated should reset to 0
|
# 8. change level -> error_accumulated should reset to 0
|
||||||
algo.set_level(AUTO_START_STOP_LEVEL_FAST)
|
algo.set_level(AUTO_START_STOP_LEVEL_FAST)
|
||||||
assert algo.accumulated_error == 0
|
assert algo.accumulated_error == 0
|
||||||
|
assert algo.last_switch_date == last_now
|
||||||
|
|
||||||
|
|
||||||
|
async def test_auto_start_stop_too_fast_change(hass: HomeAssistant):
|
||||||
|
"""Testing directly the algorithm in Slow level"""
|
||||||
|
algo: AutoStartStopDetectionAlgorithm = AutoStartStopDetectionAlgorithm(
|
||||||
|
AUTO_START_STOP_LEVEL_SLOW, "testu"
|
||||||
|
)
|
||||||
|
|
||||||
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||||
|
now: datetime = datetime.now(tz=tz)
|
||||||
|
|
||||||
|
assert algo._dt == 30
|
||||||
|
assert algo._vtherm_name == "testu"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Testing with turn_on
|
||||||
|
#
|
||||||
|
|
||||||
|
# 1. should stop
|
||||||
|
algo._accumulated_error = -100
|
||||||
|
ret = algo.calculate_action(
|
||||||
|
hvac_mode=HVACMode.HEAT,
|
||||||
|
saved_hvac_mode=HVACMode.OFF,
|
||||||
|
target_temp=10,
|
||||||
|
current_temp=21,
|
||||||
|
slope_min=0.5,
|
||||||
|
now=now,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert ret == AUTO_START_STOP_ACTION_OFF
|
||||||
|
assert algo.last_switch_date is not None
|
||||||
|
assert algo.last_switch_date == now
|
||||||
|
last_now = now
|
||||||
|
|
||||||
|
# 2. now we should turn on but to near the last change -> no nothing to do
|
||||||
|
now = now + timedelta(minutes=2)
|
||||||
|
algo._accumulated_error = -100
|
||||||
|
ret = algo.calculate_action(
|
||||||
|
hvac_mode=HVACMode.OFF,
|
||||||
|
saved_hvac_mode=HVACMode.HEAT,
|
||||||
|
target_temp=21,
|
||||||
|
current_temp=17,
|
||||||
|
slope_min=-0.1,
|
||||||
|
now=now,
|
||||||
|
)
|
||||||
|
assert ret == AUTO_START_STOP_ACTION_NOTHING
|
||||||
|
assert algo.last_switch_date == last_now
|
||||||
|
|
||||||
|
# 3. now we should turn on and now is much later ->
|
||||||
|
now = now + timedelta(minutes=30)
|
||||||
|
algo._accumulated_error = -100
|
||||||
|
ret = algo.calculate_action(
|
||||||
|
hvac_mode=HVACMode.OFF,
|
||||||
|
saved_hvac_mode=HVACMode.HEAT,
|
||||||
|
target_temp=21,
|
||||||
|
current_temp=17,
|
||||||
|
slope_min=-0.1,
|
||||||
|
now=now,
|
||||||
|
)
|
||||||
|
assert ret == AUTO_START_STOP_ACTION_ON
|
||||||
|
assert algo.last_switch_date == now
|
||||||
|
last_now = now
|
||||||
|
|
||||||
|
#
|
||||||
|
# Testing with turn_off
|
||||||
|
#
|
||||||
|
|
||||||
|
# 4. try to turn_off but too speed (29 min)
|
||||||
|
now = now + timedelta(minutes=29)
|
||||||
|
algo._accumulated_error = -100
|
||||||
|
ret = algo.calculate_action(
|
||||||
|
hvac_mode=HVACMode.HEAT,
|
||||||
|
saved_hvac_mode=HVACMode.OFF,
|
||||||
|
target_temp=17,
|
||||||
|
current_temp=21,
|
||||||
|
slope_min=0.5,
|
||||||
|
now=now,
|
||||||
|
)
|
||||||
|
assert ret == AUTO_START_STOP_ACTION_NOTHING
|
||||||
|
assert algo.last_switch_date == last_now
|
||||||
|
|
||||||
|
# 5. turn_off much later (29 min + 1 min)
|
||||||
|
now = now + timedelta(minutes=1)
|
||||||
|
algo._accumulated_error = -100
|
||||||
|
ret = algo.calculate_action(
|
||||||
|
hvac_mode=HVACMode.HEAT,
|
||||||
|
saved_hvac_mode=HVACMode.OFF,
|
||||||
|
target_temp=17,
|
||||||
|
current_temp=21,
|
||||||
|
slope_min=0.5,
|
||||||
|
now=now,
|
||||||
|
)
|
||||||
|
assert ret == AUTO_START_STOP_ACTION_OFF
|
||||||
|
assert algo.last_switch_date == now
|
||||||
|
|
||||||
|
|
||||||
async def test_auto_start_stop_algo_medium_cool_off(hass: HomeAssistant):
|
async def test_auto_start_stop_algo_medium_cool_off(hass: HomeAssistant):
|
||||||
|
|||||||
Reference in New Issue
Block a user