1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-19 06:50:15 +01:00
Files

585 lines
18 KiB
Python

"""The tests for the native services of Evohome."""
from datetime import UTC, datetime
from typing import Any
from unittest.mock import patch
from freezegun.api import FrozenDateTimeFactory
import pytest
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
from homeassistant.components.evohome.climate import EvoZone
from homeassistant.components.evohome.const import (
ATTR_DURATION,
ATTR_PERIOD,
ATTR_SETPOINT,
DOMAIN,
RESET_BREAKS_IN_HA_VERSION,
SERVICE_BREAKS_IN_HA_VERSION,
EvoService,
)
from homeassistant.components.evohome.water_heater import EvoDHW
from homeassistant.components.water_heater import DOMAIN as WATER_HEATER_DOMAIN
from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, ATTR_STATE
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.entity_platform import DATA_DOMAIN_PLATFORM_ENTITIES
from .const import TEST_INSTALLS
@pytest.mark.parametrize("install", ["default"])
@pytest.mark.usefixtures("evohome")
async def test_refresh_system(hass: HomeAssistant) -> None:
"""Test Evohome's refresh_system service (for all temperature control systems)."""
# EvoService.REFRESH_SYSTEM
with patch("evohomeasync2.location.Location.update") as mock_fcn:
await hass.services.async_call(
DOMAIN,
EvoService.REFRESH_SYSTEM,
{},
blocking=True,
)
mock_fcn.assert_awaited_once_with()
@pytest.mark.parametrize("install", TEST_INSTALLS)
@pytest.mark.usefixtures("evohome")
async def test_reset_system(
hass: HomeAssistant,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test untargeted reset_system service calls."""
# EvoService.RESET_SYSTEM
with patch("evohomeasync2.control_system.ControlSystem.reset") as mock_fcn:
await hass.services.async_call(
DOMAIN,
EvoService.RESET_SYSTEM,
{},
blocking=True,
)
mock_fcn.assert_awaited_once_with()
issue = issue_registry.async_get_issue(DOMAIN, "deprecated_reset_system_service")
assert issue is not None
assert issue.translation_key == "deprecated_reset_system_service"
assert issue.translation_placeholders == {
"breaks_in_ha_version": RESET_BREAKS_IN_HA_VERSION,
}
@pytest.mark.parametrize("install", ["default"])
@pytest.mark.usefixtures("evohome")
async def test_set_system_mode_deprecated(
hass: HomeAssistant,
issue_registry: ir.IssueRegistry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test untargeted set_system_mode service calls.
These untargeted service calls remain supported during the deprecation window but
should cause a Repair issue.
"""
# EvoService.SET_SYSTEM_MODE: Auto
with patch("evohomeasync2.control_system.ControlSystem.set_mode") as mock_fcn:
await hass.services.async_call(
DOMAIN,
EvoService.SET_SYSTEM_MODE,
{
ATTR_MODE: "Auto",
},
blocking=True,
)
mock_fcn.assert_awaited_once_with("Auto", until=None)
issue = issue_registry.async_get_issue(DOMAIN, "deprecated_set_system_mode_service")
assert issue
assert issue.translation_key == "deprecated_controller_service"
assert issue.translation_placeholders == {
"breaks_in_ha_version": SERVICE_BREAKS_IN_HA_VERSION,
"service": EvoService.SET_SYSTEM_MODE,
}
freezer.move_to("2024-07-10T12:00:00+00:00")
# EvoService.SET_SYSTEM_MODE: AutoWithEco, hours=12
with patch("evohomeasync2.control_system.ControlSystem.set_mode") as mock_fcn:
await hass.services.async_call(
DOMAIN,
EvoService.SET_SYSTEM_MODE,
{
ATTR_MODE: "AutoWithEco",
ATTR_DURATION: {"hours": 12},
},
blocking=True,
)
mock_fcn.assert_awaited_once_with(
"AutoWithEco", until=datetime(2024, 7, 11, 0, 0, tzinfo=UTC)
)
# EvoService.SET_SYSTEM_MODE: Away, days=7
with patch("evohomeasync2.control_system.ControlSystem.set_mode") as mock_fcn:
await hass.services.async_call(
DOMAIN,
EvoService.SET_SYSTEM_MODE,
{
ATTR_MODE: "Away",
ATTR_PERIOD: {"days": 7},
},
blocking=True,
)
mock_fcn.assert_awaited_once_with(
"Away", until=datetime(2024, 7, 16, 23, 0, tzinfo=UTC)
)
@pytest.mark.parametrize("install", ["default"])
async def test_set_system_mode(
hass: HomeAssistant,
ctl_id: str,
issue_registry: ir.IssueRegistry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test entity-targeted set_system_mode service calls."""
freezer.move_to("2024-07-10T12:00:00+00:00")
with patch("evohomeasync2.control_system.ControlSystem.set_mode") as mock_fcn:
await hass.services.async_call(
DOMAIN,
EvoService.SET_SYSTEM_MODE,
{
ATTR_MODE: "Away",
ATTR_PERIOD: {"days": 7},
},
target={ATTR_ENTITY_ID: ctl_id},
blocking=True,
)
mock_fcn.assert_awaited_once_with(
"Away", until=datetime(2024, 7, 16, 23, 0, tzinfo=UTC)
)
# can remove, once the domain-level service is removed
with patch("evohomeasync2.control_system.ControlSystem.set_mode") as mock_fcn:
await hass.services.async_call(
DOMAIN,
EvoService.SET_SYSTEM_MODE,
{
ATTR_ENTITY_ID: ctl_id,
ATTR_MODE: "Away",
ATTR_PERIOD: {"days": 7},
},
blocking=True,
)
mock_fcn.assert_awaited_once_with(
"Away", until=datetime(2024, 7, 16, 23, 0, tzinfo=UTC)
)
issue = issue_registry.async_get_issue(DOMAIN, "deprecated_set_system_mode_service")
assert issue is None
@pytest.mark.parametrize("install", ["default"])
async def test_clear_zone_override(
hass: HomeAssistant,
zone_id: str,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test Evohome's clear_zone_override service (for a heating zone)."""
# EvoZoneMode.FOLLOW_SCHEDULE
with patch("evohomeasync2.zone.Zone.reset") as mock_fcn:
await hass.services.async_call(
DOMAIN,
EvoService.CLEAR_ZONE_OVERRIDE,
{},
target={ATTR_ENTITY_ID: zone_id},
blocking=True,
)
mock_fcn.assert_awaited_once_with()
issue = issue_registry.async_get_issue(
DOMAIN, "deprecated_clear_zone_override_service"
)
assert issue is not None
assert issue.translation_key == "deprecated_clear_zone_override_service"
assert issue.translation_placeholders == {
"breaks_in_ha_version": RESET_BREAKS_IN_HA_VERSION,
}
@pytest.mark.parametrize("install", ["default"])
async def test_clear_zone_override_legacy(
hass: HomeAssistant,
zone_id: str,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test Evohome's clear_zone_override service with the legacy entity_id."""
# EvoZoneMode.FOLLOW_SCHEDULE
with patch("evohomeasync2.zone.Zone.reset") as mock_fcn:
await hass.services.async_call(
DOMAIN,
EvoService.CLEAR_ZONE_OVERRIDE,
{
ATTR_ENTITY_ID: zone_id,
},
blocking=True,
)
mock_fcn.assert_awaited_once_with()
issue = issue_registry.async_get_issue(
DOMAIN, "deprecated_clear_zone_override_service"
)
assert issue is not None
assert issue.translation_key == "deprecated_clear_zone_override_service"
assert issue.translation_placeholders == {
"breaks_in_ha_version": RESET_BREAKS_IN_HA_VERSION,
}
@pytest.mark.parametrize("install", ["default"])
async def test_set_zone_override(
hass: HomeAssistant,
zone_id: str,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test Evohome's set_zone_override service (for a heating zone)."""
freezer.move_to("2024-07-10T12:00:00+00:00")
# EvoZoneMode.PERMANENT_OVERRIDE
with patch("evohomeasync2.zone.Zone.set_temperature") as mock_fcn:
await hass.services.async_call(
DOMAIN,
EvoService.SET_ZONE_OVERRIDE,
{
ATTR_SETPOINT: 19.5,
},
target={ATTR_ENTITY_ID: zone_id},
blocking=True,
)
mock_fcn.assert_awaited_once_with(19.5, until=None)
# EvoZoneMode.TEMPORARY_OVERRIDE
with patch("evohomeasync2.zone.Zone.set_temperature") as mock_fcn:
await hass.services.async_call(
DOMAIN,
EvoService.SET_ZONE_OVERRIDE,
{
ATTR_SETPOINT: 19.5,
ATTR_DURATION: {"minutes": 135},
},
target={ATTR_ENTITY_ID: zone_id},
blocking=True,
)
mock_fcn.assert_awaited_once_with(
19.5, until=datetime(2024, 7, 10, 14, 15, tzinfo=UTC)
)
@pytest.mark.parametrize("install", ["default"])
async def test_set_zone_override_advance(
hass: HomeAssistant,
zone_id: str,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test Evohome's set_zone_override service with duration=0.
The override is temporary until the next schedule change.
"""
freezer.move_to("2024-05-10T12:15:00+00:00")
expected_until = datetime(2024, 5, 10, 21, 10, tzinfo=UTC)
# Simulate the schedule not yet having been fetched (e.g. HOMEASSISTANT_START)
entities = hass.data[DATA_DOMAIN_PLATFORM_ENTITIES].get(
(CLIMATE_DOMAIN, DOMAIN), {}
)
zone_entity: EvoZone = entities[zone_id] # type: ignore[assignment]
zone_entity._schedule = None
zone_entity._setpoints = {}
# EvoZoneMode.TEMPORARY_OVERRIDE with duration 0 (i.e. until next schedule change)
with patch("evohomeasync2.zone.Zone.set_temperature") as mock_fcn:
await hass.services.async_call(
DOMAIN,
EvoService.SET_ZONE_OVERRIDE,
{
ATTR_SETPOINT: 19.5,
ATTR_DURATION: {"minutes": 0},
},
target={ATTR_ENTITY_ID: zone_id},
blocking=True,
)
mock_fcn.assert_awaited_once_with(19.5, until=expected_until)
assert zone_entity.setpoints["next_sp_from"] == expected_until
@pytest.mark.parametrize("install", ["default"])
async def test_set_zone_override_legacy(
hass: HomeAssistant,
zone_id: str,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test Evohome's set_zone_override service with the legacy entity_id."""
freezer.move_to("2024-07-10T12:00:00+00:00")
# EvoZoneMode.PERMANENT_OVERRIDE
with patch("evohomeasync2.zone.Zone.set_temperature") as mock_fcn:
await hass.services.async_call(
DOMAIN,
EvoService.SET_ZONE_OVERRIDE,
{
ATTR_ENTITY_ID: zone_id,
ATTR_SETPOINT: 19.5,
},
blocking=True,
)
mock_fcn.assert_awaited_once_with(19.5, until=None)
# EvoZoneMode.TEMPORARY_OVERRIDE
with patch("evohomeasync2.zone.Zone.set_temperature") as mock_fcn:
await hass.services.async_call(
DOMAIN,
EvoService.SET_ZONE_OVERRIDE,
{
ATTR_ENTITY_ID: zone_id,
ATTR_SETPOINT: 19.5,
ATTR_DURATION: {"minutes": 135},
},
blocking=True,
)
mock_fcn.assert_awaited_once_with(
19.5, until=datetime(2024, 7, 10, 14, 15, tzinfo=UTC)
)
@pytest.mark.parametrize("install", ["default"])
@pytest.mark.parametrize(
("service", "service_data"),
[
(EvoService.CLEAR_ZONE_OVERRIDE, {}),
(EvoService.SET_ZONE_OVERRIDE, {ATTR_SETPOINT: 19.5}),
],
)
async def test_zone_services_with_ctl_id(
hass: HomeAssistant,
ctl_id: str,
service: EvoService,
service_data: dict[str, Any],
) -> None:
"""Test calling zone-only service calls with a non-zone entity_id fails."""
with pytest.raises(ServiceValidationError) as exc_info:
await hass.services.async_call(
DOMAIN,
service,
service_data,
target={ATTR_ENTITY_ID: ctl_id},
blocking=True,
)
assert exc_info.value.translation_key == "zone_only_service"
assert exc_info.value.translation_placeholders == {"service": service}
@pytest.mark.parametrize("install", ["default"])
async def test_controller_services_with_zone_id(
hass: HomeAssistant,
zone_id: str,
) -> None:
"""Test calling controller-only service calls with a zone entity_id fails."""
with pytest.raises(ServiceValidationError) as exc_info:
await hass.services.async_call(
DOMAIN,
EvoService.SET_SYSTEM_MODE,
{
ATTR_MODE: "Auto",
ATTR_ENTITY_ID: zone_id,
},
blocking=True,
)
assert exc_info.value.translation_key == "controller_only_service"
assert exc_info.value.translation_placeholders == {
"service": EvoService.SET_SYSTEM_MODE,
}
@pytest.mark.parametrize("install", ["default"])
@pytest.mark.usefixtures("evohome")
async def test_set_system_mode_entity_not_found(hass: HomeAssistant) -> None:
"""Test set_system_mode with a non-existent entity_id raises entity_not_found."""
non_existent_entity_id = "climate.non_existent_entity"
with pytest.raises(ServiceValidationError) as exc_info:
await hass.services.async_call(
DOMAIN,
EvoService.SET_SYSTEM_MODE,
{
ATTR_MODE: "Auto",
ATTR_ENTITY_ID: non_existent_entity_id,
},
blocking=True,
)
assert exc_info.value.translation_key == "entity_not_found"
assert exc_info.value.translation_placeholders == {
ATTR_ENTITY_ID: non_existent_entity_id,
}
_SET_SYSTEM_MODE_VALIDATOR_PARAMS = [
(
{ATTR_MODE: "NotARealMode"},
"mode_not_supported",
),
(
{ATTR_MODE: "Auto", ATTR_DURATION: {"hours": 1}},
"mode_cant_be_temporary",
),
(
{ATTR_MODE: "AutoWithEco", ATTR_PERIOD: {"days": 1}},
"mode_cant_have_period",
),
(
{ATTR_MODE: "DayOff", ATTR_DURATION: {"hours": 1}},
"mode_cant_have_duration",
),
]
@pytest.mark.parametrize("install", ["default"])
@pytest.mark.usefixtures("evohome")
@pytest.mark.parametrize(
("service_data", "expected_translation_key"),
_SET_SYSTEM_MODE_VALIDATOR_PARAMS,
ids=[k for _, k in _SET_SYSTEM_MODE_VALIDATOR_PARAMS],
)
async def test_set_system_mode_validator(
hass: HomeAssistant,
service_data: dict[str, Any],
expected_translation_key: str,
) -> None:
"""Test ServiceValidationError for all controller system mode validation cases."""
with pytest.raises(ServiceValidationError) as exc_info:
await hass.services.async_call(
DOMAIN,
EvoService.SET_SYSTEM_MODE,
service_data,
blocking=True,
)
assert exc_info.value.translation_key == expected_translation_key
assert exc_info.value.translation_placeholders == {
ATTR_MODE: service_data[ATTR_MODE]
}
@pytest.mark.parametrize("install", ["default"])
async def test_set_dhw_override(
hass: HomeAssistant,
dhw_id: str,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test Evohome's set_dhw_override service (for a DHW zone)."""
freezer.move_to("2024-07-10T12:00:00+00:00")
# EvoZoneMode.PERMANENT_OVERRIDE (off)
with patch("evohomeasync2.hotwater.HotWater.set_off") as mock_fcn:
await hass.services.async_call(
DOMAIN,
EvoService.SET_DHW_OVERRIDE,
{
ATTR_STATE: False,
},
target={ATTR_ENTITY_ID: dhw_id},
blocking=True,
)
mock_fcn.assert_awaited_once_with(until=None)
# EvoZoneMode.TEMPORARY_OVERRIDE (on)
with patch("evohomeasync2.hotwater.HotWater.set_on") as mock_fcn:
await hass.services.async_call(
DOMAIN,
EvoService.SET_DHW_OVERRIDE,
{
ATTR_STATE: True,
ATTR_DURATION: {"minutes": 135},
},
target={ATTR_ENTITY_ID: dhw_id},
blocking=True,
)
mock_fcn.assert_awaited_once_with(
until=datetime(2024, 7, 10, 14, 15, tzinfo=UTC)
)
@pytest.mark.parametrize("install", ["default"])
async def test_set_dhw_override_advance(
hass: HomeAssistant,
dhw_id: str,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test Evohome's set_dhw_override service with duration=0.
The override is temporary until the next schedule change.
"""
freezer.move_to("2024-05-10T12:15:00+00:00")
expected_until = datetime(2024, 5, 10, 15, 30, tzinfo=UTC)
# Simulate the schedule not yet having been fetched (e.g. HOMEASSISTANT_START)
entities = hass.data[DATA_DOMAIN_PLATFORM_ENTITIES].get(
(WATER_HEATER_DOMAIN, DOMAIN), {}
)
dhw_entity: EvoDHW = entities[dhw_id] # type: ignore[assignment]
dhw_entity._schedule = None
dhw_entity._setpoints = {}
# EvoZoneMode.TEMPORARY_OVERRIDE with duration 0 (i.e. until next schedule change)
with patch("evohomeasync2.hotwater.HotWater.set_on") as mock_fcn:
await hass.services.async_call(
DOMAIN,
EvoService.SET_DHW_OVERRIDE,
{
ATTR_STATE: True,
ATTR_DURATION: {"minutes": 0},
},
target={ATTR_ENTITY_ID: dhw_id},
blocking=True,
)
mock_fcn.assert_awaited_once_with(until=expected_until)
assert dhw_entity.setpoints["next_sp_from"] == expected_until