From f1595f93daa610f0cb89377d4aff205c96ad56f2 Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Fri, 17 Nov 2023 18:11:55 +0000 Subject: [PATCH] Issue #199 - persist and don't reset the accumulation error --- .../versatile_thermostat/base_thermostat.py | 8 ++ .../versatile_thermostat/pi_algorithm.py | 11 +- .../thermostat_climate.py | 12 ++ tests/test_auto_regulation.py | 114 ++++++++++++------ tests/test_config_flow.py | 33 ++--- 5 files changed, 124 insertions(+), 54 deletions(-) diff --git a/custom_components/versatile_thermostat/base_thermostat.py b/custom_components/versatile_thermostat/base_thermostat.py index 795b1aa..0e23e96 100644 --- a/custom_components/versatile_thermostat/base_thermostat.py +++ b/custom_components/versatile_thermostat/base_thermostat.py @@ -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: diff --git a/custom_components/versatile_thermostat/pi_algorithm.py b/custom_components/versatile_thermostat/pi_algorithm.py index 9404f2b..859e7af 100644 --- a/custom_components/versatile_thermostat/pi_algorithm.py +++ b/custom_components/versatile_thermostat/pi_algorithm.py @@ -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) diff --git a/custom_components/versatile_thermostat/thermostat_climate.py b/custom_components/versatile_thermostat/thermostat_climate.py index da08946..5a008fe 100644 --- a/custom_components/versatile_thermostat/thermostat_climate.py +++ b/custom_components/versatile_thermostat/thermostat_climate.py @@ -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""" diff --git a/tests/test_auto_regulation.py b/tests/test_auto_regulation.py index f572353..aa21e0a 100644 --- a/tests/test_auto_regulation.py +++ b/tests/test_auto_regulation.py @@ -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 \ No newline at end of file + assert ( + entity.regulated_target_temp == 17 + 0.5 + ) # 0.7 without round_to_nearest diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index f0b7b6b..c2cba6e 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -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