Use evohome constants for evohome attrs (#175494)

This commit is contained in:
David Bonnes
2026-07-03 19:06:58 +02:00
committed by GitHub
parent 715a8984e4
commit f2dfafb5a8
6 changed files with 80 additions and 93 deletions
+12 -20
View File
@@ -6,10 +6,14 @@ from typing import Any, override
import evohomeasync2 as evo
from evohomeasync2.const import (
SZ_DURATION,
SZ_MODE,
SZ_PERIOD,
SZ_SETPOINT_STATUS,
SZ_SYSTEM_MODE,
SZ_SYSTEM_MODE_STATUS,
SZ_TEMPERATURE_STATUS,
SZ_UNTIL,
SystemMode as EvoSystemMode,
ZoneMode as EvoZoneMode,
)
@@ -23,12 +27,7 @@ from homeassistant.components.climate import (
ClimateEntityFeature,
HVACMode,
)
from homeassistant.const import (
ATTR_MODE,
ATTR_TEMPERATURE,
PRECISION_TENTHS,
UnitOfTemperature,
)
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -36,14 +35,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util
from .const import (
ATTR_DURATION,
ATTR_PERIOD,
DOMAIN,
EVOHOME_DATA,
RESET_BREAKS_IN_HA_VERSION,
EvoService,
)
from .const import DOMAIN, EVOHOME_DATA, RESET_BREAKS_IN_HA_VERSION, EvoService
from .coordinator import EvoDataUpdateCoordinator
from .entity import EvoChild, EvoEntity, is_valid_zone, unique_zone_id
from .helpers import async_create_deprecation_issue_once
@@ -270,7 +262,7 @@ class EvoZone(EvoChild, EvoClimateEntity):
temperature = kwargs[ATTR_TEMPERATURE]
if (until := kwargs.get("until")) is None:
if (until := kwargs.get(SZ_UNTIL)) is None:
if self._evo_device.mode == EvoZoneMode.TEMPORARY_OVERRIDE:
until = self._evo_device.until
if self._evo_device.mode == EvoZoneMode.FOLLOW_SCHEDULE:
@@ -395,14 +387,14 @@ class EvoController(EvoClimateEntity):
await self.coordinator.call_client_api(self._evo_device.reset())
return
mode = data[ATTR_MODE] # otherwise it is EvoService.SET_SYSTEM_MODE
mode = data[SZ_MODE] # otherwise it is EvoService.SET_SYSTEM_MODE
if ATTR_PERIOD in data:
if SZ_PERIOD in data:
until = dt_util.start_of_local_day()
until += data[ATTR_PERIOD]
until += data[SZ_PERIOD]
elif ATTR_DURATION in data:
until = dt_util.now() + data[ATTR_DURATION]
elif SZ_DURATION in data:
until = dt_util.now() + data[SZ_DURATION]
else:
until = None
@@ -20,10 +20,6 @@ CONF_LOCATION_IDX: Final = "location_idx"
SCAN_INTERVAL_DEFAULT: Final = timedelta(seconds=300)
SCAN_INTERVAL_MINIMUM: Final = timedelta(seconds=60)
ATTR_DURATION: Final = "duration" # number of minutes, <24h
ATTR_PERIOD: Final = "period" # number of days
ATTR_SETPOINT: Final = "setpoint"
# Support for the refresh_system service is being deprecated
REFRESH_BREAKS_IN_HA_VERSION: Final = "2027.1.0"
# Support for the reset service calls/presets is being deprecated
+4 -1
View File
@@ -8,6 +8,9 @@ from typing import Any, override
import evohomeasync2 as evo
from evohomeasync2.const import (
SZ_SINCE,
SZ_TIME_UNTIL,
SZ_UNTIL,
ZoneModelType as EvoZoneModelType,
ZoneType as EvoZoneType,
)
@@ -28,7 +31,7 @@ def _recurse_and_revert(val: Any, _key: str | None = None) -> Any:
return {k: _recurse_and_revert(v, k) for k, v in val.items()}
if isinstance(val, (list, tuple)):
return type(val)(_recurse_and_revert(v) for v in val)
if isinstance(val, datetime) and _key in ("since", "time_until", "until"):
if isinstance(val, datetime) and _key in (SZ_SINCE, SZ_TIME_UNTIL, SZ_UNTIL):
return val.isoformat()
if isinstance(val, StrEnum):
return "".join(word.capitalize() for word in val.value.split("_"))
+21 -23
View File
@@ -8,7 +8,10 @@ from evohomeasync2 import ControlSystem
from evohomeasync2.const import (
SZ_CAN_BE_TEMPORARY,
SZ_DURATION,
SZ_MODE,
SZ_PERIOD,
SZ_SETPOINT,
SZ_STATE,
SZ_SYSTEM_MODE,
SZ_TIMING_MODE,
)
@@ -16,7 +19,7 @@ import voluptuous as vol
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
from homeassistant.components.water_heater import DOMAIN as WATER_HEATER_DOMAIN
from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, ATTR_STATE
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import (
@@ -28,9 +31,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.service import verify_domain_control
from .const import (
ATTR_DURATION,
ATTR_PERIOD,
ATTR_SETPOINT,
DOMAIN,
REFRESH_BREAKS_IN_HA_VERSION,
RESET_BREAKS_IN_HA_VERSION,
@@ -49,12 +49,12 @@ def _as_snake_case(mode: str) -> str:
# System service schemas (registered as domain services)
SET_SYSTEM_MODE_SCHEMA: Final[dict[str | vol.Marker, Any]] = {
# unsupported modes are rejected at runtime with ServiceValidationError
vol.Required(ATTR_MODE): cv.string, # ... so, don't use SystemMode enum here
vol.Exclusive(ATTR_DURATION, "temporary"): vol.All(
vol.Required(SZ_MODE): cv.string, # ... so, don't use SystemMode enum here
vol.Exclusive(SZ_DURATION, "temporary"): vol.All(
cv.time_period,
vol.Range(min=timedelta(hours=0), max=timedelta(hours=24)),
),
vol.Exclusive(ATTR_PERIOD, "temporary"): vol.All(
vol.Exclusive(SZ_PERIOD, "temporary"): vol.All(
cv.time_period,
vol.Range(min=timedelta(days=1), max=timedelta(days=99)),
),
@@ -63,10 +63,8 @@ SET_SYSTEM_MODE_SCHEMA: Final[dict[str | vol.Marker, Any]] = {
# Zone service schemas (registered as entity services)
SET_ZONE_OVERRIDE_SCHEMA: Final[dict[str | vol.Marker, Any]] = {
vol.Required(ATTR_SETPOINT): vol.All(
vol.Coerce(float), vol.Range(min=4.0, max=35.0)
),
vol.Optional(ATTR_DURATION): vol.All(
vol.Required(SZ_SETPOINT): vol.All(vol.Coerce(float), vol.Range(min=4.0, max=35.0)),
vol.Optional(SZ_DURATION): vol.All(
cv.time_period,
vol.Range(min=timedelta(days=0), max=timedelta(days=1)),
),
@@ -74,8 +72,8 @@ SET_ZONE_OVERRIDE_SCHEMA: Final[dict[str | vol.Marker, Any]] = {
# DHW service schemas (registered as entity services)
SET_DHW_OVERRIDE_SCHEMA: Final[dict[str | vol.Marker, Any]] = {
vol.Required(ATTR_STATE): cv.boolean,
vol.Optional(ATTR_DURATION): vol.All(
vol.Required(SZ_STATE): cv.boolean,
vol.Optional(SZ_DURATION): vol.All(
cv.time_period,
vol.Range(min=timedelta(days=0), max=timedelta(days=1)),
),
@@ -163,7 +161,7 @@ def _register_dhw_entity_services(hass: HomeAssistant) -> None:
def _validate_set_system_mode_params(tcs: ControlSystem, data: dict[str, Any]) -> None:
"""Validate that a set_system_mode service call is properly formed."""
mode = data[ATTR_MODE]
mode = data[SZ_MODE]
tcs_modes = {m[SZ_SYSTEM_MODE].value: m for m in tcs.allowed_system_modes}
# Validation occurs here, instead of in the library, because it uses a slightly
@@ -174,34 +172,34 @@ def _validate_set_system_mode_params(tcs: ControlSystem, data: dict[str, Any]) -
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="mode_not_supported",
translation_placeholders={ATTR_MODE: mode},
translation_placeholders={SZ_MODE: mode},
)
# voluptuous schema ensures that duration and period are not both present
if not mode_info[SZ_CAN_BE_TEMPORARY]:
if ATTR_DURATION in data or ATTR_PERIOD in data:
if SZ_DURATION in data or SZ_PERIOD in data:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="mode_cant_be_temporary",
translation_placeholders={ATTR_MODE: mode},
translation_placeholders={SZ_MODE: mode},
)
return
timing_mode = mode_info.get(SZ_TIMING_MODE) # will not be None, as can_be_temporary
if timing_mode == SZ_DURATION and ATTR_PERIOD in data:
if timing_mode == SZ_DURATION and SZ_PERIOD in data:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="mode_cant_have_period",
translation_placeholders={ATTR_MODE: mode},
translation_placeholders={SZ_MODE: mode},
)
if timing_mode == SZ_PERIOD and ATTR_DURATION in data:
if timing_mode == SZ_PERIOD and SZ_DURATION in data:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="mode_cant_have_duration",
translation_placeholders={ATTR_MODE: mode},
translation_placeholders={SZ_MODE: mode},
)
@@ -252,8 +250,8 @@ def setup_service_functions(
payload = {
"unique_id": unique_id,
"service": call.service,
"data": {**call.data, ATTR_MODE: _as_snake_case(call.data[ATTR_MODE])}
if ATTR_MODE in call.data
"data": {**call.data, SZ_MODE: _as_snake_case(call.data[SZ_MODE])}
if SZ_MODE in call.data
else call.data,
}
async_dispatcher_send(hass, DOMAIN, payload)
+32 -36
View File
@@ -4,15 +4,13 @@ from datetime import UTC, datetime
from typing import Any
from unittest.mock import patch
from evohomeasync2.const import SZ_DURATION, SZ_MODE, SZ_PERIOD, SZ_SETPOINT, SZ_STATE
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,
REFRESH_BREAKS_IN_HA_VERSION,
RESET_BREAKS_IN_HA_VERSION,
@@ -21,7 +19,7 @@ from homeassistant.components.evohome.const import (
)
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.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import issue_registry as ir
@@ -133,7 +131,7 @@ async def test_set_system_mode_deprecated(
DOMAIN,
EvoService.SET_SYSTEM_MODE,
{
ATTR_MODE: "Auto",
SZ_MODE: "Auto",
},
blocking=True,
)
@@ -156,8 +154,8 @@ async def test_set_system_mode_deprecated(
DOMAIN,
EvoService.SET_SYSTEM_MODE,
{
ATTR_MODE: "AutoWithEco",
ATTR_DURATION: {"hours": 12},
SZ_MODE: "AutoWithEco",
SZ_DURATION: {"hours": 12},
},
blocking=True,
)
@@ -172,8 +170,8 @@ async def test_set_system_mode_deprecated(
DOMAIN,
EvoService.SET_SYSTEM_MODE,
{
ATTR_MODE: "Away",
ATTR_PERIOD: {"days": 7},
SZ_MODE: "Away",
SZ_PERIOD: {"days": 7},
},
blocking=True,
)
@@ -199,8 +197,8 @@ async def test_set_system_mode(
DOMAIN,
EvoService.SET_SYSTEM_MODE,
{
ATTR_MODE: "Away",
ATTR_PERIOD: {"days": 7},
SZ_MODE: "Away",
SZ_PERIOD: {"days": 7},
},
target={ATTR_ENTITY_ID: ctl_id},
blocking=True,
@@ -217,8 +215,8 @@ async def test_set_system_mode(
EvoService.SET_SYSTEM_MODE,
{
ATTR_ENTITY_ID: ctl_id,
ATTR_MODE: "Away",
ATTR_PERIOD: {"days": 7},
SZ_MODE: "Away",
SZ_PERIOD: {"days": 7},
},
blocking=True,
)
@@ -308,7 +306,7 @@ async def test_set_zone_override(
DOMAIN,
EvoService.SET_ZONE_OVERRIDE,
{
ATTR_SETPOINT: 19.5,
SZ_SETPOINT: 19.5,
},
target={ATTR_ENTITY_ID: zone_id},
blocking=True,
@@ -322,8 +320,8 @@ async def test_set_zone_override(
DOMAIN,
EvoService.SET_ZONE_OVERRIDE,
{
ATTR_SETPOINT: 19.5,
ATTR_DURATION: {"minutes": 135},
SZ_SETPOINT: 19.5,
SZ_DURATION: {"minutes": 135},
},
target={ATTR_ENTITY_ID: zone_id},
blocking=True,
@@ -363,8 +361,8 @@ async def test_set_zone_override_advance(
DOMAIN,
EvoService.SET_ZONE_OVERRIDE,
{
ATTR_SETPOINT: 19.5,
ATTR_DURATION: {"minutes": 0},
SZ_SETPOINT: 19.5,
SZ_DURATION: {"minutes": 0},
},
target={ATTR_ENTITY_ID: zone_id},
blocking=True,
@@ -392,7 +390,7 @@ async def test_set_zone_override_legacy(
EvoService.SET_ZONE_OVERRIDE,
{
ATTR_ENTITY_ID: zone_id,
ATTR_SETPOINT: 19.5,
SZ_SETPOINT: 19.5,
},
blocking=True,
)
@@ -406,8 +404,8 @@ async def test_set_zone_override_legacy(
EvoService.SET_ZONE_OVERRIDE,
{
ATTR_ENTITY_ID: zone_id,
ATTR_SETPOINT: 19.5,
ATTR_DURATION: {"minutes": 135},
SZ_SETPOINT: 19.5,
SZ_DURATION: {"minutes": 135},
},
blocking=True,
)
@@ -422,7 +420,7 @@ async def test_set_zone_override_legacy(
("service", "service_data"),
[
(EvoService.CLEAR_ZONE_OVERRIDE, {}),
(EvoService.SET_ZONE_OVERRIDE, {ATTR_SETPOINT: 19.5}),
(EvoService.SET_ZONE_OVERRIDE, {SZ_SETPOINT: 19.5}),
],
)
async def test_zone_services_with_ctl_id(
@@ -458,7 +456,7 @@ async def test_controller_services_with_zone_id(
DOMAIN,
EvoService.SET_SYSTEM_MODE,
{
ATTR_MODE: "Auto",
SZ_MODE: "Auto",
ATTR_ENTITY_ID: zone_id,
},
blocking=True,
@@ -482,7 +480,7 @@ async def test_set_system_mode_entity_not_found(hass: HomeAssistant) -> None:
DOMAIN,
EvoService.SET_SYSTEM_MODE,
{
ATTR_MODE: "Auto",
SZ_MODE: "Auto",
ATTR_ENTITY_ID: non_existent_entity_id,
},
blocking=True,
@@ -496,19 +494,19 @@ async def test_set_system_mode_entity_not_found(hass: HomeAssistant) -> None:
_SET_SYSTEM_MODE_VALIDATOR_PARAMS = [
(
{ATTR_MODE: "NotARealMode"},
{SZ_MODE: "NotARealMode"},
"mode_not_supported",
),
(
{ATTR_MODE: "Auto", ATTR_DURATION: {"hours": 1}},
{SZ_MODE: "Auto", SZ_DURATION: {"hours": 1}},
"mode_cant_be_temporary",
),
(
{ATTR_MODE: "AutoWithEco", ATTR_PERIOD: {"days": 1}},
{SZ_MODE: "AutoWithEco", SZ_PERIOD: {"days": 1}},
"mode_cant_have_period",
),
(
{ATTR_MODE: "DayOff", ATTR_DURATION: {"hours": 1}},
{SZ_MODE: "DayOff", SZ_DURATION: {"hours": 1}},
"mode_cant_have_duration",
),
]
@@ -537,9 +535,7 @@ async def test_set_system_mode_validator(
)
assert exc_info.value.translation_key == expected_translation_key
assert exc_info.value.translation_placeholders == {
ATTR_MODE: service_data[ATTR_MODE]
}
assert exc_info.value.translation_placeholders == {SZ_MODE: service_data[SZ_MODE]}
@pytest.mark.parametrize("install", ["default"])
@@ -558,7 +554,7 @@ async def test_set_dhw_override(
DOMAIN,
EvoService.SET_DHW_OVERRIDE,
{
ATTR_STATE: False,
SZ_STATE: False,
},
target={ATTR_ENTITY_ID: dhw_id},
blocking=True,
@@ -572,8 +568,8 @@ async def test_set_dhw_override(
DOMAIN,
EvoService.SET_DHW_OVERRIDE,
{
ATTR_STATE: True,
ATTR_DURATION: {"minutes": 135},
SZ_STATE: True,
SZ_DURATION: {"minutes": 135},
},
target={ATTR_ENTITY_ID: dhw_id},
blocking=True,
@@ -613,8 +609,8 @@ async def test_set_dhw_override_advance(
DOMAIN,
EvoService.SET_DHW_OVERRIDE,
{
ATTR_STATE: True,
ATTR_DURATION: {"minutes": 0},
SZ_STATE: True,
SZ_DURATION: {"minutes": 0},
},
target={ATTR_ENTITY_ID: dhw_id},
blocking=True,
+11 -9
View File
@@ -3,9 +3,15 @@
from datetime import datetime, timedelta
from typing import Any, Final, NotRequired, TypedDict
from evohomeasync2.auth import (
SZ_ACCESS_TOKEN,
SZ_ACCESS_TOKEN_EXPIRES,
SZ_REFRESH_TOKEN,
)
import pytest
from homeassistant.components.evohome.const import DOMAIN, STORAGE_KEY, STORAGE_VER
from homeassistant.const import CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.util import dt as dt_util
@@ -30,10 +36,6 @@ class _EmptyStoreT(TypedDict):
pass
SZ_USERNAME: Final = "username"
SZ_REFRESH_TOKEN: Final = "refresh_token"
SZ_ACCESS_TOKEN: Final = "access_token"
SZ_ACCESS_TOKEN_EXPIRES: Final = "access_token_expires"
SZ_USER_DATA: Final = "user_data"
@@ -49,7 +51,7 @@ USERNAME_DIFF: Final = f"not_{USERNAME}"
USERNAME_SAME: Final = USERNAME
_TEST_STORAGE_BASE: Final[_TokenStoreT] = {
SZ_USERNAME: USERNAME_SAME,
CONF_USERNAME: USERNAME_SAME,
SZ_REFRESH_TOKEN: REFRESH_TOKEN,
SZ_ACCESS_TOKEN: ACCESS_TOKEN,
SZ_ACCESS_TOKEN_EXPIRES: ACCESS_TOKEN_EXP_STR,
@@ -92,7 +94,7 @@ async def test_auth_tokens_null(
# Confirm the expected tokens were cached to storage...
data: _TokenStoreT = hass_storage[DOMAIN]["data"]
assert data[SZ_USERNAME] == USERNAME_SAME
assert data[CONF_USERNAME] == USERNAME_SAME
assert data[SZ_REFRESH_TOKEN] == f"new_{REFRESH_TOKEN}"
assert data[SZ_ACCESS_TOKEN] == f"new_{ACCESS_TOKEN}"
assert (
@@ -120,7 +122,7 @@ async def test_auth_tokens_same(
# Confirm the expected tokens were cached to storage...
data: _TokenStoreT = hass_storage[DOMAIN]["data"]
assert data[SZ_USERNAME] == USERNAME_SAME
assert data[CONF_USERNAME] == USERNAME_SAME
assert data[SZ_REFRESH_TOKEN] == REFRESH_TOKEN
assert data[SZ_ACCESS_TOKEN] == ACCESS_TOKEN
assert dt_util.parse_datetime(data[SZ_ACCESS_TOKEN_EXPIRES]) == ACCESS_TOKEN_EXP_DTM
@@ -151,7 +153,7 @@ async def test_auth_tokens_past(
# Confirm the expected tokens were cached to storage...
data: _TokenStoreT = hass_storage[DOMAIN]["data"]
assert data[SZ_USERNAME] == USERNAME_SAME
assert data[CONF_USERNAME] == USERNAME_SAME
assert data[SZ_REFRESH_TOKEN] == f"new_{REFRESH_TOKEN}"
assert data[SZ_ACCESS_TOKEN] == f"new_{ACCESS_TOKEN}"
assert (
@@ -180,7 +182,7 @@ async def test_auth_tokens_diff(
# Confirm the expected tokens were cached to storage...
data: _TokenStoreT = hass_storage[DOMAIN]["data"]
assert data[SZ_USERNAME] == USERNAME_DIFF
assert data[CONF_USERNAME] == USERNAME_DIFF
assert data[SZ_REFRESH_TOKEN] == f"new_{REFRESH_TOKEN}"
assert data[SZ_ACCESS_TOKEN] == f"new_{ACCESS_TOKEN}"
assert (