diff --git a/custom_components/versatile_thermostat/thermostat_climate.py b/custom_components/versatile_thermostat/thermostat_climate.py index 67a9d6f..5612f19 100644 --- a/custom_components/versatile_thermostat/thermostat_climate.py +++ b/custom_components/versatile_thermostat/thermostat_climate.py @@ -911,21 +911,21 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]): _LOGGER.info( "%s - Turning OFF the Vtherm due to auto-start-stop conditions", self ) + await self.async_turn_off() + # Send an event self.send_event( - EventType.AUTO_START_STOP_EVENT, - { + event_type=EventType.AUTO_START_STOP_EVENT, + data={ "type": "stop", - "cause": "Auto start conditions reached", + "cause": "Auto stop conditions reached", "hvac_mode": self.hvac_mode, "saved_hvac_mode": self._saved_hvac_mode, - "regulated_target_temp": self.regulated_target_temp, "target_temperature": self.target_temperature, "current_temperature": self.current_temperature, "temperature_slope": self._window_auto_algo.last_slope, }, ) - await self.async_turn_off() # Stop here return ret @@ -934,7 +934,20 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]): "%s - Turning ON the Vtherm due to auto-start-stop conditions", self ) await self.async_turn_on() + # Send an event + self.send_event( + event_type=EventType.AUTO_START_STOP_EVENT, + data={ + "type": "start", + "cause": "Auto start conditions reached", + "hvac_mode": self.hvac_mode, + "saved_hvac_mode": self._saved_hvac_mode, + "target_temperature": self.target_temperature, + "current_temperature": self.current_temperature, + "temperature_slope": self._window_auto_algo.last_slope, + }, + ) # Continue the normal async_control_heating diff --git a/tests/test_auto_start_stop.py b/tests/test_auto_start_stop.py index f8d670b..9324f70 100644 --- a/tests/test_auto_start_stop.py +++ b/tests/test_auto_start_stop.py @@ -270,7 +270,7 @@ async def test_auto_start_stop_none_vtherm( @pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True]) -async def test_auto_start_stop_medium_vtherm( +async def test_auto_start_stop_medium_heat_vtherm( hass: HomeAssistant, skip_hass_states_is_state ): """Test than auto-start/stop works with a real over_climate VTherm in MEDIUM level""" @@ -370,47 +370,356 @@ async def test_auto_start_stop_medium_vtherm( # 3. Set current temperature to 19 5 min later now = now + timedelta(minutes=5) with patch( - "custom_components.versatile_thermostat.binary_sensor.send_vtherm_event" + "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" ) as mock_send_event: + vtherm._set_now(now) await send_temperature_change_event(vtherm, 19, now, True) await hass.async_block_till_done() # VTherm should still be heating assert vtherm.hvac_mode == HVACMode.HEAT assert mock_send_event.call_count == 0 + assert ( + vtherm._auto_start_stop_algo.accumulated_error == 0 + ) # target = current = 19 # 4. Set current temperature to 20 5 min later now = now + timedelta(minutes=5) with patch( - "custom_components.versatile_thermostat.binary_sensor.send_vtherm_event" + "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" ) as mock_send_event: + vtherm._set_now(now) await send_temperature_change_event(vtherm, 20, now, True) await hass.async_block_till_done() # VTherm should still be heating assert vtherm.hvac_mode == HVACMode.HEAT assert mock_send_event.call_count == 0 + # accumulated_error = target - current = -1 x 5 min / 2 + assert vtherm._auto_start_stop_algo.accumulated_error == -2.5 - # 5. Set current temperature to 21 5 min later + # 5. Set current temperature to 21 5 min later -> should turn off + now = now + timedelta(minutes=5) with patch( - "custom_components.versatile_thermostat.binary_sensor.send_vtherm_event" + "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" ) as mock_send_event: - now = now + timedelta(minutes=5) + vtherm._set_now(now) await send_temperature_change_event(vtherm, 21, now, True) await hass.async_block_till_done() # VTherm should have been stopped assert vtherm.hvac_mode == HVACMode.OFF + # accumulated_error = -2.5 + target - current = -2 x 5 min / 2 capped to 5 + assert vtherm._auto_start_stop_algo.accumulated_error == -5 + # a message should have been sent - assert mock_send_event.call_count == 1 + assert mock_send_event.call_count >= 1 mock_send_event.assert_has_calls( [ - call.send_vtherm_event( - hass=hass, + call( event_type=EventType.AUTO_START_STOP_EVENT, - entity=vtherm.entity_id, - data={}, + data={ + "type": "stop", + "cause": "Auto stop conditions reached", + "hvac_mode": HVACMode.OFF, + "saved_hvac_mode": HVACMode.HEAT, + "target_temperature": 19.0, + "current_temperature": 21.0, + "temperature_slope": 10.03, + }, + ) + ] + ) + + mock_send_event.assert_has_calls( + [ + call( + EventType.HVAC_MODE_EVENT, + { + "hvac_mode": HVACMode.OFF, + }, + ) + ] + ) + + # 6. Set temperature to small over the target, so that it will stay to OFF + now = now + timedelta(minutes=10) + with patch( + "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" + ) as mock_send_event: + vtherm._set_now(now) + await send_temperature_change_event(vtherm, 19.5, now, True) + await hass.async_block_till_done() + + # accumulated_error = .... capped to -5 + assert vtherm._auto_start_stop_algo.accumulated_error == -5 + + # VTherm should stay stopped cause slope is too low to allow the turn to On + assert vtherm.hvac_mode == HVACMode.OFF + + # 7. Set temperature to over the target, so that it will turn to heat + now = now + timedelta(minutes=20) + with patch( + "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" + ) as mock_send_event: + vtherm._set_now(now) + await send_temperature_change_event(vtherm, 18, now, True) + await hass.async_block_till_done() + + # accumulated_error = -5/2 + target - current = 1 x 20 min / 2 capped to 5 + assert vtherm._auto_start_stop_algo.accumulated_error == 5 + + # VTherm should have been stopped + assert vtherm.hvac_mode == HVACMode.HEAT + # a message should have been sent + assert mock_send_event.call_count >= 1 + mock_send_event.assert_has_calls( + [ + call( + event_type=EventType.AUTO_START_STOP_EVENT, + data={ + "type": "start", + "cause": "Auto start conditions reached", + "hvac_mode": HVACMode.HEAT, + "saved_hvac_mode": HVACMode.HEAT, # saved don't change + "target_temperature": 19.0, + "current_temperature": 18.0, + "temperature_slope": -2.06, + }, + ) + ] + ) + + mock_send_event.assert_has_calls( + [ + call( + EventType.HVAC_MODE_EVENT, + { + "hvac_mode": HVACMode.HEAT, + }, + ) + ] + ) + + +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +@pytest.mark.parametrize("expected_lingering_timers", [True]) +async def test_auto_start_stop_fast_ac_vtherm( + hass: HomeAssistant, skip_hass_states_is_state +): + """Test than auto-start/stop works with a real over_climate VTherm in FAST level and AC mode""" + + # vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass) + + # The temperatures to set + temps = { + "frost": 7.0, + "eco": 17.0, + "comfort": 19.0, + "boost": 21.0, + "eco_ac": 27.0, + "comfort_ac": 25.0, + "boost_ac": 23.0, + "frost_away": 7.1, + "eco_away": 17.1, + "comfort_away": 19.1, + "boost_away": 21.1, + "eco_ac_away": 27.1, + "comfort_ac_away": 25.1, + "boost_ac_away": 23.1, + } + + config_entry = MockConfigEntry( + domain=DOMAIN, + title="TheOverClimateMockName", + unique_id="overClimateUniqueId", + data={ + CONF_NAME: "overClimate", + CONF_TEMP_SENSOR: "sensor.mock_temp_sensor", + CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE, + 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: False, + CONF_USE_PRESENCE_FEATURE: True, + CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor", + CONF_CLIMATE: "climate.mock_climate", + CONF_MINIMAL_ACTIVATION_DELAY: 30, + CONF_SECURITY_DELAY_MIN: 5, + CONF_SECURITY_MIN_ON_PERCENT: 0.3, + CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO, + CONF_AC_MODE: True, + CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_FAST, + }, + ) + + fake_underlying_climate = MockClimate( + hass=hass, + unique_id="mock_climate", + name="mock_climate", + hvac_modes=[HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT], + ) + + with patch( + "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", + return_value=fake_underlying_climate, + ): + vtherm: ThermostatOverClimate = await create_thermostat( + hass, config_entry, "climate.overclimate" + ) + + assert vtherm is not None + + # Initialize all temps + await set_all_climate_preset_temp(hass, vtherm, temps, "overclimate") + + # Check correct initialization of auto_start_stop attributes + assert ( + vtherm._attr_extra_state_attributes["auto_start_stop_level"] + == AUTO_START_STOP_LEVEL_FAST + ) + + assert vtherm._attr_extra_state_attributes["auto_start_stop_dtmin"] == 7 + + # 1. Vtherm auto-start/stop should be in MEDIUM mode + assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST + + tz = get_tz(hass) # pylint: disable=invalid-name + now: datetime = datetime.now(tz=tz) + + # 2. Set mode to Heat and preset to Comfort + await send_presence_change_event(vtherm, True, False, now) + await send_temperature_change_event(vtherm, 27, now, True) + await vtherm.async_set_hvac_mode(HVACMode.COOL) + await vtherm.async_set_preset_mode(PRESET_COMFORT) + await hass.async_block_till_done() + + assert vtherm.target_temperature == 25.0 + # VTherm should be heating + assert vtherm.hvac_mode == HVACMode.COOL + + # 3. Set current temperature to 19 5 min later + now = now + timedelta(minutes=5) + with patch( + "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" + ) as mock_send_event: + vtherm._set_now(now) + await send_temperature_change_event(vtherm, 25, now, True) + await hass.async_block_till_done() + + # VTherm should still be heating + assert vtherm.hvac_mode == HVACMode.COOL + assert mock_send_event.call_count == 0 + assert ( + vtherm._auto_start_stop_algo.accumulated_error == 0 # target = current = 25 + ) + + # 4. Set current temperature to 23 5 min later -> should turn off + now = now + timedelta(minutes=5) + with patch( + "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" + ) as mock_send_event: + vtherm._set_now(now) + await send_temperature_change_event(vtherm, 23, now, True) + await hass.async_block_till_done() + + # VTherm should have been stopped + assert vtherm.hvac_mode == HVACMode.OFF + + # accumulated_error = target - current = 2 x 5 min / 2 capped to 2 + assert vtherm._auto_start_stop_algo.accumulated_error == 2 + + # a message should have been sent + assert mock_send_event.call_count >= 1 + mock_send_event.assert_has_calls( + [ + call( + event_type=EventType.AUTO_START_STOP_EVENT, + data={ + "type": "stop", + "cause": "Auto stop conditions reached", + "hvac_mode": HVACMode.OFF, + "saved_hvac_mode": HVACMode.COOL, + "target_temperature": 25.0, + "current_temperature": 23.0, + "temperature_slope": -16.8, + }, + ) + ] + ) + + mock_send_event.assert_has_calls( + [ + call( + EventType.HVAC_MODE_EVENT, + { + "hvac_mode": HVACMode.OFF, + }, + ) + ] + ) + + # 5. Set temperature to over the target, but slope is too low -> no change + now = now + timedelta(minutes=20) + with patch( + "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" + ) as mock_send_event: + vtherm._set_now(now) + await send_temperature_change_event(vtherm, 26, now, True) + await hass.async_block_till_done() + + # accumulated_error = 2/2 + target - current = -1 x 20 min / 2 capped to 2 + assert vtherm._auto_start_stop_algo.accumulated_error == -2 + + # VTherm should have been stopped + assert vtherm.hvac_mode == HVACMode.OFF + # a message should have been sent + assert mock_send_event.call_count == 0 + + # 6. Set temperature to over the target, so that it will turn to COOL + now = now + timedelta(minutes=5) + with patch( + "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" + ) as mock_send_event: + vtherm._set_now(now) + await send_temperature_change_event(vtherm, 26.5, now, True) + await hass.async_block_till_done() + + # accumulated_error = 2/2 + target - current = -1 x 20 min / 2 capped to 2 + assert vtherm._auto_start_stop_algo.accumulated_error == -2 + + # VTherm should have been stopped + assert vtherm.hvac_mode == HVACMode.COOL + # a message should have been sent + assert mock_send_event.call_count >= 1 + mock_send_event.assert_has_calls( + [ + call( + event_type=EventType.AUTO_START_STOP_EVENT, + data={ + "type": "start", + "cause": "Auto start conditions reached", + "hvac_mode": HVACMode.COOL, + "saved_hvac_mode": HVACMode.COOL, # saved don't change + "target_temperature": 25.0, + "current_temperature": 26.5, + "temperature_slope": 5.74, + }, + ) + ] + ) + + mock_send_event.assert_has_calls( + [ + call( + EventType.HVAC_MODE_EVENT, + { + "hvac_mode": HVACMode.COOL, + }, ) ] )