diff --git a/custom_components/versatile_thermostat/base_thermostat.py b/custom_components/versatile_thermostat/base_thermostat.py index 96ff629..06c5fa0 100644 --- a/custom_components/versatile_thermostat/base_thermostat.py +++ b/custom_components/versatile_thermostat/base_thermostat.py @@ -9,7 +9,6 @@ from datetime import timedelta, datetime from types import MappingProxyType from typing import Any, TypeVar, Generic -from homeassistant.util import dt as dt_util from homeassistant.core import ( HomeAssistant, callback, @@ -80,13 +79,6 @@ _LOGGER = logging.getLogger(__name__) ConfigData = MappingProxyType[str, Any] T = TypeVar("T", bound=UnderlyingEntity) - -def get_tz(hass: HomeAssistant): - """Get the current timezone""" - - return dt_util.get_time_zone(hass.config.time_zone) - - class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): """Representation of a base class for all Versatile Thermostat device.""" @@ -2293,7 +2285,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): @property def now(self) -> datetime: """Get now. The local datetime or the overloaded _set_now date""" - return self._now if self._now is not None else datetime.now(self._current_tz) + return self._now if self._now is not None else NowClass.get_now(self._hass) async def check_safety(self) -> bool: """Check if last temperature date is too long""" diff --git a/custom_components/versatile_thermostat/commons.py b/custom_components/versatile_thermostat/commons.py index b76267c..3e768ed 100644 --- a/custom_components/versatile_thermostat/commons.py +++ b/custom_components/versatile_thermostat/commons.py @@ -3,38 +3,20 @@ # pylint: disable=line-too-long import logging -from datetime import timedelta, datetime +from datetime import timedelta 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, ServiceConfigurationError _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)) - - def round_to_nearest(n: float, x: float) -> float: """Round a number to the nearest x (which should be decimal but not null) Example: diff --git a/custom_components/versatile_thermostat/const.py b/custom_components/versatile_thermostat/const.py index 06a3185..f66c530 100644 --- a/custom_components/versatile_thermostat/const.py +++ b/custom_components/versatile_thermostat/const.py @@ -4,8 +4,10 @@ import logging import math from typing import Literal +from datetime import datetime from enum import Enum +from homeassistant.core import HomeAssistant from homeassistant.const import CONF_NAME, Platform from homeassistant.components.climate import ( @@ -17,6 +19,7 @@ from homeassistant.components.climate import ( ) from homeassistant.exceptions import HomeAssistantError +from homeassistant.util import dt as dt_util from .prop_algorithm import ( PROPORTIONAL_FUNCTION_TPI, @@ -506,6 +509,24 @@ def get_safe_float(hass, entity_id: str): return None if math.isinf(float_val) or not math.isfinite(float_val) else float_val +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 UnknownEntity(HomeAssistantError): """Error to indicate there is an unknown entity_id given.""" diff --git a/custom_components/versatile_thermostat/thermostat_climate.py b/custom_components/versatile_thermostat/thermostat_climate.py index f5de04e..cd9a104 100644 --- a/custom_components/versatile_thermostat/thermostat_climate.py +++ b/custom_components/versatile_thermostat/thermostat_climate.py @@ -16,7 +16,7 @@ from homeassistant.components.climate import ( ClimateEntityFeature, ) -from .commons import NowClass, round_to_nearest +from .commons import round_to_nearest from .base_thermostat import BaseThermostat, ConfigData from .pi_algorithm import PITemperatureRegulator @@ -90,7 +90,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]): # 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) + self._last_regulation_change = None # NowClass.get_now(hass) @overrides def post_init(self, config_entry: ConfigData): @@ -206,16 +206,18 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]): force, ) - now: datetime = NowClass.get_now(self._hass) - period = float((now - self._last_regulation_change).total_seconds()) / 60.0 - if not force and period < self._auto_regulation_period_min: - _LOGGER.info( - "%s - period (%.1f) min is < %.0f min -> forget the regulation send", - self, - period, - self._auto_regulation_period_min, + if self._last_regulation_change is not None: + period = ( + float((self.now - self._last_regulation_change).total_seconds()) / 60.0 ) - return + if not force and period < self._auto_regulation_period_min: + _LOGGER.info( + "%s - period (%.1f) min is < %.0f min -> forget the regulation send", + self, + period, + self._auto_regulation_period_min, + ) + return if not self._regulated_target_temp: self._regulated_target_temp = self.target_temperature @@ -253,7 +255,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]): new_regulated_temp, ) - self._last_regulation_change = now + self._last_regulation_change = self.now for under in self._underlyings: # issue 348 - use device temperature if configured as offset offset_temp = 0 diff --git a/tests/commons.py b/tests/commons.py index f98ca62..be2986e 100644 --- a/tests/commons.py +++ b/tests/commons.py @@ -31,10 +31,6 @@ 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 ( # pylint: disable=unused-import - get_tz, - NowClass, -) from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI diff --git a/tests/test_auto_regulation.py b/tests/test_auto_regulation.py index 7126a48..19baddc 100644 --- a/tests/test_auto_regulation.py +++ b/tests/test_auto_regulation.py @@ -46,7 +46,7 @@ async def test_over_climate_regulation( event_timestamp = now - timedelta(minutes=10) with patch( - "custom_components.versatile_thermostat.commons.NowClass.get_now", + "custom_components.versatile_thermostat.const.NowClass.get_now", return_value=event_timestamp, ), patch( "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", @@ -87,7 +87,7 @@ async def test_over_climate_regulation( # 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", + "custom_components.versatile_thermostat.const.NowClass.get_now", return_value=event_timestamp, ): await entity.async_set_temperature(temperature=18) @@ -108,7 +108,7 @@ async def test_over_climate_regulation( # 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", + "custom_components.versatile_thermostat.const.NowClass.get_now", return_value=event_timestamp, ): await send_temperature_change_event(entity, 23, event_timestamp) @@ -144,7 +144,7 @@ async def test_over_climate_regulation_ac_mode( event_timestamp = now - timedelta(minutes=10) with patch( - "custom_components.versatile_thermostat.commons.NowClass.get_now", + "custom_components.versatile_thermostat.const.NowClass.get_now", return_value=event_timestamp, ), patch( "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", @@ -183,7 +183,7 @@ async def test_over_climate_regulation_ac_mode( # set manual target temp event_timestamp = now - timedelta(minutes=7) with patch( - "custom_components.versatile_thermostat.commons.NowClass.get_now", + "custom_components.versatile_thermostat.const.NowClass.get_now", return_value=event_timestamp, ): await entity.async_set_temperature(temperature=25) @@ -204,7 +204,7 @@ async def test_over_climate_regulation_ac_mode( # 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", + "custom_components.versatile_thermostat.const.NowClass.get_now", return_value=event_timestamp, ): await send_temperature_change_event(entity, 26, event_timestamp) @@ -219,7 +219,7 @@ async def test_over_climate_regulation_ac_mode( # 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", + "custom_components.versatile_thermostat.const.NowClass.get_now", return_value=event_timestamp, ): await send_temperature_change_event(entity, 18, event_timestamp) @@ -260,7 +260,7 @@ async def test_over_climate_regulation_limitations( event_timestamp = now - timedelta(minutes=20) with patch( - "custom_components.versatile_thermostat.commons.NowClass.get_now", + "custom_components.versatile_thermostat.const.NowClass.get_now", return_value=event_timestamp, ), patch( "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", @@ -286,71 +286,61 @@ async def test_over_climate_regulation_limitations( assert entity.is_over_climate is True assert entity.is_regulated is True + entity._set_now(event_timestamp) + # Will initialize the _last_regulation_change # 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 + await entity.async_set_temperature(temperature=17) # it is cold today 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 - 19) -> the regulation should be ignored because too early + # 1. 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, - ): - await entity.async_set_temperature(temperature=18) + entity._set_now(event_timestamp) + await entity.async_set_temperature(temperature=18) - fake_underlying_climate.set_hvac_action( - HVACAction.HEATING - ) # simulate under heating - assert entity.hvac_action == HVACAction.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 == 19.5 + # the regulated temperature will not change because when we set temp manually it is forced + assert entity.regulated_target_temp == 17 # 19.5 - # set manual target temp (at now - 18) -> the regulation should be taken into account + # 2. 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, - ): - await entity.async_set_temperature(temperature=17) - assert entity.regulated_target_temp > entity.target_temperature - 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 + entity._set_now(event_timestamp) - # change temperature so that dtemp < 0.5 and time is > period_min (+ 3min) + await entity.async_set_temperature(temperature=17) + assert entity.regulated_target_temp > entity.target_temperature + 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 + + # 3. 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, - ): - await send_temperature_change_event(entity, 16, event_timestamp) - await send_ext_temperature_change_event(entity, 10, event_timestamp) + entity._set_now(event_timestamp) + await send_temperature_change_event(entity, 16, event_timestamp) + await send_ext_temperature_change_event(entity, 10, event_timestamp) - # the regulated temperature should be under - assert entity.regulated_target_temp <= old_regulated_temp + # the regulated temperature should be under + assert entity.regulated_target_temp <= old_regulated_temp - # change temperature so that dtemp > 0.5 and time is > period_min (+ 3min) + # 4. 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, - ): - await send_temperature_change_event(entity, 15, event_timestamp) - await send_ext_temperature_change_event(entity, 12, event_timestamp) + entity._set_now(event_timestamp) + await send_ext_temperature_change_event(entity, 12, event_timestamp) + await send_temperature_change_event(entity, 15, 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 + # 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 @pytest.mark.parametrize("expected_lingering_tasks", [True]) @@ -383,7 +373,7 @@ async def test_over_climate_regulation_use_device_temp( event_timestamp = now - timedelta(minutes=10) with patch( - "custom_components.versatile_thermostat.commons.NowClass.get_now", + "custom_components.versatile_thermostat.const.NowClass.get_now", return_value=event_timestamp, ), patch( "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", @@ -416,7 +406,7 @@ async def test_over_climate_regulation_use_device_temp( fake_underlying_climate.set_current_temperature(15) event_timestamp = now - timedelta(minutes=7) with patch( - "custom_components.versatile_thermostat.commons.NowClass.get_now", + "custom_components.versatile_thermostat.const.NowClass.get_now", return_value=event_timestamp, ), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call: await entity.async_set_temperature(temperature=16) @@ -462,7 +452,7 @@ async def test_over_climate_regulation_use_device_temp( event_timestamp = now - timedelta(minutes=5) with patch( - "custom_components.versatile_thermostat.commons.NowClass.get_now", + "custom_components.versatile_thermostat.const.NowClass.get_now", return_value=event_timestamp, ), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call: await send_temperature_change_event(entity, 15, event_timestamp) @@ -497,7 +487,7 @@ async def test_over_climate_regulation_use_device_temp( event_timestamp = now - timedelta(minutes=3) with patch( - "custom_components.versatile_thermostat.commons.NowClass.get_now", + "custom_components.versatile_thermostat.const.NowClass.get_now", return_value=event_timestamp, ), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call: await send_temperature_change_event(entity, 25, event_timestamp) @@ -545,7 +535,7 @@ async def test_over_climate_regulation_dtemp_null( event_timestamp = now - timedelta(minutes=20) with patch( - "custom_components.versatile_thermostat.commons.NowClass.get_now", + "custom_components.versatile_thermostat.const.NowClass.get_now", return_value=event_timestamp, ), patch( "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", @@ -573,7 +563,7 @@ async def test_over_climate_regulation_dtemp_null( # set manual target temp event_timestamp = now - timedelta(minutes=17) with patch( - "custom_components.versatile_thermostat.commons.NowClass.get_now", + "custom_components.versatile_thermostat.const.NowClass.get_now", return_value=event_timestamp, ): await entity.async_set_temperature(temperature=20) @@ -594,7 +584,7 @@ async def test_over_climate_regulation_dtemp_null( # change temperature so that the regulated temperature should slow down event_timestamp = now - timedelta(minutes=15) with patch( - "custom_components.versatile_thermostat.commons.NowClass.get_now", + "custom_components.versatile_thermostat.const.NowClass.get_now", return_value=event_timestamp, ): await send_temperature_change_event(entity, 19, event_timestamp) @@ -607,7 +597,7 @@ async def test_over_climate_regulation_dtemp_null( # change temperature so that the regulated temperature should slow down event_timestamp = now - timedelta(minutes=13) with patch( - "custom_components.versatile_thermostat.commons.NowClass.get_now", + "custom_components.versatile_thermostat.const.NowClass.get_now", return_value=event_timestamp, ): await send_temperature_change_event(entity, 20, event_timestamp) @@ -621,7 +611,7 @@ async def test_over_climate_regulation_dtemp_null( # Test if a small temperature change is taken into account : change temperature so that dtemp < 0.5 and time is > period_min (+ 3min) event_timestamp = now - timedelta(minutes=10) with patch( - "custom_components.versatile_thermostat.commons.NowClass.get_now", + "custom_components.versatile_thermostat.const.NowClass.get_now", return_value=event_timestamp, ): await send_temperature_change_event(entity, 19.6, event_timestamp) diff --git a/tests/test_bugs.py b/tests/test_bugs.py index 361802e..804e354 100644 --- a/tests/test_bugs.py +++ b/tests/test_bugs.py @@ -161,19 +161,6 @@ async def test_bug_272( "homeassistant.core.ServiceRegistry.async_call" ) as mock_service_call: entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname") - # entry.add_to_hass(hass) - # await hass.config_entries.async_setup(entry.entry_id) - # assert entry.state is ConfigEntryState.LOADED - # - # def find_my_entity(entity_id) -> ClimateEntity: - # """Find my new entity""" - # component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN] - # for entity in component.entities: - # if entity.entity_id == entity_id: - # return entity - # - # entity = find_my_entity("climate.theoverclimatemockname") - assert entity assert entity.name == "TheOverClimateMockName" @@ -215,16 +202,18 @@ async def test_bug_272( ) tz = get_tz(hass) # pylint: disable=invalid-name - now: datetime = datetime.now(tz=tz) + event_timestamp: datetime = datetime.now(tz=tz) + entity._set_now(now) with patch( "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" ), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call: # Set room temperature to something very cold - event_timestamp = now + timedelta(minutes=1) + await send_temperature_change_event(entity, 13, now) + await send_ext_temperature_change_event(entity, 9, now) - await send_temperature_change_event(entity, 13, event_timestamp) - await send_ext_temperature_change_event(entity, 9, event_timestamp) + event_timestamp = event_timestamp + timedelta(minutes=3) + entity._set_now(event_timestamp) # Not in the accepted interval (15-19) await entity.async_set_temperature(temperature=10) @@ -248,12 +237,15 @@ async def test_bug_272( "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" ), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call: # Set room temperature to something very cold - event_timestamp = now + timedelta(minutes=1) + event_timestamp = event_timestamp + timedelta(minutes=1) + entity._set_now(event_timestamp) await send_temperature_change_event(entity, 13, event_timestamp) await send_ext_temperature_change_event(entity, 9, event_timestamp) # In the accepted interval + event_timestamp = event_timestamp + timedelta(minutes=3) + entity._set_now(event_timestamp) await entity.async_set_temperature(temperature=20.8) assert mock_service_call.call_count == 1 mock_service_call.assert_has_calls( diff --git a/tests/test_overclimate.py b/tests/test_overclimate.py index 9bad2a9..35cb646 100644 --- a/tests/test_overclimate.py +++ b/tests/test_overclimate.py @@ -517,6 +517,9 @@ async def test_bug_508( data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay ) + tz = get_tz(hass) # pylint: disable=invalid-name + now: datetime = datetime.now(tz=tz) + # Min_temp is 10 and max_temp is 31 and features contains TARGET_TEMPERATURE_RANGE fake_underlying_climate = MagicMockClimateWithTemperatureRange() @@ -545,6 +548,9 @@ async def test_bug_508( # Set the hvac_mode to HEAT await entity.async_set_hvac_mode(HVACMode.HEAT) + now = now + timedelta(minutes=3) # avoid temporal filter + entity._set_now(now) + # Not In the accepted interval -> should be converted into 10 (the min) and send with target_temp_high and target_temp_low await entity.async_set_temperature(temperature=8.5) @@ -568,6 +574,9 @@ async def test_bug_508( with patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call: # Not In the accepted interval -> should be converted into 10 (the min) and send with target_temp_high and target_temp_low + now = now + timedelta(minutes=3) # avoid temporal filter + entity._set_now(now) + await entity.async_set_temperature(temperature=32) # MagicMock climate is already HEAT by default. So there is no SET_HAVC_MODE call diff --git a/tests/test_overclimate_valve.py b/tests/test_overclimate_valve.py index 84cbc25..497fc2d 100644 --- a/tests/test_overclimate_valve.py +++ b/tests/test_overclimate_valve.py @@ -136,15 +136,15 @@ async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get assert mock_service_call.call_count == 3 mock_service_call.assert_has_calls( [ - call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree'}), - call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree'}), - # we have no current_temperature yet - # call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration'}), call("climate","set_temperature",{ "entity_id": "climate.mock_climate", "temperature": 15, # temp-min }, ), + call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree'}), + call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree'}), + # we have no current_temperature yet + # call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration'}), ] ) @@ -416,6 +416,7 @@ async def test_over_climate_valve_multi_presence( assert vtherm.target_temperature == 17.2 + # 2: set presence on -> should activate the valve and change target # fmt: off with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \ patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\ @@ -432,164 +433,43 @@ async def test_over_climate_valve_multi_presence( # the underlying set temperature call and the call to the valve assert mock_service_call.call_count == 8 - mock_service_call.assert_has_calls( - [ - call(domain='number', service='set_value', service_data={'value': 40}, target={'entity_id': 'number.mock_opening_degree1'}), - call(domain='number', service='set_value', service_data={'value': 60}, target={'entity_id': 'number.mock_closing_degree1'}), - call(domain='number', service='set_value', service_data={'value': 3}, target={'entity_id': 'number.mock_offset_calibration1'}), - call(domain='number', service='set_value', service_data={'value': 40}, target={'entity_id': 'number.mock_opening_degree2'}), - call(domain='number', service='set_value', service_data={'value': 60}, target={'entity_id': 'number.mock_closing_degree2'}), - call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration2'}), - call("climate","set_temperature",{ - "entity_id": "climate.mock_climate1", - "temperature": 19, - }, - ), - call("climate","set_temperature",{ - "entity_id": "climate.mock_climate2", - "temperature": 19, - }, - ), + mock_service_call.assert_has_calls([ + call('climate', 'set_temperature', {'entity_id': 'climate.mock_climate1', 'temperature': 19.0}), + call('climate', 'set_temperature', {'entity_id': 'climate.mock_climate2', 'temperature': 19.0}), + call(domain='number', service='set_value', service_data={'value': 40}, target={'entity_id': 'number.mock_opening_degree1'}), + call(domain='number', service='set_value', service_data={'value': 60}, target={'entity_id': 'number.mock_closing_degree1'}), + call(domain='number', service='set_value', service_data={'value': 3.0}, target={'entity_id': 'number.mock_offset_calibration1'}), + call(domain='number', service='set_value', service_data={'value': 40}, target={'entity_id': 'number.mock_opening_degree2'}), + call(domain='number', service='set_value', service_data={'value': 60}, target={'entity_id': 'number.mock_closing_degree2'}), + call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration2'}) ] ) - assert mock_get_state.call_count > 5 # each temp sensor + each valve - - - # 2. Starts heating slowly (18 vs 19) - now = now + timedelta(minutes=1) - vtherm._set_now(now) - - await vtherm.async_set_hvac_mode(HVACMode.HEAT) + # 3: set presence off -> should deactivate the valve and change target # fmt: off with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \ patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\ patch("homeassistant.core.StateMachine.get", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state: # fmt: on - now = now + timedelta(minutes=2) # avoid temporal filter + now = now + timedelta(minutes=3) vtherm._set_now(now) - await vtherm.async_set_preset_mode(PRESET_COMFORT) + await send_presence_change_event(vtherm, False, True, now) await hass.async_block_till_done() - assert vtherm.hvac_mode is HVACMode.HEAT - assert vtherm.preset_mode is PRESET_COMFORT - assert vtherm.target_temperature == 19 - assert vtherm.current_temperature == 18 - assert vtherm.valve_open_percent == 40 # 0.3*1 + 0.1*1 - - - assert mock_service_call.call_count == 4 - mock_service_call.assert_has_calls( - [ - call('climate', 'set_temperature', {'entity_id': 'climate.mock_climate', 'temperature': 19.0}), - call(domain='number', service='set_value', service_data={'value': 40}, target={'entity_id': 'number.mock_opening_degree'}), - call(domain='number', service='set_value', service_data={'value': 60}, target={'entity_id': 'number.mock_closing_degree'}), - # 3 = 18 (room) - 15 (current of underlying) + 0 (current offset) - call(domain='number', service='set_value', service_data={'value': 3.0}, target={'entity_id': 'number.mock_offset_calibration'}) - ] - ) - - # set the opening to 40% - mock_get_state_side_effect.add_or_update_side_effect( - "number.mock_opening_degree", - State( - "number.mock_opening_degree", "40", {"min": 0, "max": 100} - )) - - assert vtherm.hvac_action is HVACAction.HEATING - assert vtherm.is_device_active is True - - # 2. Starts heating very slowly (18.9 vs 19) - now = now + timedelta(minutes=2) - vtherm._set_now(now) - # fmt: off - with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \ - patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\ - patch("homeassistant.core.StateMachine.get", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state: - # fmt: on - # set the offset to 3 - mock_get_state_side_effect.add_or_update_side_effect( - "number.mock_offset_calibration", - State( - "number.mock_offset_calibration", "3", {"min": -12, "max": 12} - )) - - await send_temperature_change_event(vtherm, 18.9, now, True) - await hass.async_block_till_done() - - assert vtherm.hvac_mode is HVACMode.HEAT - assert vtherm.preset_mode is PRESET_COMFORT - assert vtherm.target_temperature == 19 - assert vtherm.current_temperature == 18.9 - assert vtherm.valve_open_percent == 13 # 0.3*0.1 + 0.1*1 - - - assert mock_service_call.call_count == 3 - mock_service_call.assert_has_calls( - [ - call(domain='number', service='set_value', service_data={'value': 13}, target={'entity_id': 'number.mock_opening_degree'}), - call(domain='number', service='set_value', service_data={'value': 87}, target={'entity_id': 'number.mock_closing_degree'}), - # 6 = 18 (room) - 15 (current of underlying) + 3 (current offset) - call(domain='number', service='set_value', service_data={'value': 6.899999999999999}, target={'entity_id': 'number.mock_offset_calibration'}) - ] - ) - - # set the opening to 13% - mock_get_state_side_effect.add_or_update_side_effect( - "number.mock_opening_degree", - State( - "number.mock_opening_degree", "13", {"min": 0, "max": 100} - )) - - assert vtherm.hvac_action is HVACAction.HEATING - assert vtherm.is_device_active is True - - # 3. Stop heating 21 > 19 - now = now + timedelta(minutes=2) - vtherm._set_now(now) - # fmt: off - with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \ - patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\ - patch("homeassistant.core.StateMachine.get", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state: - # fmt: on - # set the offset to 3 - mock_get_state_side_effect.add_or_update_side_effect( - "number.mock_offset_calibration", - State( - "number.mock_offset_calibration", "3", {"min": -12, "max": 12} - )) - - await send_temperature_change_event(vtherm, 21, now, True) - await hass.async_block_till_done() - - assert vtherm.hvac_mode is HVACMode.HEAT - assert vtherm.preset_mode is PRESET_COMFORT - assert vtherm.target_temperature == 19 - assert vtherm.current_temperature == 21 - assert vtherm.valve_open_percent == 0 # 0.3* (-2) + 0.1*1 - - - assert mock_service_call.call_count == 3 - mock_service_call.assert_has_calls( - [ - call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree'}), - call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree'}), - # 6 = 18 (room) - 15 (current of underlying) + 3 (current offset) - call(domain='number', service='set_value', service_data={'value': 9.0}, target={'entity_id': 'number.mock_offset_calibration'}) - ] - ) - - # set the opening to 13% - mock_get_state_side_effect.add_or_update_side_effect( - "number.mock_opening_degree", - State( - "number.mock_opening_degree", "0", {"min": 0, "max": 100} - )) - - assert vtherm.hvac_action is HVACAction.OFF assert vtherm.is_device_active is False + assert vtherm.valve_open_percent == 0 - - - await hass.async_block_till_done() + # the underlying set temperature call and the call to the valve + assert mock_service_call.call_count == 8 + mock_service_call.assert_has_calls([ + call('climate', 'set_temperature', {'entity_id': 'climate.mock_climate1', 'temperature': 17.2}), + call('climate', 'set_temperature', {'entity_id': 'climate.mock_climate2', 'temperature': 17.2}), + call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree1'}), + call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree1'}), + call(domain='number', service='set_value', service_data={'value': 3.0}, target={'entity_id': 'number.mock_offset_calibration1'}), + call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree2'}), + call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree2'}), + call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration2'}) + ] + ) diff --git a/tests/test_temp_number.py b/tests/test_temp_number.py index 3672458..0100cb6 100644 --- a/tests/test_temp_number.py +++ b/tests/test_temp_number.py @@ -17,7 +17,7 @@ from custom_components.versatile_thermostat.base_thermostat import BaseThermosta from custom_components.versatile_thermostat.thermostat_switch import ( ThermostatOverSwitch, ) -from custom_components.versatile_thermostat.commons import NowClass +from custom_components.versatile_thermostat.const import NowClass from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI from .commons import *