Compare commits

..

2 Commits

Author SHA1 Message Date
Jean-Marc Collin dc7739d53b Fix slope None 2024-12-08 17:18:02 +00:00
Jean-Marc Collin 729f263cc8 Fix 2024-12-08 17:05:39 +00:00
6 changed files with 16 additions and 148 deletions
@@ -57,13 +57,10 @@ 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):
@@ -146,26 +143,17 @@ 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)
@@ -175,13 +163,11 @@ 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(
@@ -192,15 +178,11 @@ 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 ( if temp_at_dt <= target_temp - TEMP_HYSTERESIS:
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(
@@ -210,15 +192,11 @@ 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 ( if temp_at_dt >= target_temp + TEMP_HYSTERESIS:
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(
@@ -257,10 +235,5 @@ 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}"
@@ -1329,8 +1329,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._attr_preset_mode = PRESET_ACTIVITY self._attr_preset_mode = PRESET_ACTIVITY
await self._async_update_motion_temp() await self._async_update_motion_temp()
else: else:
# if self._attr_preset_mode == PRESET_NONE: if self._attr_preset_mode == PRESET_NONE:
# self._saved_target_temp = self._target_temp self._saved_target_temp = self._target_temp
self._attr_preset_mode = preset_mode self._attr_preset_mode = preset_mode
await self._async_internal_set_temperature( await self._async_internal_set_temperature(
self.find_preset_temp(preset_mode) self.find_preset_temp(preset_mode)
@@ -60,7 +60,6 @@ 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",
} }
) )
@@ -556,10 +555,6 @@ 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
) )
@@ -1119,6 +1114,15 @@ 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.
-105
View File
@@ -15,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
@@ -45,7 +44,6 @@ 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)
@@ -59,7 +57,6 @@ 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)
@@ -73,7 +70,6 @@ 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)
@@ -87,7 +83,6 @@ 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)
@@ -101,9 +96,6 @@ 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)
@@ -117,111 +109,14 @@ 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):
+1 -1
View File
@@ -19,7 +19,7 @@ logging.getLogger().setLevel(logging.DEBUG)
# @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])
# this test fails if run in // with the next because the underlying_valve_regulation is mixed. Don't know why # this test fails if run in // with the next because the underlying_valve_regulation is mixed. Don't know why
# @pytest.mark.skip # @pytest.mark.skip
async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get): async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get):
+2 -6
View File
@@ -1802,7 +1802,7 @@ async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is
event_timestamp = event_timestamp + timedelta(minutes=1) event_timestamp = event_timestamp + timedelta(minutes=1)
await send_temperature_change_event(entity, 19, event_timestamp) await send_temperature_change_event(entity, 19, event_timestamp)
# 2. Make the temperature down -> no change # 2. Make the temperature down
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, patch( ) as mock_send_event, patch(
@@ -1824,7 +1824,7 @@ async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is
assert entity.window_state is STATE_OFF assert entity.window_state is STATE_OFF
assert entity.window_auto_state is STATE_OFF assert entity.window_auto_state is STATE_OFF
# 3. send one degre down in one minute -> window is on # 3. send one degre down in one minute
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, patch( ) as mock_send_event, patch(
@@ -1852,8 +1852,6 @@ async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is
assert entity.preset_mode is PRESET_BOOST assert entity.preset_mode is PRESET_BOOST
# The eco temp # The eco temp
assert entity.target_temperature == 10 assert entity.target_temperature == 10
# The last temp is saved
assert entity._saved_target_temp == 21
mock_send_event.assert_has_calls( mock_send_event.assert_has_calls(
[ [
@@ -1891,7 +1889,6 @@ async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is
assert entity.preset_mode is PRESET_BOOST assert entity.preset_mode is PRESET_BOOST
# The eco temp # The eco temp
assert entity.target_temperature == 10 assert entity.target_temperature == 10
assert entity._saved_target_temp == 21
# 5. send another plus 1.1 degre in one minute -> restore state # 5. send another plus 1.1 degre in one minute -> restore state
with patch( with patch(
@@ -1932,7 +1929,6 @@ async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is
assert entity.preset_mode is PRESET_BOOST assert entity.preset_mode is PRESET_BOOST
# The eco temp # The eco temp
assert entity.target_temperature == 21 assert entity.target_temperature == 21
assert entity._saved_target_temp == 21
# Clean the entity # Clean the entity
entity.remove_thermostat() entity.remove_thermostat()