From 05fe2055e20df2f112d82c9fe875087e5c7dcf7e Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Sun, 19 Jan 2025 20:45:37 +0100 Subject: [PATCH] [Feature Request] - Pwoer shedding should prevent all VTherm to start at the same time (#845) Fixes #844 Co-authored-by: Jean-Marc Collin --- .../versatile_thermostat/base_thermostat.py | 4 +- .../central_feature_power_manager.py | 15 ++ .../feature_power_manager.py | 10 +- tests/test_binary_sensors.py | 4 +- tests/test_central_power_manager.py | 156 ++++++++++++++++++ tests/test_motion.py | 99 ++++------- tests/test_multiple_switch.py | 73 +++----- tests/test_power.py | 9 +- tests/test_window.py | 36 ++-- 9 files changed, 261 insertions(+), 145 deletions(-) diff --git a/custom_components/versatile_thermostat/base_thermostat.py b/custom_components/versatile_thermostat/base_thermostat.py index 0ce7f6a..6e48783 100644 --- a/custom_components/versatile_thermostat/base_thermostat.py +++ b/custom_components/versatile_thermostat/base_thermostat.py @@ -1528,8 +1528,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): is_window_detected = self._window_manager.is_window_detected if new_central_mode == CENTRAL_MODE_AUTO: if not is_window_detected and not first_init: - await self.restore_hvac_mode() - await self.restore_preset_mode() + await self.restore_preset_mode(force=False) + await self.restore_hvac_mode(need_control_heating=True) elif is_window_detected and self.hvac_mode == HVACMode.OFF: # do not restore but mark the reason of off with window detection self.set_hvac_off_reason(HVAC_OFF_REASON_WINDOW_DETECTION) diff --git a/custom_components/versatile_thermostat/central_feature_power_manager.py b/custom_components/versatile_thermostat/central_feature_power_manager.py index 8762413..3461adb 100644 --- a/custom_components/versatile_thermostat/central_feature_power_manager.py +++ b/custom_components/versatile_thermostat/central_feature_power_manager.py @@ -47,6 +47,7 @@ class CentralFeaturePowerManager(BaseFeatureManager): self._current_max_power: float = None self._power_temp: float = None self._cancel_calculate_shedding_call = None + self._started_vtherm_total_power: float = None # Not used now self._last_shedding_date = None @@ -71,6 +72,7 @@ class CentralFeaturePowerManager(BaseFeatureManager): and self._power_temp ): self._is_configured = True + self._started_vtherm_total_power = 0 else: _LOGGER.info("Power management is not fully configured and will be deactivated") @@ -102,6 +104,8 @@ class CentralFeaturePowerManager(BaseFeatureManager): """Handle power changes.""" _LOGGER.debug("Receive new Power event") _LOGGER.debug(event) + + self._started_vtherm_total_power = 0 await self.refresh_state() @callback @@ -275,6 +279,12 @@ class CentralFeaturePowerManager(BaseFeatureManager): vtherms.sort(key=cmp_to_key(cmp_temps)) return vtherms + def add_started_vtherm_total_power(self, started_power: float): + """Add the power into the _started_vtherm_total_power which holds all VTherm started after + the last power measurement""" + self._started_vtherm_total_power += started_power + _LOGGER.debug("%s - started_vtherm_total_power is now %s", self, self._started_vtherm_total_power) + @property def is_configured(self) -> bool: """True if the FeatureManager is fully configured""" @@ -305,5 +315,10 @@ class CentralFeaturePowerManager(BaseFeatureManager): """Return the max power sensor entity id""" return self._max_power_sensor_entity_id + @property + def started_vtherm_total_power(self) -> float | None: + """Return the started_vtherm_total_power""" + return self._started_vtherm_total_power + def __str__(self): return "CentralPowerManager" diff --git a/custom_components/versatile_thermostat/feature_power_manager.py b/custom_components/versatile_thermostat/feature_power_manager.py index 8642d78..21f1986 100644 --- a/custom_components/versatile_thermostat/feature_power_manager.py +++ b/custom_components/versatile_thermostat/feature_power_manager.py @@ -104,7 +104,8 @@ class FeaturePowerManager(BaseFeatureManager): async def check_power_available(self) -> bool: """Check if the Vtherm can be started considering overpowering. - Returns True if no overpowering conditions are found + Returns True if no overpowering conditions are found. + If True the vtherm power is written into the temporay vtherm started """ vtherm_api = VersatileThermostatAPI.get_vtherm_api() @@ -116,6 +117,7 @@ class FeaturePowerManager(BaseFeatureManager): current_power = vtherm_api.central_power_manager.current_power current_max_power = vtherm_api.central_power_manager.current_max_power + started_vtherm_total_power = vtherm_api.central_power_manager.started_vtherm_total_power if ( current_power is None or current_max_power is None @@ -146,7 +148,7 @@ class FeaturePowerManager(BaseFeatureManager): self._device_power * self._vtherm.proportional_algorithm.on_percent, ) - ret = (current_power + power_consumption_max) < current_max_power + ret = (current_power + started_vtherm_total_power + power_consumption_max) < current_max_power if not ret: _LOGGER.info( "%s - there is not enough power available power=%.3f, max_power=%.3f heater power=%.3f", @@ -155,6 +157,10 @@ class FeaturePowerManager(BaseFeatureManager): current_max_power, self._device_power, ) + else: + # Adds the current_power_max to the started vtherm total power + vtherm_api.central_power_manager.add_started_vtherm_total_power(power_consumption_max) + return ret async def set_overpowering(self, overpowering: bool, power_consumption_max=0): diff --git a/tests/test_binary_sensors.py b/tests/test_binary_sensors.py index c74f9bc..da3807e 100644 --- a/tests/test_binary_sensors.py +++ b/tests/test_binary_sensors.py @@ -1,7 +1,7 @@ # pylint: disable=wildcard-import, unused-wildcard-import, unused-argument, line-too-long, protected-access """ Test the normal start of a Thermostat """ -from unittest.mock import patch +from unittest.mock import patch, PropertyMock from datetime import timedelta, datetime from homeassistant.core import HomeAssistant @@ -179,7 +179,7 @@ async def test_overpowering_binary_sensors( ) # fmt:off with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \ - patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", return_value="True"): + patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", new_callable=PropertyMock, return_value=True): # fmt: on await send_power_change_event(entity, 150, now) await send_max_power_change_event(entity, 100, now) diff --git a/tests/test_central_power_manager.py b/tests/test_central_power_manager.py index f2a4852..3f83095 100644 --- a/tests/test_central_power_manager.py +++ b/tests/test_central_power_manager.py @@ -1,5 +1,6 @@ # pylint: disable=protected-access, unused-argument, line-too-long """ Test the Central Power management """ +import asyncio from unittest.mock import patch, AsyncMock, MagicMock, PropertyMock from datetime import datetime, timedelta import logging @@ -10,6 +11,14 @@ from custom_components.versatile_thermostat.feature_power_manager import ( from custom_components.versatile_thermostat.central_feature_power_manager import ( CentralFeaturePowerManager, ) + +from custom_components.versatile_thermostat.thermostat_switch import ( + ThermostatOverSwitch, +) +from custom_components.versatile_thermostat.thermostat_climate import ( + ThermostatOverClimate, +) + from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import logging.getLogger().setLevel(logging.DEBUG) @@ -700,3 +709,150 @@ async def test_central_power_manager_max_power_event( assert central_power_manager.current_max_power == expected_power assert mock_calculate_shedding.call_count == nb_call + + +async def test_central_power_manager_start_vtherm_power(hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager): + """Tests the central power start VTherm power. The objective is to starts VTherm until the power max is exceeded""" + + temps = { + "eco": 17, + "comfort": 18, + "boost": 19, + } + + entry = MockConfigEntry( + domain=DOMAIN, + title="TheOverSwitchMockName", + unique_id="uniqueId", + data={ + CONF_NAME: "TheOverSwitchMockName", + CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH, + CONF_TEMP_SENSOR: "sensor.mock_temp_sensor", + CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor", + CONF_CYCLE_MIN: 5, + CONF_TEMP_MIN: 15, + CONF_TEMP_MAX: 30, + CONF_USE_WINDOW_FEATURE: False, + CONF_USE_MOTION_FEATURE: False, + CONF_USE_POWER_FEATURE: True, + CONF_USE_PRESENCE_FEATURE: False, + CONF_UNDERLYING_LIST: ["switch.mock_switch"], + CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, + CONF_TPI_COEF_INT: 0.3, + CONF_TPI_COEF_EXT: 0.01, + CONF_MINIMAL_ACTIVATION_DELAY: 30, + CONF_SAFETY_DELAY_MIN: 5, + CONF_SAFETY_MIN_ON_PERCENT: 0.3, + CONF_DEVICE_POWER: 1000, + CONF_PRESET_POWER: 12, + }, + ) + + entity: ThermostatOverSwitch = await create_thermostat(hass, entry, "climate.theoverswitchmockname", temps) + assert entity + + now: datetime = NowClass.get_now(hass) + VersatileThermostatAPI.get_vtherm_api()._set_now(now) + + central_power_manager = VersatileThermostatAPI.get_vtherm_api().central_power_manager + assert central_power_manager + + side_effects = SideEffects( + { + "sensor.the_power_sensor": State("sensor.the_power_sensor", 1000), + "sensor.the_max_power_sensor": State("sensor.the_max_power_sensor", 2100), + }, + State("unknown.entity_id", "unknown"), + ) + + # 1. Make the heater heats + # fmt: off + with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \ + patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", new_callable=PropertyMock, return_value=False): + # fmt: on + # make the heater heats + await send_power_change_event(entity, 1000, now) + await send_max_power_change_event(entity, 2100, now) + + await send_temperature_change_event(entity, 15, now) + await send_ext_temperature_change_event(entity, 1, now) + + await entity.async_set_preset_mode(PRESET_BOOST) + assert entity.preset_mode is PRESET_BOOST + assert entity.power_manager.overpowering_state is STATE_UNKNOWN + assert entity.target_temperature == 19 + await hass.async_block_till_done() + + await entity.async_set_hvac_mode(HVACMode.HEAT) + assert entity.hvac_mode is HVACMode.HEAT + + await hass.async_block_till_done() + await asyncio.sleep(0.1) + + # the power of Vtherm should have been added + assert central_power_manager.started_vtherm_total_power == 1000 + + # 2. Check that another heater cannot heat + entry2 = MockConfigEntry( + domain=DOMAIN, + title="TheOverClimateMockName2", + unique_id="uniqueId2", + data={ + CONF_NAME: "TheOverClimateMockName2", + CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE, + CONF_TEMP_SENSOR: "sensor.mock_temp_sensor", + CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor", + CONF_CYCLE_MIN: 5, + CONF_TEMP_MIN: 15, + CONF_TEMP_MAX: 30, + CONF_USE_WINDOW_FEATURE: False, + CONF_USE_MOTION_FEATURE: False, + CONF_USE_POWER_FEATURE: True, + CONF_USE_PRESENCE_FEATURE: False, + CONF_UNDERLYING_LIST: ["switch.mock_climate"], + CONF_MINIMAL_ACTIVATION_DELAY: 30, + CONF_SAFETY_DELAY_MIN: 5, + CONF_SAFETY_MIN_ON_PERCENT: 0.3, + CONF_DEVICE_POWER: 150, + CONF_PRESET_POWER: 12, + }, + ) + + entity2: ThermostatOverClimate = await create_thermostat(hass, entry2, "climate.theoverclimatemockname2", temps) + assert entity2 + + fake_underlying_climate = MockClimate( + hass=hass, + unique_id="mockUniqueId", + name="MockClimateName", + ) + + # fmt: off + with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \ + patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", new_callable=PropertyMock, return_value=False), \ + patch("custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",return_value=fake_underlying_climate): + # fmt: on + # make the heater heats + await entity2.async_set_preset_mode(PRESET_COMFORT) + assert entity2.preset_mode is PRESET_COMFORT + assert entity2.power_manager.overpowering_state is STATE_UNKNOWN + assert entity2.target_temperature == 18 + await entity2.async_set_hvac_mode(HVACMode.HEAT) + assert entity2.hvac_mode is HVACMode.HEAT + + await hass.async_block_till_done() + await asyncio.sleep(0.1) + + # the power of Vtherm should have not been added (cause it has not started) and the entity2 should be shedding + assert central_power_manager.started_vtherm_total_power == 1000 + + + assert entity2.power_manager.overpowering_state is STATE_ON + + # 3. sends a new power sensor event + await send_max_power_change_event(entity, 2150, now) + # No change + assert central_power_manager.started_vtherm_total_power == 1000 + + await send_power_change_event(entity, 1010, now) + assert central_power_manager.started_vtherm_total_power == 0 diff --git a/tests/test_motion.py b/tests/test_motion.py index ae7bf00..fb345c9 100644 --- a/tests/test_motion.py +++ b/tests/test_motion.py @@ -347,22 +347,17 @@ async def test_motion_management_time_not_enough( assert entity.presence_state == STATE_ON # 2. starts detecting motion with time not enough - with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" - ) as mock_send_event, patch( + with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" - ) as mock_heater_on, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" - ) as mock_heater_off, patch( + ) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", + new_callable=PropertyMock, return_value=False, ), patch( "homeassistant.helpers.condition.state", return_value=False ) as mock_condition, patch( "homeassistant.core.StateMachine.get", - return_value=State( - entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF - ), + return_value=State(entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF), ): event_timestamp = now - timedelta(minutes=4) try_condition = await send_motion_change_event( @@ -387,14 +382,11 @@ async def test_motion_management_time_not_enough( assert mock_send_event.call_count == 0 # starts detecting motion with time enough this time - with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" - ) as mock_send_event, patch( + with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" - ) as mock_heater_on, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" - ) as mock_heater_off, patch( + ) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", + new_callable=PropertyMock, return_value=False, ), patch( "homeassistant.helpers.condition.state", return_value=True @@ -415,22 +407,17 @@ async def test_motion_management_time_not_enough( assert entity.presence_state == STATE_ON # stop detecting motion with off delay too low - with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" - ) as mock_send_event, patch( + with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" - ) as mock_heater_on, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" - ) as mock_heater_off, patch( + ) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", + new_callable=PropertyMock, return_value=True, ) as mock_device_active, patch( "homeassistant.helpers.condition.state", return_value=False ) as mock_condition, patch( "homeassistant.core.StateMachine.get", - return_value=State( - entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF - ), + return_value=State(entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF), ): event_timestamp = now - timedelta(minutes=2) try_condition = await send_motion_change_event( @@ -454,14 +441,11 @@ async def test_motion_management_time_not_enough( assert mock_send_event.call_count == 0 # stop detecting motion with off delay enough long - with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" - ) as mock_send_event, patch( + with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" - ) as mock_heater_on, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" - ) as mock_heater_off, patch( + ) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", + new_callable=PropertyMock, return_value=True, ) as mock_device_active, patch( "homeassistant.helpers.condition.state", return_value=True @@ -562,14 +546,11 @@ async def test_motion_management_time_enough_and_presence( assert entity.presence_state == "on" # starts detecting motion - with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" - ) as mock_send_event, patch( + with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" - ) as mock_heater_on, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" - ) as mock_heater_off, patch( + ) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", + new_callable=PropertyMock, return_value=False, ), patch( "homeassistant.helpers.condition.state", return_value=True @@ -590,14 +571,11 @@ async def test_motion_management_time_enough_and_presence( assert mock_send_event.call_count == 0 # stop detecting motion with confirmation of stop - with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" - ) as mock_send_event, patch( + with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" - ) as mock_heater_on, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" - ) as mock_heater_off, patch( + ) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", + new_callable=PropertyMock, return_value=True, ), patch( "homeassistant.helpers.condition.state", return_value=True @@ -693,14 +671,11 @@ async def test_motion_management_time_enough_and_not_presence( assert entity.presence_state == STATE_OFF # starts detecting motion - with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" - ) as mock_send_event, patch( + with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" - ) as mock_heater_on, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" - ) as mock_heater_off, patch( + ) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", + new_callable=PropertyMock, return_value=False, ), patch( "homeassistant.helpers.condition.state", return_value=True @@ -722,14 +697,11 @@ async def test_motion_management_time_enough_and_not_presence( assert mock_send_event.call_count == 0 # stop detecting motion with confirmation of stop - with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" - ) as mock_send_event, patch( + with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" - ) as mock_heater_on, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" - ) as mock_heater_off, patch( + ) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", + new_callable=PropertyMock, return_value=True, ), patch( "homeassistant.helpers.condition.state", return_value=True @@ -826,14 +798,11 @@ async def test_motion_management_with_stop_during_condition( assert entity.presence_state == STATE_OFF # starts detecting motion - with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" - ) as mock_send_event, patch( + with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" - ) as mock_heater_on, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" - ) as mock_heater_off, patch( + ) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", + new_callable=PropertyMock, return_value=True, ), patch( "homeassistant.helpers.condition.state", return_value=True @@ -959,12 +928,11 @@ async def test_motion_management_with_stop_during_condition_last_state_on( # 1. starts detecting motion but the sensor is off with patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", + new_callable=PropertyMock, return_value=True, ), patch("homeassistant.helpers.condition.state", return_value=False), patch( "homeassistant.core.StateMachine.get", - return_value=State( - entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF - ), + return_value=State(entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF), ): event_timestamp = now - timedelta(minutes=5) try_condition1 = await send_motion_change_event( @@ -982,12 +950,11 @@ async def test_motion_management_with_stop_during_condition_last_state_on( # 2. starts detecting motion but the sensor is on with patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", + new_callable=PropertyMock, return_value=True, ), patch("homeassistant.helpers.condition.state", return_value=False), patch( "homeassistant.core.StateMachine.get", - return_value=State( - entity_id="binary_sensor.mock_motion_sensor", state=STATE_ON - ), + return_value=State(entity_id="binary_sensor.mock_motion_sensor", state=STATE_ON), ): event_timestamp = now - timedelta(minutes=5) try_condition1 = await send_motion_change_event( diff --git a/tests/test_multiple_switch.py b/tests/test_multiple_switch.py index 38be50b..0c85e91 100644 --- a/tests/test_multiple_switch.py +++ b/tests/test_multiple_switch.py @@ -2,7 +2,7 @@ """ Test the Multiple switch management """ import asyncio -from unittest.mock import patch, call, ANY +from unittest.mock import patch, call, ANY, PropertyMock from datetime import datetime, timedelta import logging @@ -84,14 +84,11 @@ async def test_one_switch_cycle( assert mock_is_state.call_count == 1 # Set temperature to a low level - with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" - ) as mock_send_event, patch( + with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" - ) as mock_heater_on, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" - ) as mock_heater_off, patch( + ) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", + new_callable=PropertyMock, return_value=False, ) as mock_device_active, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.call_later", @@ -107,7 +104,8 @@ async def test_one_switch_cycle( # assert mock_heater_on.call_count == 1 assert mock_heater_on.call_count == 0 # There is no check if active - assert mock_device_active.call_count == 0 + # don't work with PropertyMock + # assert mock_device_active.call_count == 0 # 4 calls dispatched along the cycle assert mock_call_later.call_count == 1 @@ -119,14 +117,11 @@ async def test_one_switch_cycle( # Set a temperature at middle level event_timestamp = now - timedelta(minutes=4) - with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" - ) as mock_send_event, patch( + with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" - ) as mock_heater_on, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" - ) as mock_heater_off, patch( + ) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", + new_callable=PropertyMock, return_value=False, ) as mock_device_active: await send_temperature_change_event(entity, 18, event_timestamp) @@ -141,14 +136,11 @@ async def test_one_switch_cycle( # Set another temperature at middle level event_timestamp = now - timedelta(minutes=3) - with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" - ) as mock_send_event, patch( + with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" - ) as mock_heater_on, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" - ) as mock_heater_off, patch( + ) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", + new_callable=PropertyMock, return_value=True, ) as mock_device_active: await send_temperature_change_event(entity, 18.1, event_timestamp) @@ -176,14 +168,11 @@ async def test_one_switch_cycle( # Simulate the end of heater on cycle event_timestamp = now - timedelta(minutes=3) - with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" - ) as mock_send_event, patch( + with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" - ) as mock_heater_on, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" - ) as mock_heater_off, patch( + ) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", + new_callable=PropertyMock, return_value=True, ) as mock_device_active: await entity.underlying_entity( @@ -201,14 +190,11 @@ async def test_one_switch_cycle( # Simulate the start of heater on cycle event_timestamp = now - timedelta(minutes=3) - with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" - ) as mock_send_event, patch( + with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" - ) as mock_heater_on, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" - ) as mock_heater_off, patch( + ) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", + new_callable=PropertyMock, return_value=True, ) as mock_device_active: await entity.underlying_entity( @@ -306,14 +292,11 @@ async def test_multiple_switchs( ) # Set temperature to a low level - with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" - ) as mock_send_event, patch( + with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" - ) as mock_heater_on, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" - ) as mock_heater_off, patch( + ) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", + new_callable=PropertyMock, return_value=False, ) as mock_device_active, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.call_later", @@ -329,7 +312,8 @@ async def test_multiple_switchs( # assert mock_heater_on.call_count == 1 assert mock_heater_on.call_count == 0 # There is no check if active - assert mock_device_active.call_count == 0 + # don't work with PropertyMock + # assert mock_device_active.call_count == 0 # 4 calls dispatched along the cycle assert mock_call_later.call_count == 4 @@ -344,14 +328,11 @@ async def test_multiple_switchs( # Set a temperature at middle level event_timestamp = now - timedelta(minutes=4) - with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" - ) as mock_send_event, patch( + with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" - ) as mock_heater_on, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" - ) as mock_heater_off, patch( + ) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", + new_callable=PropertyMock, return_value=False, ) as mock_device_active: await send_temperature_change_event(entity, 18, event_timestamp) @@ -818,7 +799,7 @@ async def test_multiple_switch_power_management( with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \ patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \ patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, \ - patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", return_value="True"): + patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", new_callable=PropertyMock, return_value=True): #fmt: on now = now + timedelta(seconds=30) VersatileThermostatAPI.get_vtherm_api()._set_now(now) diff --git a/tests/test_power.py b/tests/test_power.py index 8c38493..948bd51 100644 --- a/tests/test_power.py +++ b/tests/test_power.py @@ -523,7 +523,7 @@ async def test_power_management_hvac_on( patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \ patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \ patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, \ - patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", return_value="True"): + patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", new_callable=PropertyMock, return_value=True): # fmt: on now = now + timedelta(seconds=30) VersatileThermostatAPI.get_vtherm_api()._set_now(now) @@ -913,12 +913,15 @@ async def test_power_management_turn_off_while_shedding(hass: HomeAssistant, ski # 1. Set VTherm to overpowering # Send power max mesurement too low and HVACMode is on and device is active + + # + # # fmt:off with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \ patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"), \ patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \ patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, \ - patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", return_value="True"): + patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", new_callable=PropertyMock, return_value=True): # fmt: on now = now + timedelta(seconds=30) VersatileThermostatAPI.get_vtherm_api()._set_now(now) @@ -939,7 +942,7 @@ async def test_power_management_turn_off_while_shedding(hass: HomeAssistant, ski patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \ patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \ patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, \ - patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", return_value="True"): + patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", new_callable=PropertyMock, return_value=True): # fmt: on now = now + timedelta(seconds=30) VersatileThermostatAPI.get_vtherm_api()._set_now(now) diff --git a/tests/test_window.py b/tests/test_window.py index 29adb62..e8f7002 100644 --- a/tests/test_window.py +++ b/tests/test_window.py @@ -2039,16 +2039,13 @@ async def test_bug_66( assert entity.window_state is STATE_UNKNOWN # Open the window and let the thermostat shut down - with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" - ) as mock_send_event, patch( + with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" - ) as mock_heater_on, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" - ) as mock_heater_off, patch( + ) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( "homeassistant.helpers.condition.state", return_value=True ) as mock_condition, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", + new_callable=PropertyMock, return_value=True, ): await send_temperature_change_event(entity, 15, now) @@ -2067,16 +2064,13 @@ async def test_bug_66( assert entity.window_state == STATE_ON # Close the window but too shortly - with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" - ) as mock_send_event, patch( + with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" - ) as mock_heater_on, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" - ) as mock_heater_off, patch( + ) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( "homeassistant.helpers.condition.state", return_value=False ) as mock_condition, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", + new_callable=PropertyMock, return_value=False, ): event_timestamp = now + timedelta(minutes=1) @@ -2090,16 +2084,13 @@ async def test_bug_66( assert entity.window_state == STATE_ON # Reopen immediatly with sufficient time - with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" - ) as mock_send_event, patch( + with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" - ) as mock_heater_on, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" - ) as mock_heater_off, patch( + ) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( "homeassistant.helpers.condition.state", return_value=True ) as mock_condition, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", + new_callable=PropertyMock, return_value=False, ): try_window_condition = await send_window_change_event( @@ -2113,16 +2104,13 @@ async def test_bug_66( assert entity.hvac_mode == HVACMode.OFF # Close the window but with sufficient time this time - with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" - ) as mock_send_event, patch( + with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" - ) as mock_heater_on, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" - ) as mock_heater_off, patch( + ) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch( "homeassistant.helpers.condition.state", return_value=True ) as mock_condition, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", + new_callable=PropertyMock, return_value=False, ): event_timestamp = now + timedelta(minutes=2)