Add tests to Evohome for its native services (#139104)

initial commit
This commit is contained in:
David Bonnes 2025-02-23 11:43:25 +00:00 committed by GitHub
parent 91668e99e3
commit 746d1800f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 202 additions and 65 deletions

View File

@ -25,6 +25,7 @@ import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
ATTR_MODE,
CONF_PASSWORD, CONF_PASSWORD,
CONF_SCAN_INTERVAL, CONF_SCAN_INTERVAL,
CONF_USERNAME, CONF_USERNAME,
@ -40,11 +41,10 @@ from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey from homeassistant.util.hass_dict import HassKey
from .const import ( from .const import (
ATTR_DURATION_DAYS, ATTR_DURATION,
ATTR_DURATION_HOURS,
ATTR_DURATION_UNTIL, ATTR_DURATION_UNTIL,
ATTR_SYSTEM_MODE, ATTR_PERIOD,
ATTR_ZONE_TEMP, ATTR_SETPOINT,
CONF_LOCATION_IDX, CONF_LOCATION_IDX,
DOMAIN, DOMAIN,
SCAN_INTERVAL_DEFAULT, SCAN_INTERVAL_DEFAULT,
@ -81,7 +81,7 @@ RESET_ZONE_OVERRIDE_SCHEMA: Final = vol.Schema(
SET_ZONE_OVERRIDE_SCHEMA: Final = vol.Schema( SET_ZONE_OVERRIDE_SCHEMA: Final = vol.Schema(
{ {
vol.Required(ATTR_ENTITY_ID): cv.entity_id, vol.Required(ATTR_ENTITY_ID): cv.entity_id,
vol.Required(ATTR_ZONE_TEMP): vol.All( vol.Required(ATTR_SETPOINT): vol.All(
vol.Coerce(float), vol.Range(min=4.0, max=35.0) vol.Coerce(float), vol.Range(min=4.0, max=35.0)
), ),
vol.Optional(ATTR_DURATION_UNTIL): vol.All( vol.Optional(ATTR_DURATION_UNTIL): vol.All(
@ -222,7 +222,7 @@ def setup_service_functions(
# Permanent-only modes will use this schema # Permanent-only modes will use this schema
perm_modes = [m[SZ_SYSTEM_MODE] for m in modes if not m[SZ_CAN_BE_TEMPORARY]] perm_modes = [m[SZ_SYSTEM_MODE] for m in modes if not m[SZ_CAN_BE_TEMPORARY]]
if perm_modes: # any of: "Auto", "HeatingOff": permanent only if perm_modes: # any of: "Auto", "HeatingOff": permanent only
schema = vol.Schema({vol.Required(ATTR_SYSTEM_MODE): vol.In(perm_modes)}) schema = vol.Schema({vol.Required(ATTR_MODE): vol.In(perm_modes)})
system_mode_schemas.append(schema) system_mode_schemas.append(schema)
modes = [m for m in modes if m[SZ_CAN_BE_TEMPORARY]] modes = [m for m in modes if m[SZ_CAN_BE_TEMPORARY]]
@ -232,8 +232,8 @@ def setup_service_functions(
if temp_modes: # any of: "AutoWithEco", permanent or for 0-24 hours if temp_modes: # any of: "AutoWithEco", permanent or for 0-24 hours
schema = vol.Schema( schema = vol.Schema(
{ {
vol.Required(ATTR_SYSTEM_MODE): vol.In(temp_modes), vol.Required(ATTR_MODE): vol.In(temp_modes),
vol.Optional(ATTR_DURATION_HOURS): vol.All( vol.Optional(ATTR_DURATION): vol.All(
cv.time_period, cv.time_period,
vol.Range(min=timedelta(hours=0), max=timedelta(hours=24)), vol.Range(min=timedelta(hours=0), max=timedelta(hours=24)),
), ),
@ -246,8 +246,8 @@ def setup_service_functions(
if temp_modes: # any of: "Away", "Custom", "DayOff", permanent or for 1-99 days if temp_modes: # any of: "Away", "Custom", "DayOff", permanent or for 1-99 days
schema = vol.Schema( schema = vol.Schema(
{ {
vol.Required(ATTR_SYSTEM_MODE): vol.In(temp_modes), vol.Required(ATTR_MODE): vol.In(temp_modes),
vol.Optional(ATTR_DURATION_DAYS): vol.All( vol.Optional(ATTR_PERIOD): vol.All(
cv.time_period, cv.time_period,
vol.Range(min=timedelta(days=1), max=timedelta(days=99)), vol.Range(min=timedelta(days=1), max=timedelta(days=99)),
), ),

View File

@ -29,7 +29,7 @@ from homeassistant.components.climate import (
ClimateEntityFeature, ClimateEntityFeature,
HVACMode, HVACMode,
) )
from homeassistant.const import PRECISION_TENTHS, UnitOfTemperature from homeassistant.const import ATTR_MODE, PRECISION_TENTHS, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -38,11 +38,10 @@ from homeassistant.util import dt as dt_util
from . import EVOHOME_KEY from . import EVOHOME_KEY
from .const import ( from .const import (
ATTR_DURATION_DAYS, ATTR_DURATION,
ATTR_DURATION_HOURS,
ATTR_DURATION_UNTIL, ATTR_DURATION_UNTIL,
ATTR_SYSTEM_MODE, ATTR_PERIOD,
ATTR_ZONE_TEMP, ATTR_SETPOINT,
EvoService, EvoService,
) )
from .coordinator import EvoDataUpdateCoordinator from .coordinator import EvoDataUpdateCoordinator
@ -180,7 +179,7 @@ class EvoZone(EvoChild, EvoClimateEntity):
return return
# otherwise it is EvoService.SET_ZONE_OVERRIDE # otherwise it is EvoService.SET_ZONE_OVERRIDE
temperature = max(min(data[ATTR_ZONE_TEMP], self.max_temp), self.min_temp) temperature = max(min(data[ATTR_SETPOINT], self.max_temp), self.min_temp)
if ATTR_DURATION_UNTIL in data: if ATTR_DURATION_UNTIL in data:
duration: timedelta = data[ATTR_DURATION_UNTIL] duration: timedelta = data[ATTR_DURATION_UNTIL]
@ -349,16 +348,16 @@ class EvoController(EvoClimateEntity):
Data validation is not required, it will have been done upstream. Data validation is not required, it will have been done upstream.
""" """
if service == EvoService.SET_SYSTEM_MODE: if service == EvoService.SET_SYSTEM_MODE:
mode = data[ATTR_SYSTEM_MODE] mode = data[ATTR_MODE]
else: # otherwise it is EvoService.RESET_SYSTEM else: # otherwise it is EvoService.RESET_SYSTEM
mode = EvoSystemMode.AUTO_WITH_RESET mode = EvoSystemMode.AUTO_WITH_RESET
if ATTR_DURATION_DAYS in data: if ATTR_PERIOD in data:
until = dt_util.start_of_local_day() until = dt_util.start_of_local_day()
until += data[ATTR_DURATION_DAYS] until += data[ATTR_PERIOD]
elif ATTR_DURATION_HOURS in data: elif ATTR_DURATION in data:
until = dt_util.now() + data[ATTR_DURATION_HOURS] until = dt_util.now() + data[ATTR_DURATION]
else: else:
until = None until = None

View File

@ -18,11 +18,10 @@ USER_DATA: Final = "user_data"
SCAN_INTERVAL_DEFAULT: Final = timedelta(seconds=300) SCAN_INTERVAL_DEFAULT: Final = timedelta(seconds=300)
SCAN_INTERVAL_MINIMUM: Final = timedelta(seconds=60) SCAN_INTERVAL_MINIMUM: Final = timedelta(seconds=60)
ATTR_SYSTEM_MODE: Final = "mode" ATTR_PERIOD: Final = "period" # number of days
ATTR_DURATION_DAYS: Final = "period" ATTR_DURATION: Final = "duration" # number of minutes, <24h
ATTR_DURATION_HOURS: Final = "duration"
ATTR_ZONE_TEMP: Final = "setpoint" ATTR_SETPOINT: Final = "setpoint"
ATTR_DURATION_UNTIL: Final = "duration" ATTR_DURATION_UNTIL: Final = "duration"

View File

@ -0,0 +1,177 @@
"""The tests for the native services of Evohome."""
from __future__ import annotations
from datetime import UTC, datetime
from unittest.mock import patch
from evohomeasync2 import EvohomeClient
from freezegun.api import FrozenDateTimeFactory
import pytest
from homeassistant.components.evohome.const import (
ATTR_DURATION,
ATTR_PERIOD,
ATTR_SETPOINT,
DOMAIN,
EvoService,
)
from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE
from homeassistant.core import HomeAssistant
@pytest.mark.parametrize("install", ["default"])
async def test_service_refresh_system(
hass: HomeAssistant,
evohome: EvohomeClient,
) -> 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", ["default"])
async def test_service_reset_system(
hass: HomeAssistant,
ctl_id: str,
) -> None:
"""Test Evohome's reset_system service (for a temperature control system)."""
# EvoService.RESET_SYSTEM (if SZ_AUTO_WITH_RESET in modes)
with patch("evohomeasync2.control_system.ControlSystem.set_mode") as mock_fcn:
await hass.services.async_call(
DOMAIN,
EvoService.RESET_SYSTEM,
{},
blocking=True,
)
mock_fcn.assert_awaited_once_with("AutoWithReset", until=None)
@pytest.mark.parametrize("install", ["default"])
async def test_ctl_set_system_mode(
hass: HomeAssistant,
ctl_id: str,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test Evohome's set_system_mode service (for a temperature control system)."""
# 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)
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_zone_clear_zone_override(
hass: HomeAssistant,
zone_id: str,
) -> 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.RESET_ZONE_OVERRIDE,
{
ATTR_ENTITY_ID: zone_id,
},
blocking=True,
)
mock_fcn.assert_awaited_once_with()
@pytest.mark.parametrize("install", ["default"])
async def test_zone_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_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)
)

View File

@ -1,4 +1,4 @@
"""The tests for evohome.""" """The tests for Evohome."""
from __future__ import annotations from __future__ import annotations
@ -11,7 +11,7 @@ from evohomeasync2 import EvohomeClient, exceptions as exc
import pytest import pytest
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
from homeassistant.components.evohome.const import DOMAIN, EvoService from homeassistant.components.evohome.const import DOMAIN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -187,41 +187,3 @@ async def test_setup(
""" """
assert hass.services.async_services_for_domain(DOMAIN).keys() == snapshot assert hass.services.async_services_for_domain(DOMAIN).keys() == snapshot
@pytest.mark.parametrize("install", ["default"])
async def test_service_refresh_system(
hass: HomeAssistant,
evohome: EvohomeClient,
) -> None:
"""Test EvoService.REFRESH_SYSTEM of an evohome system."""
# 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", ["default"])
async def test_service_reset_system(
hass: HomeAssistant,
evohome: EvohomeClient,
) -> None:
"""Test EvoService.RESET_SYSTEM of an evohome system."""
# EvoService.RESET_SYSTEM (if SZ_AUTO_WITH_RESET in modes)
with patch("evohomeasync2.control_system.ControlSystem.set_mode") as mock_fcn:
await hass.services.async_call(
DOMAIN,
EvoService.RESET_SYSTEM,
{},
blocking=True,
)
mock_fcn.assert_awaited_once_with("AutoWithReset", until=None)