"""The tests for the climate platform of evohome. All evohome systems have controllers and at least one zone. """ from datetime import timedelta from unittest.mock import patch from evohomeasync2 import exceptions as evo_exc from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.climate import ( ATTR_HVAC_MODE, ATTR_PRESET_MODE, DOMAIN as CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, SERVICE_SET_PRESET_MODE, SERVICE_SET_TEMPERATURE, HVACMode, ) from homeassistant.components.evohome.climate import PRESET_RESET from homeassistant.components.evohome.const import DOMAIN, RESET_BREAKS_IN_HA_VERSION from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_OFF, SERVICE_TURN_ON, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import issue_registry as ir from .conftest import setup_evohome from .const import TEST_INSTALLS from tests.common import async_fire_time_changed @pytest.mark.parametrize("install", [*TEST_INSTALLS, "botched"]) async def test_setup_platform( hass: HomeAssistant, config: dict[str, str], install: str, snapshot: SnapshotAssertion, freezer: FrozenDateTimeFactory, ) -> None: """Test entities and their states after setup of evohome.""" # Cannot use the evohome fixture here, as need to set dtm first # - some extended state attrs are relative the current time freezer.move_to("2024-07-10T12:00:00Z") async for _ in setup_evohome(hass, config, install=install): pass climate_states = hass.states.async_all(CLIMATE_DOMAIN) assert climate_states for x in climate_states: assert x == snapshot(name=f"{x.entity_id}-state") @pytest.mark.parametrize("install", ["default"]) async def test_entities_update_over_time( hass: HomeAssistant, config: dict[str, str], install: str, snapshot: SnapshotAssertion, freezer: FrozenDateTimeFactory, ) -> None: """Test extended attributes update as time passes. Verifies that time-dependent state attrs (e.g. schedules) vary as time advances. """ # Cannot use the evohome fixture here, as need to set dtm first # - some extended state attrs are relative the current time freezer.move_to("2024-07-10T05:30:00Z") # stay inside this context to have the mocked RESTful API async for _ in setup_evohome(hass, config, install=install): for x in hass.states.async_all(CLIMATE_DOMAIN): assert x == snapshot(name=f"{x.entity_id}-state-initial") freezer.tick(timedelta(hours=12)) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) for x in hass.states.async_all(CLIMATE_DOMAIN): assert x == snapshot(name=f"{x.entity_id}-state-updated") @pytest.mark.parametrize("install", TEST_INSTALLS) async def test_ctl_set_hvac_mode( hass: HomeAssistant, ctl_id: str, snapshot: SnapshotAssertion, ) -> None: """Test SERVICE_SET_HVAC_MODE of an evohome controller.""" results = [] # SERVICE_SET_HVAC_MODE: HVACMode.OFF with patch("evohomeasync2.control_system.ControlSystem.set_mode") as mock_fcn: await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, { ATTR_ENTITY_ID: ctl_id, ATTR_HVAC_MODE: HVACMode.OFF, }, blocking=True, ) try: mock_fcn.assert_awaited_once_with("HeatingOff", until=None) except AssertionError: mock_fcn.assert_awaited_once_with("Off", until=None) results.append(mock_fcn.await_args.args) # type: ignore[union-attr] # SERVICE_SET_HVAC_MODE: HVACMode.HEAT with patch("evohomeasync2.control_system.ControlSystem.set_mode") as mock_fcn: await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, { ATTR_ENTITY_ID: ctl_id, ATTR_HVAC_MODE: HVACMode.HEAT, }, blocking=True, ) try: mock_fcn.assert_awaited_once_with("Auto", until=None) except AssertionError: mock_fcn.assert_awaited_once_with("Heat", until=None) results.append(mock_fcn.await_args.args) # type: ignore[union-attr] assert results == snapshot @pytest.mark.parametrize("install", TEST_INSTALLS) async def test_ctl_set_temperature( hass: HomeAssistant, ctl_id: str, ) -> None: """Test SERVICE_SET_TEMPERATURE of an evohome controller.""" # Entity climate.xxx does not support this service with pytest.raises(HomeAssistantError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: ctl_id, ATTR_TEMPERATURE: 19.1, }, blocking=True, ) @pytest.mark.parametrize("install", TEST_INSTALLS) async def test_ctl_turn_off( hass: HomeAssistant, ctl_id: str, snapshot: SnapshotAssertion, ) -> None: """Test SERVICE_TURN_OFF of an evohome controller.""" results = [] # SERVICE_TURN_OFF with patch("evohomeasync2.control_system.ControlSystem.set_mode") as mock_fcn: await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_TURN_OFF, { ATTR_ENTITY_ID: ctl_id, }, blocking=True, ) try: mock_fcn.assert_awaited_once_with("HeatingOff", until=None) except AssertionError: mock_fcn.assert_awaited_once_with("Off", until=None) results.append(mock_fcn.await_args.args) # type: ignore[union-attr] assert results == snapshot @pytest.mark.parametrize("install", ["default"]) async def test_ctl_invalid_system_mode( hass: HomeAssistant, ctl_id: str, ) -> None: """Test translated exception when the requested system mode is invalid.""" with ( patch( "evohomeasync2.control_system.ControlSystem.set_mode", side_effect=evo_exc.InvalidSystemModeError("Unsupported mode: xxx"), ), pytest.raises(ServiceValidationError) as exc_info, ): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_TURN_OFF, { ATTR_ENTITY_ID: ctl_id, }, blocking=True, ) assert exc_info.value.translation_key == "invalid_system_mode" @pytest.mark.parametrize("install", TEST_INSTALLS) async def test_ctl_turn_on( hass: HomeAssistant, ctl_id: str, snapshot: SnapshotAssertion, ) -> None: """Test SERVICE_TURN_ON of an evohome controller.""" results = [] # SERVICE_TURN_ON with patch("evohomeasync2.control_system.ControlSystem.set_mode") as mock_fcn: await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_TURN_ON, { ATTR_ENTITY_ID: ctl_id, }, blocking=True, ) try: mock_fcn.assert_awaited_once_with("Auto", until=None) except AssertionError: mock_fcn.assert_awaited_once_with("Heat", until=None) results.append(mock_fcn.await_args.args) # type: ignore[union-attr] assert results == snapshot @pytest.mark.parametrize("install", ["default"]) async def test_ctl_preset_reset_deprecated( hass: HomeAssistant, ctl_id: str, issue_registry: ir.IssueRegistry, ) -> None: """Test deprecated warning when using the Reset preset on controllers.""" with patch("evohomeasync2.control_system.ControlSystem.set_mode") as mock_fcn: await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, { ATTR_ENTITY_ID: ctl_id, ATTR_PRESET_MODE: PRESET_RESET, }, blocking=True, ) mock_fcn.assert_awaited_once_with("AutoWithReset", until=None) issue = issue_registry.async_get_issue(DOMAIN, "deprecated_preset_reset") assert issue is not None assert issue.translation_key == "deprecated_preset_reset" assert issue.translation_placeholders == { "breaks_in_ha_version": RESET_BREAKS_IN_HA_VERSION, } @pytest.mark.parametrize("install", TEST_INSTALLS) async def test_zone_set_hvac_mode( hass: HomeAssistant, zone_id: str, snapshot: SnapshotAssertion, ) -> None: """Test SERVICE_SET_HVAC_MODE of an evohome heating zone.""" results = [] # SERVICE_SET_HVAC_MODE: HVACMode.HEAT with patch("evohomeasync2.zone.Zone.reset") as mock_fcn: await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, { ATTR_ENTITY_ID: zone_id, ATTR_HVAC_MODE: HVACMode.HEAT, }, blocking=True, ) mock_fcn.assert_awaited_once_with() # SERVICE_SET_HVAC_MODE: HVACMode.OFF with patch("evohomeasync2.zone.Zone.set_temperature") as mock_fcn: await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, { ATTR_ENTITY_ID: zone_id, ATTR_HVAC_MODE: HVACMode.OFF, }, blocking=True, ) mock_fcn.assert_awaited_once() assert mock_fcn.await_args is not None # mypy hint assert mock_fcn.await_args.args != () # minimum target temp assert mock_fcn.await_args.kwargs == {"until": None} results.append(mock_fcn.await_args.args) assert results == snapshot @pytest.mark.parametrize("install", TEST_INSTALLS) async def test_zone_set_preset_mode( hass: HomeAssistant, zone_id: str, freezer: FrozenDateTimeFactory, snapshot: SnapshotAssertion, ) -> None: """Test SERVICE_SET_PRESET_MODE of an evohome heating zone.""" freezer.move_to("2024-07-10T12:00:00Z") results = [] # SERVICE_SET_PRESET_MODE: none with patch("evohomeasync2.zone.Zone.reset") as mock_fcn: await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, { ATTR_ENTITY_ID: zone_id, ATTR_PRESET_MODE: "none", }, blocking=True, ) mock_fcn.assert_awaited_once_with() # SERVICE_SET_PRESET_MODE: permanent with patch("evohomeasync2.zone.Zone.set_temperature") as mock_fcn: await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, { ATTR_ENTITY_ID: zone_id, ATTR_PRESET_MODE: "permanent", }, blocking=True, ) mock_fcn.assert_awaited_once() assert mock_fcn.await_args is not None # mypy hint assert mock_fcn.await_args.args != () # current target temp assert mock_fcn.await_args.kwargs == {"until": None} results.append(mock_fcn.await_args.args) # SERVICE_SET_PRESET_MODE: temporary with patch("evohomeasync2.zone.Zone.set_temperature") as mock_fcn: await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, { ATTR_ENTITY_ID: zone_id, ATTR_PRESET_MODE: "temporary", }, blocking=True, ) mock_fcn.assert_awaited_once() assert mock_fcn.await_args is not None # mypy hint assert mock_fcn.await_args.args != () # current target temp assert mock_fcn.await_args.kwargs != {} # next setpoint dtm results.append(mock_fcn.await_args.args) results.append(mock_fcn.await_args.kwargs) assert results == snapshot @pytest.mark.parametrize("install", TEST_INSTALLS) async def test_zone_set_temperature( hass: HomeAssistant, zone_id: str, snapshot: SnapshotAssertion, ) -> None: """Test SERVICE_SET_TEMPERATURE of an evohome heating zone.""" results = [] # SERVICE_SET_TEMPERATURE: temperature with patch("evohomeasync2.zone.Zone.set_temperature") as mock_fcn: await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: zone_id, ATTR_TEMPERATURE: 19.1, }, blocking=True, ) mock_fcn.assert_awaited_once() assert mock_fcn.await_args is not None # mypy hint assert mock_fcn.await_args.args == (19.1,) assert mock_fcn.await_args.kwargs != {} # next setpoint dtm results.append(mock_fcn.await_args.kwargs) assert results == snapshot @pytest.mark.parametrize("install", TEST_INSTALLS) async def test_zone_turn_off( hass: HomeAssistant, zone_id: str, snapshot: SnapshotAssertion, ) -> None: """Test SERVICE_TURN_OFF of an evohome heating zone.""" results = [] # SERVICE_TURN_OFF with patch("evohomeasync2.zone.Zone.set_temperature") as mock_fcn: await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_TURN_OFF, { ATTR_ENTITY_ID: zone_id, }, blocking=True, ) mock_fcn.assert_awaited_once() assert mock_fcn.await_args is not None # mypy hint assert mock_fcn.await_args.args != () # minimum target temp assert mock_fcn.await_args.kwargs == {"until": None} results.append(mock_fcn.await_args.args) assert results == snapshot @pytest.mark.parametrize("install", TEST_INSTALLS) async def test_zone_turn_on( hass: HomeAssistant, zone_id: str, ) -> None: """Test SERVICE_TURN_ON of an evohome heating zone.""" # SERVICE_TURN_ON with patch("evohomeasync2.zone.Zone.reset") as mock_fcn: await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_TURN_ON, { ATTR_ENTITY_ID: zone_id, }, blocking=True, ) mock_fcn.assert_awaited_once_with()