Skip to content

Commit

Permalink
Issue #625 - auto-start/stop change sometimes too fast (#693)
Browse files Browse the repository at this point in the history
Co-authored-by: Jean-Marc Collin <[email protected]>
  • Loading branch information
jmcollin78 and Jean-Marc Collin authored Dec 8, 2024
1 parent 980c24c commit e9dcb21
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,13 @@ class AutoStartStopDetectionAlgorithm:
_accumulated_error: float = 0
_error_threshold: float | 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:
"""Initalize a new algorithm with the right constants"""
self._vtherm_name = vtherm_name
self._last_calculation_date = None
self._last_switch_date = None
self._init_level(level)

def _init_level(self, level: TYPE_AUTO_START_STOP_LEVELS):
Expand Down Expand Up @@ -143,17 +146,26 @@ def calculate_action(

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
# When we hit the threshold, that mean we can turn off
if hvac_mode == HVACMode.HEAT:
if (
self._accumulated_error <= -self._error_threshold
and temp_at_dt >= target_temp + TEMP_HYSTERESIS
and nb_minutes_since_last_switch >= self._dt
):
_LOGGER.info(
"%s - We need to stop, there is no need for heating for a long time.",
self,
)
self._last_switch_date = now
return AUTO_START_STOP_ACTION_OFF
else:
_LOGGER.debug("%s - nothing to do, we are heating", self)
Expand All @@ -163,11 +175,13 @@ def calculate_action(
if (
self._accumulated_error >= self._error_threshold
and temp_at_dt <= target_temp - TEMP_HYSTERESIS
and nb_minutes_since_last_switch >= self._dt
):
_LOGGER.info(
"%s - We need to stop, there is no need for cooling for a long time.",
self,
)
self._last_switch_date = now
return AUTO_START_STOP_ACTION_OFF
else:
_LOGGER.debug(
Expand All @@ -178,11 +192,15 @@ def calculate_action(

# check to turn on
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(
"%s - We need to start, because it will be time to heat",
self,
)
self._last_switch_date = now
return AUTO_START_STOP_ACTION_ON
else:
_LOGGER.debug(
Expand All @@ -192,11 +210,15 @@ def calculate_action(
return AUTO_START_STOP_ACTION_NOTHING

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(
"%s - We need to start, because it will be time to cool",
self,
)
self._last_switch_date = now
return AUTO_START_STOP_ACTION_ON
else:
_LOGGER.debug(
Expand Down Expand Up @@ -235,5 +257,10 @@ def level(self) -> TYPE_AUTO_START_STOP_LEVELS:
"""Get the level value"""
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:
return f"AutoStartStopDetectionAlgorithm-{self._vtherm_name}"
14 changes: 5 additions & 9 deletions custom_components/versatile_thermostat/thermostat_climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
"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",
}
)
Expand Down Expand Up @@ -555,6 +556,10 @@ def update_custom_attributes(self):
"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
)
Expand Down Expand Up @@ -1114,15 +1119,6 @@ def supported_features(self):

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
def target_temperature_high(self) -> float | None:
"""Return the highbound target temperature we try to reach.
Expand Down
105 changes: 105 additions & 0 deletions tests/test_auto_start_stop.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
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

Expand Down Expand Up @@ -44,6 +45,7 @@ async def test_auto_start_stop_algo_slow_heat_off(hass: HomeAssistant):
)
assert ret == AUTO_START_STOP_ACTION_NOTHING
assert algo.accumulated_error == -1
assert algo.last_switch_date is None

# 2. should not stop (accumulated_error too low)
now = now + timedelta(minutes=5)
Expand All @@ -57,6 +59,7 @@ async def test_auto_start_stop_algo_slow_heat_off(hass: HomeAssistant):
)
assert ret == AUTO_START_STOP_ACTION_NOTHING
assert algo.accumulated_error == -6
assert algo.last_switch_date is None

# 3. should not stop (accumulated_error too low)
now = now + timedelta(minutes=2)
Expand All @@ -70,6 +73,7 @@ async def test_auto_start_stop_algo_slow_heat_off(hass: HomeAssistant):
)
assert algo.accumulated_error == -8
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
now = now + timedelta(seconds=11)
Expand All @@ -83,6 +87,7 @@ async def test_auto_start_stop_algo_slow_heat_off(hass: HomeAssistant):
)
assert algo.accumulated_error == -8
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)
now = now + timedelta(minutes=4)
Expand All @@ -96,6 +101,9 @@ async def test_auto_start_stop_algo_slow_heat_off(hass: HomeAssistant):
)
assert algo.accumulated_error == -10
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
now = now + timedelta(minutes=2)
Expand All @@ -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 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
algo.set_level(AUTO_START_STOP_LEVEL_SLOW)
assert algo.accumulated_error == -4
assert algo.last_switch_date == last_now

# 8. change level -> error_accumulated should reset to 0
algo.set_level(AUTO_START_STOP_LEVEL_FAST)
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):
Expand Down

0 comments on commit e9dcb21

Please sign in to comment.