diff --git a/custom_components/versatile_thermostat/base_thermostat.py b/custom_components/versatile_thermostat/base_thermostat.py index a0f6020..c9619ec 100644 --- a/custom_components/versatile_thermostat/base_thermostat.py +++ b/custom_components/versatile_thermostat/base_thermostat.py @@ -2005,13 +2005,17 @@ class BaseThermostat(ClimateEntity, RestoreEntity): self._device_power, ) - if self.is_over_climate: - power_consumption_max = self._device_power + # issue 407 - power_consumption_max is power we need to add. If already active we don't need to add more power + if self.is_device_active: + power_consumption_max = 0 else: - power_consumption_max = max( - self._device_power / self.nb_underlying_entities, - self._device_power * self._prop_algorithm.on_percent, - ) + if self.is_over_climate: + power_consumption_max = self._device_power + else: + power_consumption_max = max( + self._device_power / self.nb_underlying_entities, + self._device_power * self._prop_algorithm.on_percent, + ) ret = (self._current_power + power_consumption_max) >= self._current_power_max if not self._overpowering_state and ret and self._hvac_mode != HVACMode.OFF: diff --git a/tests/test_bugs.py b/tests/test_bugs.py index 9dac4df..484ccc0 100644 --- a/tests/test_bugs.py +++ b/tests/test_bugs.py @@ -1,7 +1,9 @@ # pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long """ Test the Window management """ -from unittest.mock import patch, call +import asyncio + +from unittest.mock import patch, call, PropertyMock from datetime import datetime, timedelta import logging @@ -718,3 +720,130 @@ async def test_bug_272( ), ] ) + + +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +@pytest.mark.parametrize("expected_lingering_timers", [True]) +async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state): + """Test the followin case in power management: + 1. a heater is active (heating). So the power consumption takes the heater power into account. We suppose the power consumption is near the threshold, + 2. the user switch preset let's say from Comfort to Boost, + 3. expected: no shredding should occur because the heater was already active, + 4. constated: the heater goes into shredding. + + """ + + 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, + "eco_temp": 17, + "comfort_temp": 18, + "boost_temp": 19, + CONF_USE_WINDOW_FEATURE: False, + CONF_USE_MOTION_FEATURE: False, + CONF_USE_POWER_FEATURE: True, + CONF_USE_PRESENCE_FEATURE: False, + CONF_HEATER: "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_SECURITY_DELAY_MIN: 5, + CONF_SECURITY_MIN_ON_PERCENT: 0.3, + CONF_POWER_SENSOR: "sensor.mock_power_sensor", + CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor", + CONF_DEVICE_POWER: 100, + CONF_PRESET_POWER: 12, + }, + ) + + entity: ThermostatOverSwitch = await create_thermostat( + hass, entry, "climate.theoverswitchmockname" + ) + assert entity + + tpi_algo = entity._prop_algorithm + assert tpi_algo + + tz = get_tz(hass) # pylint: disable=invalid-name + now: datetime = datetime.now(tz=tz) + + await send_temperature_change_event(entity, 16, now) + await send_ext_temperature_change_event(entity, 10, now) + + # 1. An already active heater will not switch to overpowering + with patch( + "homeassistant.core.ServiceRegistry.async_call" + ) as mock_service_call, patch( + "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", + new_callable=PropertyMock, + return_value=True, + ): + await entity.async_set_hvac_mode(HVACMode.HEAT) + await entity.async_set_preset_mode(PRESET_COMFORT) + assert entity.hvac_mode is HVACMode.HEAT + assert entity.preset_mode is PRESET_COMFORT + assert entity.overpowering_state is None + assert entity.target_temperature == 18 + # waits that the heater starts + await asyncio.sleep(0.1) + assert mock_service_call.call_count >= 1 + assert entity.is_device_active is True + + # Send power max mesurement + await send_max_power_change_event(entity, 110, datetime.now()) + # Send power mesurement (theheater is already in the power measurement) + await send_power_change_event(entity, 100, datetime.now()) + # No overpowering yet + assert await entity.check_overpowering() is False + # All configuration is complete and power is < power_max + assert entity.preset_mode is PRESET_COMFORT + assert entity.overpowering_state is False + assert entity.is_device_active is True + + # 2. An already active heater that switch preset will not switch to overpowering + with patch( + "homeassistant.core.ServiceRegistry.async_call" + ) as mock_service_call, patch( + "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", + new_callable=PropertyMock, + return_value=True, + ): + # change preset to Boost + await entity.async_set_preset_mode(PRESET_BOOST) + # waits that the heater starts + await asyncio.sleep(0.1) + + assert await entity.check_overpowering() is False + assert entity.hvac_mode is HVACMode.HEAT + assert entity.preset_mode is PRESET_BOOST + assert entity.overpowering_state is False + assert entity.target_temperature == 19 + assert mock_service_call.call_count >= 1 + + # 3. if heater is stopped (is_device_active==False), then overpowering should be started + with patch( + "homeassistant.core.ServiceRegistry.async_call" + ) as mock_service_call, patch( + "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", + new_callable=PropertyMock, + return_value=False, + ): + # change preset to Boost + await entity.async_set_preset_mode(PRESET_COMFORT) + # waits that the heater starts + await asyncio.sleep(0.1) + + assert await entity.check_overpowering() is True + assert entity.hvac_mode is HVACMode.HEAT + assert entity.preset_mode is PRESET_POWER + assert entity.overpowering_state is True diff --git a/tests/test_window.py b/tests/test_window.py index e43472f..189dcb1 100644 --- a/tests/test_window.py +++ b/tests/test_window.py @@ -168,6 +168,7 @@ async def test_window_management_time_enough( "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, ): await send_temperature_change_event(entity, 15, datetime.now()) @@ -188,6 +189,7 @@ async def test_window_management_time_enough( "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_window_change_event(entity, True, False, datetime.now()) @@ -216,6 +218,7 @@ async def test_window_management_time_enough( "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_function = await send_window_change_event( @@ -319,6 +322,7 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state): "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, ): event_timestamp = event_timestamp + timedelta(minutes=1) @@ -341,6 +345,7 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state): "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, ): event_timestamp = event_timestamp + timedelta(minutes=1) @@ -514,6 +519,7 @@ async def test_window_auto_fast_and_sensor( "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, ): event_timestamp = event_timestamp + timedelta(minutes=1) @@ -536,6 +542,7 @@ async def test_window_auto_fast_and_sensor( "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, ): event_timestamp = event_timestamp + timedelta(minutes=1) @@ -646,6 +653,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode" ) as mock_set_hvac_mode, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", + new_callable=PropertyMock, return_value=True, ): event_timestamp = event_timestamp + timedelta(minutes=1) @@ -689,7 +697,8 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode" ) as mock_set_hvac_mode, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", - return_value=False, + new_callable=PropertyMock, + return_value=True, ): # simulate the expiration of the delay await dearm_window_auto(None) @@ -783,6 +792,7 @@ async def test_window_auto_no_on_percent( "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, ): event_timestamp = event_timestamp + timedelta(minutes=1) @@ -805,6 +815,7 @@ async def test_window_auto_no_on_percent( "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, ): event_timestamp = event_timestamp + timedelta(minutes=1) @@ -890,6 +901,7 @@ async def test_window_bypass(hass: HomeAssistant, skip_hass_states_is_state): "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, ): await send_temperature_change_event(entity, 15, datetime.now()) @@ -916,6 +928,7 @@ async def test_window_bypass(hass: HomeAssistant, skip_hass_states_is_state): "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_window_change_event(entity, True, False, datetime.now()) @@ -941,6 +954,7 @@ async def test_window_bypass(hass: HomeAssistant, skip_hass_states_is_state): "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_function = await send_window_change_event( @@ -1038,6 +1052,7 @@ async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state "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, ): event_timestamp = event_timestamp + timedelta(minutes=1) @@ -1062,6 +1077,7 @@ async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state "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, ): event_timestamp = event_timestamp + timedelta(minutes=1) @@ -1145,6 +1161,7 @@ async def test_window_bypass_reactivate(hass: HomeAssistant, skip_hass_states_is "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, ): await send_temperature_change_event(entity, 15, datetime.now()) @@ -1165,6 +1182,7 @@ async def test_window_bypass_reactivate(hass: HomeAssistant, skip_hass_states_is "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_window_change_event(entity, True, False, datetime.now()) @@ -1191,6 +1209,7 @@ async def test_window_bypass_reactivate(hass: HomeAssistant, skip_hass_states_is "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, ): await entity.service_set_window_bypass_state(True) @@ -1588,6 +1607,7 @@ async def test_window_action_eco_temp(hass: HomeAssistant, skip_hass_states_is_s "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, ): event_timestamp = event_timestamp + timedelta(minutes=1) @@ -1609,6 +1629,7 @@ async def test_window_action_eco_temp(hass: HomeAssistant, skip_hass_states_is_s "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, ): event_timestamp = event_timestamp + timedelta(minutes=1) @@ -1783,6 +1804,7 @@ async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is "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, ): event_timestamp = event_timestamp + timedelta(minutes=1) @@ -1804,6 +1826,7 @@ async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is "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, ): event_timestamp = event_timestamp + timedelta(minutes=1)