From 9d099e3169bee22c23e119c9cf2a98a35702d411 Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Tue, 31 Oct 2023 09:07:38 +0000 Subject: [PATCH] Add regulation limitations --- .../versatile_thermostat/base_thermostat.py | 5 +- .../versatile_thermostat/climate.py | 13 +- .../versatile_thermostat/commons.py | 20 ++- .../versatile_thermostat/config_flow.py | 5 + .../versatile_thermostat/const.py | 8 +- .../versatile_thermostat/services.yaml | 23 +++- .../versatile_thermostat/strings.json | 16 ++- .../thermostat_climate.py | 88 ++++++++++--- .../versatile_thermostat/translations/en.json | 16 ++- .../versatile_thermostat/translations/fr.json | 16 ++- tests/commons.py | 9 +- tests/const.py | 10 +- tests/test_auto_regulation.py | 119 ++++++++++-------- 13 files changed, 245 insertions(+), 103 deletions(-) diff --git a/custom_components/versatile_thermostat/base_thermostat.py b/custom_components/versatile_thermostat/base_thermostat.py index 61faa85..d0b0e9e 100644 --- a/custom_components/versatile_thermostat/base_thermostat.py +++ b/custom_components/versatile_thermostat/base_thermostat.py @@ -1219,7 +1219,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity): await self.async_control_heating(force=True) async def _async_internal_set_temperature(self, temperature): - """Set the target temperature and the target temperature of underlying climate if any""" + """Set the target temperature and the target temperature of underlying climate if any + For testing purpose you can pass an event_timestamp. + """ self._target_temp = temperature return @@ -2247,7 +2249,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity): await self.async_control_heating() self.update_custom_attributes() - #PR - Adding Window ByPass async def service_set_window_bypass_state(self, window_bypass): """Called by a service call: service: versatile_thermostat.set_window_bypass diff --git a/custom_components/versatile_thermostat/climate.py b/custom_components/versatile_thermostat/climate.py index d614d08..2baadc5 100644 --- a/custom_components/versatile_thermostat/climate.py +++ b/custom_components/versatile_thermostat/climate.py @@ -24,12 +24,12 @@ from .const import ( SERVICE_SET_PRESENCE, SERVICE_SET_PRESET_TEMPERATURE, SERVICE_SET_SECURITY, - #PR - Adding Window ByPass SERVICE_SET_WINDOW_BYPASS, + SERVICE_SET_AUTO_REGULATION_MODE, CONF_THERMOSTAT_TYPE, CONF_THERMOSTAT_SWITCH, CONF_THERMOSTAT_CLIMATE, - CONF_THERMOSTAT_VALVE, + CONF_THERMOSTAT_VALVE ) from .thermostat_switch import ThermostatOverSwitch @@ -99,7 +99,6 @@ async def async_setup_entry( "service_set_security", ) - #PR - Adding Window ByPass platform.async_register_entity_service( SERVICE_SET_WINDOW_BYPASS, { @@ -108,3 +107,11 @@ async def async_setup_entry( }, "service_set_window_bypass_state", ) + + platform.async_register_entity_service( + SERVICE_SET_AUTO_REGULATION_MODE, + { + vol.Required("auto_regulation_mode"): vol.In(["None", "Light", "Medium", "Strong"]), + }, + "service_set_auto_regulation_mode", + ) diff --git a/custom_components/versatile_thermostat/commons.py b/custom_components/versatile_thermostat/commons.py index 5032e55..d799080 100644 --- a/custom_components/versatile_thermostat/commons.py +++ b/custom_components/versatile_thermostat/commons.py @@ -1,18 +1,34 @@ """ Some usefull commons class """ import logging -from datetime import timedelta +from datetime import timedelta, datetime from homeassistant.core import HomeAssistant, callback, Event from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import Entity from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType from homeassistant.helpers.event import async_track_state_change_event, async_call_later +from homeassistant.util import dt as dt_util from .base_thermostat import BaseThermostat from .const import DOMAIN, DEVICE_MANUFACTURER _LOGGER = logging.getLogger(__name__) +def get_tz(hass: HomeAssistant): + """Get the current timezone""" + + return dt_util.get_time_zone(hass.config.time_zone) + +class NowClass: + """ For testing purpose only""" + + @staticmethod + def get_now(hass: HomeAssistant) -> datetime: + """ A test function to get the now. + For testing purpose this method can be overriden to get a specific + timestamp + """ + return datetime.now( get_tz(hass)) class VersatileThermostatBaseEntity(Entity): """A base class for all entities""" @@ -98,7 +114,7 @@ class VersatileThermostatBaseEntity(Entity): await try_find_climate(None) @callback - async def async_my_climate_changed(self, event: Event): + async def async_my_climate_changed(self, event: Event): # pylint: disable=unused-argument """Called when my climate have change This method aims to be overriden to take the status change """ diff --git a/custom_components/versatile_thermostat/config_flow.py b/custom_components/versatile_thermostat/config_flow.py index 306f6e2..a694da1 100644 --- a/custom_components/versatile_thermostat/config_flow.py +++ b/custom_components/versatile_thermostat/config_flow.py @@ -102,6 +102,8 @@ from .const import ( CONF_AUTO_REGULATION_MODES, CONF_AUTO_REGULATION_MODE, CONF_AUTO_REGULATION_NONE, + CONF_AUTO_REGULATION_DTEMP, + CONF_AUTO_REGULATION_PERIOD_MIN, UnknownEntity, WindowOpenDetectionMethod, ) @@ -264,6 +266,9 @@ class VersatileThermostatBaseConfigFlow(FlowHandler): options=CONF_AUTO_REGULATION_MODES, translation_key="auto_regulation_mode" ) ), + vol.Optional(CONF_AUTO_REGULATION_DTEMP, default=0.5): vol.Coerce(float), + vol.Optional(CONF_AUTO_REGULATION_PERIOD_MIN, default=5): cv.positive_int + } ) diff --git a/custom_components/versatile_thermostat/const.py b/custom_components/versatile_thermostat/const.py index 4b6ba51..8ffb3cd 100644 --- a/custom_components/versatile_thermostat/const.py +++ b/custom_components/versatile_thermostat/const.py @@ -88,6 +88,8 @@ CONF_AUTO_REGULATION_NONE= "auto_regulation_none" CONF_AUTO_REGULATION_LIGHT= "auto_regulation_light" CONF_AUTO_REGULATION_MEDIUM= "auto_regulation_medium" CONF_AUTO_REGULATION_STRONG= "auto_regulation_strong" +CONF_AUTO_REGULATION_DTEMP="auto_regulation_dtemp" +CONF_AUTO_REGULATION_PERIOD_MIN="auto_regulation_periode_min" CONF_PRESETS = { p: f"{p}_temp" @@ -189,7 +191,9 @@ ALL_CONF = ( CONF_VALVE_2, CONF_VALVE_3, CONF_VALVE_4, - CONF_AUTO_REGULATION_MODE + CONF_AUTO_REGULATION_MODE, + CONF_AUTO_REGULATION_DTEMP, + CONF_AUTO_REGULATION_PERIOD_MIN ] + CONF_PRESETS_VALUES + CONF_PRESETS_AWAY_VALUES @@ -210,8 +214,8 @@ SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE SERVICE_SET_PRESENCE = "set_presence" SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature" SERVICE_SET_SECURITY = "set_security" -#PR - Adding Window ByPass SERVICE_SET_WINDOW_BYPASS = "set_window_bypass" +SERVICE_SET_AUTO_REGULATION_MODE = "set_auto_regulation_mode" DEFAULT_SECURITY_MIN_ON_PERCENT = 0.5 DEFAULT_SECURITY_DEFAULT_ON_PERCENT = 0.1 diff --git a/custom_components/versatile_thermostat/services.yaml b/custom_components/versatile_thermostat/services.yaml index e57381a..6899cb1 100644 --- a/custom_components/versatile_thermostat/services.yaml +++ b/custom_components/versatile_thermostat/services.yaml @@ -137,4 +137,25 @@ set_window_bypass: advanced: false default: true selector: - boolean: \ No newline at end of file + boolean: + +set_auto_regulation_mode: + name: Set Auto Regulation mode + description: Change the mode of self-regulation (only for VTherm over climate) + target: + entity: + integration: versatile_thermostat + fields: + auto_regulation_mode: + name: Auto regulation mode + description: Possible values + required: true + advanced: false + default: true + selector: + select: + options: + - "None" + - "Light" + - "Medium" + - "Strong" diff --git a/custom_components/versatile_thermostat/strings.json b/custom_components/versatile_thermostat/strings.json index 353373c..506b1fa 100644 --- a/custom_components/versatile_thermostat/strings.json +++ b/custom_components/versatile_thermostat/strings.json @@ -39,7 +39,9 @@ "valve_entity2_id": "2nd valve number", "valve_entity3_id": "3rd valve number", "valve_entity4_id": "4th valve number", - "auto_regulation_mode": "Self-regulation" + "auto_regulation_mode": "Self-regulation", + "auto_regulation_dtemp": "Regulation threshold", + "auto_regulation_periode_min": "Regulation minimal period" }, "data_description": { "heater_entity_id": "Mandatory heater entity id", @@ -56,7 +58,9 @@ "valve_entity2_id": "2nd valve number entity id", "valve_entity3_id": "3rd valve number entity id", "valve_entity4_id": "4th valve number entity id", - "auto_regulation_mode": "Auto adjustment of the target temperature" + "auto_regulation_mode": "Auto adjustment of the target temperature", + "auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send", + "auto_regulation_periode_min": "Duration in minutes between two regulation update" } }, "tpi": { @@ -202,7 +206,9 @@ "valve_entity2_id": "2nd valve number", "valve_entity3_id": "3rd valve number", "valve_entity4_id": "4th valve number", - "auto_regulation_mode": "Self-regulation" + "auto_regulation_mode": "Self-regulation", + "auto_regulation_dtemp": "Regulation threshold", + "auto_regulation_periode_min": "Regulation minimal period" }, "data_description": { "heater_entity_id": "Mandatory heater entity id", @@ -219,7 +225,9 @@ "valve_entity2_id": "2nd valve number entity id", "valve_entity3_id": "3rd valve number entity id", "valve_entity4_id": "4th valve number entity id", - "auto_regulation_mode": "Auto adjustment of the target temperature" + "auto_regulation_mode": "Auto adjustment of the target temperature", + "auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send", + "auto_regulation_periode_min": "Duration in minutes between two regulation update" } }, "tpi": { diff --git a/custom_components/versatile_thermostat/thermostat_climate.py b/custom_components/versatile_thermostat/thermostat_climate.py index 5237f3a..347e55b 100644 --- a/custom_components/versatile_thermostat/thermostat_climate.py +++ b/custom_components/versatile_thermostat/thermostat_climate.py @@ -1,13 +1,14 @@ # pylint: disable=line-too-long """ A climate over switch classe """ import logging -from datetime import timedelta +from datetime import timedelta, datetime from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.event import async_track_state_change_event, async_track_time_interval from homeassistant.components.climate import HVACAction, HVACMode +from .commons import NowClass from .base_thermostat import BaseThermostat from .pi_algorithm import PITemperatureRegulator @@ -22,6 +23,8 @@ from .const import ( CONF_AUTO_REGULATION_LIGHT, CONF_AUTO_REGULATION_MEDIUM, CONF_AUTO_REGULATION_STRONG, + CONF_AUTO_REGULATION_DTEMP, + CONF_AUTO_REGULATION_PERIOD_MIN, RegulationParamLight, RegulationParamMedium, RegulationParamStrong @@ -33,9 +36,12 @@ _LOGGER = logging.getLogger(__name__) class ThermostatOverClimate(BaseThermostat): """Representation of a base class for a Versatile Thermostat over a climate""" - _regulation_mode:str = None + _auto_regulation_mode:str = None _regulation_algo = None _regulated_target_temp: float = None + _auto_regulation_dtemp: float = None + _auto_regulation_period_min: int = None + _last_regulation_change: datetime = None _entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union(frozenset( { @@ -48,6 +54,7 @@ class ThermostatOverClimate(BaseThermostat): # super.__init__ calls post_init at the end. So it must be called after regulation initialization super().__init__(hass, unique_id, name, entry_infos) self._regulated_target_temp = self.target_temperature + self._last_regulation_change = NowClass.get_now(hass) @property def is_over_climate(self) -> bool: @@ -84,17 +91,31 @@ class ThermostatOverClimate(BaseThermostat): async def _send_regulated_temperature(self): """ Sends the regulated temperature to all underlying """ - new_regulated_temp = self._regulation_algo.calculate_regulated_temperature(self.current_temperature, self._cur_ext_temp) - if new_regulated_temp != self._regulated_target_temp: - _LOGGER.info("%s - Regulated temp have changed to %.1f. Resend it to underlyings", self, new_regulated_temp) - self._regulated_target_temp = new_regulated_temp + if not self._regulated_target_temp: + self._regulated_target_temp = self.target_temperature - for under in self._underlyings: - await under.set_temperature( - self.regulated_target_temp, self._attr_max_temp, self._attr_min_temp - ) - else: - _LOGGER.debug("%s - No change on regulated temperature (%.1f)", self, self._regulated_target_temp) + new_regulated_temp = self._regulation_algo.calculate_regulated_temperature(self.current_temperature, self._cur_ext_temp) + dtemp = new_regulated_temp - self._regulated_target_temp + + if abs(dtemp) < self._auto_regulation_dtemp: + _LOGGER.info("!!!!! %s - dtemp (%.1f) is < %.1f -> forget the regulation send", self, dtemp, self._auto_regulation_dtemp) + return + + now:datetime = NowClass.get_now(self._hass) + period = float((now - self._last_regulation_change).total_seconds()) / 60. + if period < self._auto_regulation_period_min: + _LOGGER.debug("%s - period (%.1f) is < %.0f -> forget the regulation send", self, period, self._auto_regulation_period_min) + return + + + self._regulated_target_temp = new_regulated_temp + _LOGGER.info("!!!!! %s - Regulated temp have changed to %.1f. Resend it to underlyings", self, new_regulated_temp) + self._last_regulation_change = now + + for under in self._underlyings: + await under.set_temperature( + self.regulated_target_temp, self._attr_max_temp, self._attr_min_temp + ) @overrides def post_init(self, entry_infos): @@ -116,9 +137,17 @@ class ThermostatOverClimate(BaseThermostat): ) ) - self._regulation_mode = entry_infos.get(CONF_AUTO_REGULATION_MODE) if entry_infos.get(CONF_AUTO_REGULATION_MODE) is not None else CONF_AUTO_REGULATION_NONE + self.choose_auto_regulation_mode( + entry_infos.get(CONF_AUTO_REGULATION_MODE) if entry_infos.get(CONF_AUTO_REGULATION_MODE) is not None else CONF_AUTO_REGULATION_NONE + ) - if self._regulation_mode == CONF_AUTO_REGULATION_LIGHT: + self._auto_regulation_dtemp = entry_infos.get(CONF_AUTO_REGULATION_DTEMP) if entry_infos.get(CONF_AUTO_REGULATION_DTEMP) is not None else 0.5 + self._auto_regulation_period_min = entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN) if entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN) is not None else 5 + + def choose_auto_regulation_mode(self, auto_regulation_mode): + """ Choose or change the regulation mode""" + self._auto_regulation_mode = auto_regulation_mode + if self._auto_regulation_mode == CONF_AUTO_REGULATION_LIGHT: self._regulation_algo = PITemperatureRegulator( self.target_temperature, RegulationParamLight.kp, @@ -127,7 +156,7 @@ class ThermostatOverClimate(BaseThermostat): RegulationParamLight.offset_max, RegulationParamLight.stabilization_threshold, RegulationParamLight.accumulated_error_threshold) - elif self._regulation_mode == CONF_AUTO_REGULATION_MEDIUM: + elif self._auto_regulation_mode == CONF_AUTO_REGULATION_MEDIUM: self._regulation_algo = PITemperatureRegulator( self.target_temperature, RegulationParamMedium.kp, @@ -136,7 +165,7 @@ class ThermostatOverClimate(BaseThermostat): RegulationParamMedium.offset_max, RegulationParamMedium.stabilization_threshold, RegulationParamMedium.accumulated_error_threshold) - elif self._regulation_mode == CONF_AUTO_REGULATION_STRONG: + elif self._auto_regulation_mode == CONF_AUTO_REGULATION_STRONG: self._regulation_algo = PITemperatureRegulator( self.target_temperature, RegulationParamStrong.kp, @@ -430,9 +459,9 @@ class ThermostatOverClimate(BaseThermostat): return ret @property - def regulation_mode(self): + def auto_regulation_mode(self): """ Get the regulation mode """ - return self._regulation_mode + return self._auto_regulation_mode @property def regulated_target_temp(self): @@ -442,7 +471,7 @@ class ThermostatOverClimate(BaseThermostat): @property def is_regulated(self): """ Check if the ThermostatOverClimate is regulated """ - return self.regulation_mode != CONF_AUTO_REGULATION_NONE + return self.auto_regulation_mode != CONF_AUTO_REGULATION_NONE @property def hvac_modes(self): @@ -617,3 +646,24 @@ class ThermostatOverClimate(BaseThermostat): await under.set_swing_mode(swing_mode) self._swing_mode = swing_mode self.async_write_ha_state() + + async def service_set_auto_regulation_mode(self, auto_regulation_mode): + """Called by a service call: + service: versatile_thermostat.set_auto_regulation_mode + data: + auto_regulation_mode: [None | Light | Medium | Strong] + target: + entity_id: climate.thermostat_1 + """ + _LOGGER.info("%s - Calling service_set_auto_regulation_mode, auto_regulation_mode: %s", self, auto_regulation_mode) + if auto_regulation_mode == "None": + self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_NONE) + elif auto_regulation_mode == "Light": + self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_LIGHT) + elif auto_regulation_mode == "Medium": + self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_MEDIUM) + elif auto_regulation_mode == "Strong": + self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_STRONG) + + await self._send_regulated_temperature() + self.update_custom_attributes() diff --git a/custom_components/versatile_thermostat/translations/en.json b/custom_components/versatile_thermostat/translations/en.json index 353373c..506b1fa 100644 --- a/custom_components/versatile_thermostat/translations/en.json +++ b/custom_components/versatile_thermostat/translations/en.json @@ -39,7 +39,9 @@ "valve_entity2_id": "2nd valve number", "valve_entity3_id": "3rd valve number", "valve_entity4_id": "4th valve number", - "auto_regulation_mode": "Self-regulation" + "auto_regulation_mode": "Self-regulation", + "auto_regulation_dtemp": "Regulation threshold", + "auto_regulation_periode_min": "Regulation minimal period" }, "data_description": { "heater_entity_id": "Mandatory heater entity id", @@ -56,7 +58,9 @@ "valve_entity2_id": "2nd valve number entity id", "valve_entity3_id": "3rd valve number entity id", "valve_entity4_id": "4th valve number entity id", - "auto_regulation_mode": "Auto adjustment of the target temperature" + "auto_regulation_mode": "Auto adjustment of the target temperature", + "auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send", + "auto_regulation_periode_min": "Duration in minutes between two regulation update" } }, "tpi": { @@ -202,7 +206,9 @@ "valve_entity2_id": "2nd valve number", "valve_entity3_id": "3rd valve number", "valve_entity4_id": "4th valve number", - "auto_regulation_mode": "Self-regulation" + "auto_regulation_mode": "Self-regulation", + "auto_regulation_dtemp": "Regulation threshold", + "auto_regulation_periode_min": "Regulation minimal period" }, "data_description": { "heater_entity_id": "Mandatory heater entity id", @@ -219,7 +225,9 @@ "valve_entity2_id": "2nd valve number entity id", "valve_entity3_id": "3rd valve number entity id", "valve_entity4_id": "4th valve number entity id", - "auto_regulation_mode": "Auto adjustment of the target temperature" + "auto_regulation_mode": "Auto adjustment of the target temperature", + "auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send", + "auto_regulation_periode_min": "Duration in minutes between two regulation update" } }, "tpi": { diff --git a/custom_components/versatile_thermostat/translations/fr.json b/custom_components/versatile_thermostat/translations/fr.json index e1eb5cd..1bc1896 100644 --- a/custom_components/versatile_thermostat/translations/fr.json +++ b/custom_components/versatile_thermostat/translations/fr.json @@ -39,7 +39,9 @@ "valve_entity2_id": "2ème valve number", "valve_entity3_id": "3ème valve number", "valve_entity4_id": "4ème valve number", - "auto_regulation_mode": "Auto-régulation" + "auto_regulation_mode": "Auto-régulation", + "auto_regulation_dtemp": "Seuil de régulation", + "auto_regulation_periode_min": "Période minimale de régulation" }, "data_description": { "heater_entity_id": "Entity id du 1er radiateur obligatoire", @@ -56,7 +58,9 @@ "valve_entity2_id": "Entity id de la 2ème valve", "valve_entity3_id": "Entity id de la 3ème valve", "valve_entity4_id": "Entity id de la 4ème valve", - "auto_regulation_mode": "Ajustement automatique de la température cible" + "auto_regulation_mode": "Ajustement automatique de la température cible", + "auto_regulation_dtemp": "Le seuil en ° au-dessous duquel la régulation ne sera pas envoyée", + "auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation" } }, "tpi": { @@ -203,7 +207,9 @@ "valve_entity2_id": "2ème valve", "valve_entity3_id": "3ème valve", "valve_entity4_id": "4ème valve", - "auto_regulation_mode": "Auto-regulation" + "auto_regulation_mode": "Auto-regulation", + "auto_regulation_dtemp": "Seuil de régulation", + "auto_regulation_periode_min": "Période minimale de régulation" }, "data_description": { "heater_entity_id": "Entity id du 1er radiateur obligatoire", @@ -220,7 +226,9 @@ "valve_entity2_id": "Entity id de la 2ème valve", "valve_entity3_id": "Entity id de la 3ème valve", "valve_entity4_id": "Entity id de la 4ème valve", - "auto_regulation_mode": "Ajustement automatique de la consigne" + "auto_regulation_mode": "Ajustement automatique de la consigne", + "auto_regulation_dtemp": "Le seuil en ° au-dessous duquel la régulation ne sera pas envoyée", + "auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation" } }, "tpi": { diff --git a/tests/commons.py b/tests/commons.py index 9aedd3a..32a3320 100644 --- a/tests/commons.py +++ b/tests/commons.py @@ -10,7 +10,6 @@ from homeassistant.core import HomeAssistant, Event, EVENT_STATE_CHANGED, State from homeassistant.const import UnitOfTemperature, STATE_ON, STATE_OFF, ATTR_TEMPERATURE from homeassistant.config_entries import ConfigEntryState -from homeassistant.util import dt as dt_util from homeassistant.helpers.entity import Entity from homeassistant.components.climate import ( ClimateEntity, @@ -25,6 +24,7 @@ from pytest_homeassistant_custom_component.common import MockConfigEntry from custom_components.versatile_thermostat.base_thermostat import BaseThermostat from custom_components.versatile_thermostat.const import * # pylint: disable=wildcard-import, unused-wildcard-import from custom_components.versatile_thermostat.underlyings import * # pylint: disable=wildcard-import, unused-wildcard-import +from custom_components.versatile_thermostat.commons import get_tz, NowClass # pylint: disable=unused-import from .const import ( # pylint: disable=unused-import MOCK_TH_OVER_SWITCH_USER_CONFIG, @@ -478,13 +478,6 @@ async def send_presence_change_event( await asyncio.sleep(0.1) return ret - -def get_tz(hass: HomeAssistant): - """Get the current timezone""" - - return dt_util.get_time_zone(hass.config.time_zone) - - async def send_climate_change_event( entity: BaseThermostat, new_hvac_mode: HVACMode, diff --git a/tests/const.py b/tests/const.py index f12db1e..ea40d73 100644 --- a/tests/const.py +++ b/tests/const.py @@ -53,6 +53,8 @@ from custom_components.versatile_thermostat.const import ( CONF_AUTO_REGULATION_MODE, CONF_AUTO_REGULATION_MEDIUM, CONF_AUTO_REGULATION_NONE, + CONF_AUTO_REGULATION_DTEMP, + CONF_AUTO_REGULATION_PERIOD_MIN ) MOCK_TH_OVER_SWITCH_USER_CONFIG = { CONF_NAME: "TheOverSwitchMockName", @@ -125,7 +127,9 @@ MOCK_TH_OVER_SWITCH_TPI_CONFIG = { MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = { CONF_CLIMATE: "climate.mock_climate", CONF_AC_MODE: False, - CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_MEDIUM + CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_MEDIUM, + CONF_AUTO_REGULATION_DTEMP: 0.5, + CONF_AUTO_REGULATION_PERIOD_MIN: 2 } MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG = { @@ -137,7 +141,9 @@ MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG = { MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG = { CONF_CLIMATE: "climate.mock_climate", CONF_AC_MODE: True, - CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_MEDIUM + CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_MEDIUM, + CONF_AUTO_REGULATION_DTEMP: 0.5, + CONF_AUTO_REGULATION_PERIOD_MIN: 1 } MOCK_PRESETS_CONFIG = { diff --git a/tests/test_auto_regulation.py b/tests/test_auto_regulation.py index 5851134..eed8f02 100644 --- a/tests/test_auto_regulation.py +++ b/tests/test_auto_regulation.py @@ -20,7 +20,7 @@ from .commons import * # pylint: disable=wildcard-import, unused-wildcard-impor @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): +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( @@ -37,8 +37,11 @@ async def test_over_climate_regulation(hass: HomeAssistant, skip_hass_states_is_ fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {}) # Creates the regulated VTherm over climate + # change temperature so that the heating will start + event_timestamp = now - timedelta(minutes=10) + with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" + "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, @@ -73,45 +76,48 @@ async def test_over_climate_regulation(hass: HomeAssistant, skip_hass_states_is_ ] assert entity.preset_mode is PRESET_NONE - # Activate the heating by changing HVACMode and temperature - with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" - ): + # Activate the heating by changing HVACMode and temperature # Select a hvacmode, presence and preset await entity.async_set_hvac_mode(HVACMode.HEAT) assert entity.hvac_mode is HVACMode.HEAT assert entity.hvac_action == HVACAction.OFF - # change temperature so that the heating will start - event_timestamp = now - timedelta(minutes=10) + assert entity.regulated_target_temp is entity.min_temp + await send_temperature_change_event(entity, 15, event_timestamp) await send_ext_temperature_change_event(entity, 10, event_timestamp) + # 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 + ): + await entity.async_set_temperature(temperature=18) - # set manual target temp - await entity.async_set_temperature(temperature=18) + 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 - 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 - - # the regulated temperature should be greater - assert entity.regulated_target_temp > entity.target_temperature - assert entity.regulated_target_temp == 18+2.9 # In medium we could go up to +3 degre - assert entity.hvac_action == HVACAction.HEATING + # the regulated temperature should be greater + assert entity.regulated_target_temp > entity.target_temperature + assert entity.regulated_target_temp == 18+2.2 # In medium we could go up to +3 degre + assert entity.hvac_action == HVACAction.HEATING # change temperature so that the regulated temperature should slow down - event_timestamp = now - timedelta(minutes=9) - await send_temperature_change_event(entity, 19, event_timestamp) - await send_ext_temperature_change_event(entity, 18, event_timestamp) + event_timestamp = now - timedelta(minutes=5) + with patch( + "custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp + ): + await send_temperature_change_event(entity, 22, 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.1 + # the regulated temperature should be under + assert entity.regulated_target_temp < entity.target_temperature + assert entity.regulated_target_temp == 18-0.6 @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): +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( @@ -128,8 +134,11 @@ async def test_over_climate_regulation_ac_mode(hass: HomeAssistant, skip_hass_st fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {}) # Creates the regulated VTherm over climate + # change temperature so that the heating will start + event_timestamp = now - timedelta(minutes=10) + with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" + "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, @@ -164,47 +173,53 @@ async def test_over_climate_regulation_ac_mode(hass: HomeAssistant, skip_hass_st ] assert entity.preset_mode is PRESET_NONE - # Activate the heating by changing HVACMode and temperature - with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" - ): + # Activate the heating by changing HVACMode and temperature # Select a hvacmode, presence and preset await entity.async_set_hvac_mode(HVACMode.HEAT) assert entity.hvac_mode is HVACMode.HEAT assert entity.hvac_action == HVACAction.OFF # change temperature so that the heating will start - event_timestamp = now - timedelta(minutes=10) await send_temperature_change_event(entity, 30, event_timestamp) await send_ext_temperature_change_event(entity, 35, event_timestamp) # set manual target temp - await entity.async_set_temperature(temperature=25) + event_timestamp = now - timedelta(minutes=7) + with patch( + "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 - assert entity.hvac_action == HVACAction.COOLING - assert entity.preset_mode == PRESET_NONE # Manual mode + 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 - # 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.hvac_action == HVACAction.COOLING + # 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.hvac_action == HVACAction.COOLING # change temperature so that the regulated temperature should slow down - event_timestamp = now - timedelta(minutes=9) - await send_temperature_change_event(entity, 26, event_timestamp) - await send_ext_temperature_change_event(entity, 35, event_timestamp) + event_timestamp = now - timedelta(minutes=5) + with patch( + "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.7 + # the regulated temperature should be under + assert entity.regulated_target_temp < entity.target_temperature + assert entity.regulated_target_temp == 25-2.3 - # change temperature so that the regulated temperature should slow down - event_timestamp = now - timedelta(minutes=9) - await send_temperature_change_event(entity, 20, event_timestamp) - await send_ext_temperature_change_event(entity, 30, event_timestamp) + # 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 + ): + await send_temperature_change_event(entity, 20, 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+1.8 + # the regulated temperature should be greater + assert entity.regulated_target_temp > entity.target_temperature + assert entity.regulated_target_temp == 25+0.4