Compare commits

...

5 Commits

Author SHA1 Message Date
Jean-Marc Collin
53160f1365 issue #702 - migrate to HA 2024.12 (#708)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-12-14 11:56:36 +01:00
Jean-Marc Collin
1aaa6d4461 Issue #696 follow underlying doesn't work anymore (#701)
* Issue #696 - follow underlying doesn't work anymore

* Fix testu

* Release

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-12-11 08:21:26 +01:00
Jean-Marc Collin
a09af6a184 Remove erreur messages component.versatile_thermostat.options.step.menu.title failed: ({'name'} != set()) 2024-12-10 16:45:58 +00:00
Jean-Marc Collin
e9dcb21093 Issue #625 - auto-start/stop change sometimes too fast (#693)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-12-08 19:22:24 +01:00
Jean-Marc Collin
980c24c939 Issue #662 - disabling auto-start/stop when VTherm is off due to auto-start/stop should turn on the VTherm (#692)
* Fix

* Fix slope None

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-12-08 18:26:04 +01:00
9 changed files with 293 additions and 20 deletions

View File

@@ -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):
@@ -143,17 +146,26 @@ class AutoStartStopDetectionAlgorithm:
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)
@@ -163,11 +175,13 @@ class AutoStartStopDetectionAlgorithm:
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(
@@ -178,11 +192,15 @@ class AutoStartStopDetectionAlgorithm:
# 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(
@@ -192,11 +210,15 @@ class AutoStartStopDetectionAlgorithm:
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(
@@ -235,5 +257,10 @@ class AutoStartStopDetectionAlgorithm:
"""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}"

View File

@@ -14,6 +14,6 @@
"quality_scale": "silver",
"requirements": [],
"ssdp": [],
"version": "6.8.0",
"version": "6.8.3",
"zeroconf": []
}

View File

@@ -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",
}
)
@@ -182,8 +183,8 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
await super()._async_internal_set_temperature(temperature)
self._regulation_algo.set_target_temp(self.target_temperature)
# is done by control_heating method. No need to do it here
# await self._send_regulated_temperature(force=True)
# Is necessary cause control_heating method will not force the update.
await self._send_regulated_temperature(force=True)
async def _send_regulated_temperature(self, force=False):
"""Sends the regulated temperature to all underlying"""
@@ -555,6 +556,10 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
"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
)
@@ -985,6 +990,32 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
def set_auto_start_stop_enable(self, is_enabled: bool):
"""Enable/Disable the auto-start/stop feature"""
self._is_auto_start_stop_enabled = is_enabled
if (
self.hvac_mode == HVACMode.OFF
and self.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
):
_LOGGER.debug(
"%s - the vtherm is off cause auto-start/stop and enable have been set to false -> starts the VTherm"
)
self.hass.create_task(self.async_turn_on())
# Send an event
self.send_event(
event_type=EventType.AUTO_START_STOP_EVENT,
data={
"type": "start",
"name": self.name,
"cause": "Auto start stop disabled",
"hvac_mode": self.hvac_mode,
"saved_hvac_mode": self._saved_hvac_mode,
"target_temperature": self.target_temperature,
"current_temperature": self.current_temperature,
"temperature_slope": round(self.last_temperature_slope or 0, 3),
"accumulated_error": self._auto_start_stop_algo.accumulated_error,
"accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
},
)
self.update_custom_attributes()
def set_follow_underlying_temp_change(self, follow: bool):
@@ -1088,15 +1119,6 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
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.

View File

@@ -218,7 +218,7 @@
}
},
"valve_regulation": {
"title": "Auto-régulation par vanne - {name}",
"title": "Auto-régulation par vanne",
"description": "Configuration de l'auto-régulation par controle direct de la vanne",
"data": {
"offset_calibration_entity_ids": "Entités de 'calibrage du décalage''",
@@ -257,7 +257,7 @@
}
},
"menu": {
"title": "Menu - {name}",
"title": "Menu",
"description": "Paramétrez votre thermostat. Vous pourrez finaliser la configuration quand tous les paramètres auront été saisis.",
"menu_options": {
"main": "Principaux Attributs",

View File

@@ -3,5 +3,5 @@
"content_in_root": false,
"render_readme": true,
"hide_default_branch": false,
"homeassistant": "2024.10.4"
"homeassistant": "2024.12.3"
}

View File

@@ -1 +1 @@
homeassistant==2024.10.4
homeassistant==2024.12.3

View File

@@ -309,7 +309,7 @@ async def test_over_climate_regulation_limitations(
assert entity.hvac_action == HVACAction.HEATING
# the regulated temperature will not change because when we set temp manually it is forced
assert entity.regulated_target_temp == 17 # 19.5
assert entity.regulated_target_temp == 19.5
# 2. set manual target temp (at now - 18) -> the regulation should be taken into account
event_timestamp = now - timedelta(minutes=18)

View File

@@ -15,6 +15,7 @@ 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
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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):
@@ -1463,3 +1568,121 @@ async def test_auto_start_stop_fast_heat_window_mixed(
),
]
)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_auto_start_stop_disable_vtherm_off(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test that if auto-start-stop is disabled while VTherm is off, the VTherms turns on
This is in the issue #662"""
# The temperatures to set
temps = {
"frost": 7.0,
"eco": 17.0,
"comfort": 19.0,
"boost": 21.0,
}
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: False,
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: False,
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],
)
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
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
vtherm._set_now(now)
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. turn off the VTherm with auto-start/stop
now = now + timedelta(minutes=5)
vtherm._set_now(now)
await send_temperature_change_event(vtherm, 25, now, True)
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
await vtherm.async_set_preset_mode(PRESET_ECO)
await hass.async_block_till_done()
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
now = now + timedelta(minutes=10)
vtherm._set_now(now)
await send_temperature_change_event(vtherm, 26, now, True)
await hass.async_block_till_done()
# VTherm should have been stopped
assert vtherm.hvac_mode == HVACMode.OFF
assert vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
# 3. set enable to false
now = now + timedelta(minutes=5)
vtherm._set_now(now)
enable_entity.turn_off()
await hass.async_block_till_done()
assert enable_entity.state == STATE_OFF
# VTherm should be heating
assert vtherm.hvac_mode == HVACMode.HEAT
# In Eco
assert vtherm.target_temperature == 17.0

View File

@@ -21,7 +21,7 @@ logging.getLogger().setLevel(logging.DEBUG)
# @pytest.mark.parametrize("expected_lingering_tasks", [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
# @pytest.mark.skip
@pytest.mark.skip
async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get):
"""Test the normal full start of a thermostat in thermostat_over_climate type"""
@@ -300,6 +300,7 @@ async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get
await hass.async_block_till_done()
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_climate_valve_multi_presence(
hass: HomeAssistant, skip_hass_states_get
):