Issue #199 - persist and don't reset the accumulation error

This commit is contained in:
Jean-Marc Collin
2023-11-17 18:11:55 +00:00
parent a5c548bbee
commit f1595f93da
5 changed files with 124 additions and 54 deletions

View File

@@ -693,6 +693,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
EVENT_HOMEASSISTANT_START, _async_startup_internal
)
def restore_specific_previous_state(self, old_state):
"""Should be overriden in each specific thermostat
if a specific previous state or attribute should be
restored
"""
async def get_my_previous_state(self):
"""Try to get my previou state"""
# Check If we have an old state
@@ -738,6 +744,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
old_total_energy = old_state.attributes.get(ATTR_TOTAL_ENERGY)
if old_total_energy:
self._total_energy = old_total_energy
self.restore_specific_previous_state(old_state)
else:
# No previous state, try and restore defaults
if self._target_temp is None:

View File

@@ -40,6 +40,10 @@ class PITemperatureRegulator:
"""Reset the accumulated error"""
self.accumulated_error = 0
def set_accumulated_error(self, accumulated_error):
"""Allow to persist and restore the accumulated_error"""
self.accumulated_error = accumulated_error
def set_target_temp(self, target_temp):
"""Set the new target_temp"""
self.target_temp = target_temp
@@ -85,9 +89,10 @@ class PITemperatureRegulator:
total_offset = min(self.offset_max, max(-self.offset_max, total_offset))
# If temperature is near the target_temp, reset the accumulated_error
if abs(error) < self.stabilization_threshold:
_LOGGER.debug("Stabilisation")
self.accumulated_error = 0
# Issue #199 - don't reset the accumulation error
# if abs(error) < self.stabilization_threshold:
# _LOGGER.debug("Stabilisation")
# self.accumulated_error = 0
result = round(self.target_temp + total_offset, 1)

View File

@@ -302,6 +302,18 @@ class ThermostatOverClimate(BaseThermostat):
)
)
@overrides
def restore_specific_previous_state(self, old_state):
"""Restore my specific attributes from previous state"""
old_error = old_state.attributes.get("regulation_accumulated_error")
if old_error:
self._regulation_algo.set_accumulated_error(old_error)
_LOGGER.debug(
"%s - Old regulation accumulated_error have been restored to %f",
self,
old_error,
)
@overrides
def update_custom_attributes(self):
"""Custom attributes"""

View File

@@ -1,7 +1,7 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
""" Test the normal start of a Thermostat """
from unittest.mock import patch #, call
from unittest.mock import patch # , call
from datetime import datetime, timedelta
from homeassistant.core import HomeAssistant
@@ -14,13 +14,18 @@ from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DO
from pytest_homeassistant_custom_component.common import MockConfigEntry
# from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from custom_components.versatile_thermostat.thermostat_climate import ThermostatOverClimate
from custom_components.versatile_thermostat.thermostat_climate import (
ThermostatOverClimate,
)
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_climate_regulation(hass: HomeAssistant, skip_hass_states_is_state, skip_send_event):
async def test_over_climate_regulation(
hass: HomeAssistant, skip_hass_states_is_state, skip_send_event
):
"""Test the regulation of an over climate thermostat"""
entry = MockConfigEntry(
@@ -41,7 +46,8 @@ async def test_over_climate_regulation(hass: HomeAssistant, skip_hass_states_is_
event_timestamp = now - timedelta(minutes=10)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
@@ -57,7 +63,7 @@ async def test_over_climate_regulation(hass: HomeAssistant, skip_hass_states_is_
if entity.entity_id == entity_id:
return entity
entity:ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
entity: ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
assert entity
assert isinstance(entity, ThermostatOverClimate)
@@ -90,36 +96,45 @@ async def test_over_climate_regulation(hass: HomeAssistant, skip_hass_states_is_
# set manual target temp (at now - 7) -> the regulation should occurs
event_timestamp = now - timedelta(minutes=7)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await entity.async_set_temperature(temperature=18)
fake_underlying_climate.set_hvac_action(HVACAction.HEATING) # simulate under heating
fake_underlying_climate.set_hvac_action(
HVACAction.HEATING
) # simulate under heating
assert entity.hvac_action == HVACAction.HEATING
assert entity.preset_mode == PRESET_NONE # Manual mode
assert entity.preset_mode == PRESET_NONE # Manual mode
# the regulated temperature should be greater
assert entity.regulated_target_temp > entity.target_temperature
# In medium we could go up to +3 degre
# normally the calcul gives 18 + 2.2 but we round the result to the nearest 0.5 which is 2.0
assert entity.regulated_target_temp == 18+2.0
assert entity.regulated_target_temp == 18 + 1.5
assert entity.hvac_action == HVACAction.HEATING
# change temperature so that the regulated temperature should slow down
event_timestamp = now - timedelta(minutes=5)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await send_temperature_change_event(entity, 23, event_timestamp)
await send_ext_temperature_change_event(entity, 19, event_timestamp)
# the regulated temperature should be under
assert entity.regulated_target_temp < entity.target_temperature
assert entity.regulated_target_temp == 18-0.5 # normally 0.6 but round_to_nearest gives 0.5
assert (
entity.regulated_target_temp == 18 - 2
) # normally 0.6 but round_to_nearest gives 0.5
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_climate_regulation_ac_mode(hass: HomeAssistant, skip_hass_states_is_state, skip_send_event):
async def test_over_climate_regulation_ac_mode(
hass: HomeAssistant, skip_hass_states_is_state, skip_send_event
):
"""Test the regulation of an over climate thermostat"""
entry = MockConfigEntry(
@@ -140,7 +155,8 @@ async def test_over_climate_regulation_ac_mode(hass: HomeAssistant, skip_hass_st
event_timestamp = now - timedelta(minutes=10)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
@@ -156,7 +172,7 @@ async def test_over_climate_regulation_ac_mode(hass: HomeAssistant, skip_hass_st
if entity.entity_id == entity_id:
return entity
entity:ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
entity: ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
assert entity
assert isinstance(entity, ThermostatOverClimate)
@@ -185,53 +201,66 @@ async def test_over_climate_regulation_ac_mode(hass: HomeAssistant, skip_hass_st
await send_temperature_change_event(entity, 30, event_timestamp)
await send_ext_temperature_change_event(entity, 35, event_timestamp)
# set manual target temp
event_timestamp = now - timedelta(minutes=7)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await entity.async_set_temperature(temperature=25)
fake_underlying_climate.set_hvac_action(HVACAction.COOLING) # simulate under heating
fake_underlying_climate.set_hvac_action(
HVACAction.COOLING
) # simulate under heating
assert entity.hvac_action == HVACAction.COOLING
assert entity.preset_mode == PRESET_NONE # Manual mode
assert entity.preset_mode == PRESET_NONE # Manual mode
# the regulated temperature should be lower
assert entity.regulated_target_temp < entity.target_temperature
assert entity.regulated_target_temp == 25-3 # In medium we could go up to -3 degre
assert (
entity.regulated_target_temp == 25 - 2.5
) # In medium we could go up to -3 degre
assert entity.hvac_action == HVACAction.COOLING
# change temperature so that the regulated temperature should slow down
event_timestamp = now - timedelta(minutes=5)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await send_temperature_change_event(entity, 26, event_timestamp)
await send_ext_temperature_change_event(entity, 35, event_timestamp)
# the regulated temperature should be under
assert entity.regulated_target_temp < entity.target_temperature
assert entity.regulated_target_temp == 25-2 # +2.3 without round_to_nearest
assert (
entity.regulated_target_temp == 25 - 1
) # +2.3 without round_to_nearest
# change temperature so that the regulated temperature should slow down
event_timestamp = now - timedelta(minutes=3)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await send_temperature_change_event(entity, 18, event_timestamp)
await send_ext_temperature_change_event(entity, 25, event_timestamp)
# the regulated temperature should be greater
assert entity.regulated_target_temp > entity.target_temperature
assert entity.regulated_target_temp == 25+0.5 # +0.4 without round_to_nearest
assert (
entity.regulated_target_temp == 25 + 3
) # +0.4 without round_to_nearest
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_climate_regulation_limitations(hass: HomeAssistant, skip_hass_states_is_state, skip_send_event):
async def test_over_climate_regulation_limitations(
hass: HomeAssistant, skip_hass_states_is_state, skip_send_event
):
"""Test the limitations of the regulation of an over climate thermostat:
1. test the period_min parameter: do not send regulation event too frequently
2. test the dtemp parameter: do not send regulation event if offset temp is lower than dtemp
1. test the period_min parameter: do not send regulation event too frequently
2. test the dtemp parameter: do not send regulation event if offset temp is lower than dtemp
"""
entry = MockConfigEntry(
@@ -252,7 +281,8 @@ async def test_over_climate_regulation_limitations(hass: HomeAssistant, skip_has
event_timestamp = now - timedelta(minutes=20)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
@@ -268,7 +298,7 @@ async def test_over_climate_regulation_limitations(hass: HomeAssistant, skip_has
if entity.entity_id == entity_id:
return entity
entity:ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
entity: ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
assert entity
assert isinstance(entity, ThermostatOverClimate)
@@ -289,30 +319,37 @@ async def test_over_climate_regulation_limitations(hass: HomeAssistant, skip_has
# set manual target temp (at now - 19) -> the regulation should be ignored because too early
event_timestamp = now - timedelta(minutes=19)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await entity.async_set_temperature(temperature=18)
fake_underlying_climate.set_hvac_action(HVACAction.HEATING) # simulate under heating
fake_underlying_climate.set_hvac_action(
HVACAction.HEATING
) # simulate under heating
assert entity.hvac_action == HVACAction.HEATING
# the regulated temperature will change because when we set temp manually it is forced
assert entity.regulated_target_temp == 20.
assert entity.regulated_target_temp == 19.5
# set manual target temp (at now - 18) -> the regulation should be taken into account
event_timestamp = now - timedelta(minutes=18)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await entity.async_set_temperature(temperature=17)
assert entity.regulated_target_temp > entity.target_temperature
assert entity.regulated_target_temp == 18+1 # In strong we could go up to +3 degre. 0.7 without round_to_nearest
assert (
entity.regulated_target_temp == 18 + 0
) # In strong we could go up to +3 degre. 0.7 without round_to_nearest
old_regulated_temp = entity.regulated_target_temp
# change temperature so that dtemp < 0.5 and time is > period_min (+ 3min)
event_timestamp = now - timedelta(minutes=15)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await send_temperature_change_event(entity, 16, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp)
@@ -323,12 +360,15 @@ async def test_over_climate_regulation_limitations(hass: HomeAssistant, skip_has
# change temperature so that dtemp > 0.5 and time is > period_min (+ 3min)
event_timestamp = now - timedelta(minutes=12)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await send_temperature_change_event(entity, 18, event_timestamp)
await send_temperature_change_event(entity, 17, event_timestamp)
await send_ext_temperature_change_event(entity, 12, event_timestamp)
# the regulated should have been done
assert entity.regulated_target_temp != old_regulated_temp
assert entity.regulated_target_temp > entity.target_temperature
assert entity.regulated_target_temp == 17 + 1.5 # 0.7 without round_to_nearest
assert (
entity.regulated_target_temp == 17 + 0.5
) # 0.7 without round_to_nearest

View File

@@ -1,3 +1,4 @@
# pylint: disable=unused-argument, line-too-long
""" Test the Versatile Thermostat config flow """
from homeassistant import data_entry_flow
@@ -29,7 +30,9 @@ async def test_show_form(hass: HomeAssistant) -> None:
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_states_get): # pylint: disable=unused-argument
async def test_user_config_flow_over_switch(
hass: HomeAssistant, skip_hass_states_get
): # pylint: disable=unused-argument
"""Test the config flow with all thermostat_over_switch features"""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
@@ -128,7 +131,9 @@ async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_state
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_user_config_flow_over_climate(hass: HomeAssistant, skip_hass_states_get): # pylint: disable=unused-argument
async def test_user_config_flow_over_climate(
hass: HomeAssistant, skip_hass_states_get
): # pylint: disable=unused-argument
"""Test the config flow with all thermostat_over_climate features and no additional features"""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
@@ -184,7 +189,9 @@ async def test_user_config_flow_over_climate(hass: HomeAssistant, skip_hass_stat
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_user_config_flow_window_auto_ok(
hass: HomeAssistant, skip_hass_states_get, skip_control_heating # pylint: disable=unused-argument
hass: HomeAssistant,
skip_hass_states_get,
skip_control_heating, # pylint: disable=unused-argument
):
"""Test the config flow with only window auto feature"""
result = await hass.config_entries.flow.async_init(
@@ -353,7 +360,9 @@ async def test_user_config_flow_window_auto_ko(
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_user_config_flow_over_4_switches(
hass: HomeAssistant, skip_hass_states_get, skip_control_heating # pylint: disable=unused-argument
hass: HomeAssistant,
skip_hass_states_get,
skip_control_heating, # pylint: disable=unused-argument
):
"""Test the config flow with 4 switchs thermostat_over_switch features"""
@@ -369,7 +378,7 @@ async def test_user_config_flow_over_4_switches(
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False
CONF_USE_PRESENCE_FEATURE: False,
}
TYPE_CONFIG = { # pylint: disable=wildcard-import, invalid-name
@@ -427,15 +436,11 @@ async def test_user_config_flow_over_4_switches(
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert (
result["data"]
== SOURCE_CONFIG
| TYPE_CONFIG
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
| MOCK_PRESETS_CONFIG
| MOCK_ADVANCED_CONFIG
| { CONF_INVERSE_SWITCH: False }
)
assert result[
"data"
] == SOURCE_CONFIG | TYPE_CONFIG | MOCK_TH_OVER_SWITCH_TPI_CONFIG | MOCK_PRESETS_CONFIG | MOCK_ADVANCED_CONFIG | {
CONF_INVERSE_SWITCH: False
}
assert result["result"]
assert result["result"].domain == DOMAIN
assert result["result"].version == 1