From af36efd33e521502e36be6f26abb3dbb60ac9adf Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Mon, 23 Sep 2024 10:52:17 +0100 Subject: [PATCH] Use .id, and not xxxxId (nor ._id), tweak super classes --- src/evohomeasync/base.py | 4 +- src/evohomeasync/broker.py | 12 +- src/evohomeasync/exceptions.py | 2 +- src/evohomeasync/schema.py | 2 +- src/evohomeasync2/__init__.py | 16 +- src/evohomeasync2/base.py | 19 +- src/evohomeasync2/broker.py | 11 +- src/evohomeasync2/client.py | 2 +- src/evohomeasync2/controlsystem.py | 48 ++- src/evohomeasync2/exceptions.py | 2 +- src/evohomeasync2/gateway.py | 30 +- src/evohomeasync2/hotwater.py | 18 +- src/evohomeasync2/location.py | 38 ++- src/evohomeasync2/schema/__init__.py | 14 +- src/evohomeasync2/schema/const.py | 125 ++++---- src/evohomeasync2/schema/schedule.py | 4 +- src/evohomeasync2/schema/typing.py | 8 - src/evohomeasync2/zone.py | 59 ++-- tests/tests/__snapshots__/test_installs.ambr | 295 +++++++++---------- tests/tests/conftest.py | 8 + tests/tests/test_installs.py | 13 +- tests/tests_rf/conftest.py | 2 + tests/tests_rf/faked_server/__init__.py | 1 - tests/tests_rf/faked_server/aiohttp.py | 4 + tests/tests_rf/faked_server/const.py | 4 +- tests/tests_rf/faked_server/vendor.py | 15 +- tests/tests_rf/test_v2_apis.py | 12 +- tests/tests_rf/test_v2_task.py | 6 +- tests/tests_rf/test_v2_urls.py | 41 +-- 29 files changed, 414 insertions(+), 401 deletions(-) diff --git a/src/evohomeasync/base.py b/src/evohomeasync/base.py index 544fab4c..07a77b3c 100644 --- a/src/evohomeasync/base.py +++ b/src/evohomeasync/base.py @@ -232,7 +232,7 @@ def user_data(self) -> _UserDataT | None: # TODO: deprecate? async def _populate_user_data( self, force_refresh: bool = False ) -> dict[str, bool | int | str]: - """Retrieve the cached user data (excl. the session ID). + """Retrieve the cached user data (excl. the session id). Pull the latest JSON from the web only if force_refresh is True. """ @@ -241,7 +241,7 @@ async def _populate_user_data( user_data = await self.broker.populate_user_data() self.user_info = user_data[SZ_USER_INFO] # type: ignore[assignment] - return self.user_info # excludes session ID + return self.user_info # excludes session id async def _get_user(self) -> _UserInfoT: """Return the user (if needed, get the JSON).""" diff --git a/src/evohomeasync/broker.py b/src/evohomeasync/broker.py index 1ce0da35..7613a787 100644 --- a/src/evohomeasync/broker.py +++ b/src/evohomeasync/broker.py @@ -61,7 +61,7 @@ def __init__( self._headers: dict[str, str] = { "content-type": "application/json" - } # NB: no sessionId yet + } # NB: no session_id yet self._POST_DATA: Final[dict[str, str]] = { "Username": self.username, "Password": password, @@ -142,7 +142,7 @@ async def _make_request( async with func(url_, json=data, headers=self._headers) as response: response_text = await response.text() # why cant I move this below the if? - # if 401/unauthorized, may need to refresh sessionId (expires in 15 mins?) + # if 401/unauthorized, may need to refresh session_id (expires in 15 mins?) if response.status != HTTPStatus.UNAUTHORIZED or _dont_reauthenticate: return response @@ -159,12 +159,14 @@ async def _make_request( # return response # ...because: the user credentials must be invalid _LOGGER.debug(f"Session now expired/invalid ({self._session_id})...") - self._headers = {"content-type": "application/json"} # remove the sessionId + self._headers = { + "content-type": "application/json" + } # remove the session_id - _, response = await self._populate_user_data() # Get a fresh sessionId + _, response = await self._populate_user_data() # Get a fresh session_id assert self._session_id is not None # mypy hint - _LOGGER.debug(f"... success: new sessionId = {self._session_id}") + _LOGGER.debug(f"... success: new session_id = {self._session_id}") self._headers[SZ_SESSION_ID] = self._session_id if "session" in url_: # retry not needed for /session diff --git a/src/evohomeasync/exceptions.py b/src/evohomeasync/exceptions.py index 5be7dc17..80401c5f 100644 --- a/src/evohomeasync/exceptions.py +++ b/src/evohomeasync/exceptions.py @@ -21,7 +21,7 @@ class DeprecationError(EvohomeBaseError): class InvalidSchema(EvohomeError): - """The config/status JSON is invalid (e.g. missing an entity Id).""" + """The config/status JSON is invalid (e.g. missing an entity id).""" class RequestFailed(EvohomeError): diff --git a/src/evohomeasync/schema.py b/src/evohomeasync/schema.py index 62d0e101..35d0b02b 100644 --- a/src/evohomeasync/schema.py +++ b/src/evohomeasync/schema.py @@ -24,7 +24,7 @@ _TaskIdT = NewType("_TaskIdT", str) # TODO: int or str? -SZ_SESSION_ID: Final = "sessionId" # id Id, not ID +SZ_SESSION_ID: Final = "sessionId" # is Id, not ID # schema keys (start with a lower case letter) SZ_ALLOWED_MODES: Final = "allowedModes" diff --git a/src/evohomeasync2/__init__.py b/src/evohomeasync2/__init__.py index 01b3f29f..90647ed5 100644 --- a/src/evohomeasync2/__init__.py +++ b/src/evohomeasync2/__init__.py @@ -13,7 +13,7 @@ import aiohttp -from .base import EvohomeClient as _EvohomeClientNew +from .base import EvohomeClient from .broker import AbstractTokenManager, Broker # noqa: F401 from .controlsystem import ControlSystem # noqa: F401 from .exceptions import ( # noqa: F401 @@ -53,8 +53,8 @@ def __init__( ) -> None: super().__init__(username, password, websession) - self.refresh_token = refresh_token - self.access_token = access_token + self.refresh_token = refresh_token or "" + self.access_token = access_token or "" self.access_token_expires = access_token_expires or dt.min async def restore_access_token(self) -> None: @@ -64,7 +64,7 @@ async def save_access_token(self) -> None: pass -class EvohomeClient(_EvohomeClientNew): +class EvohomeClientOld(EvohomeClient): """A wrapper to expose the new EvohomeClient in the old style.""" def __init__( @@ -95,21 +95,21 @@ def __init__( super().__init__(self._token_manager, websession, debug=debug) @property - def access_token(self) -> str: + def access_token(self) -> str: # type: ignore[override] """Return the access_token attr.""" return self._token_manager.access_token @property - def access_token_expires(self) -> dt: + def access_token_expires(self) -> dt: # type: ignore[override] """Return the access_token_expires attr.""" return self._token_manager.access_token_expires @property - def refresh_token(self) -> str: + def refresh_token(self) -> str: # type: ignore[override] """Return the refresh_token attr.""" return self._token_manager.refresh_token @property - def username(self) -> str: + def username(self) -> str: # type: ignore[override] """Return the username attr.""" return self._token_manager.username diff --git a/src/evohomeasync2/base.py b/src/evohomeasync2/base.py index c49b9e15..cd3c7109 100644 --- a/src/evohomeasync2/base.py +++ b/src/evohomeasync2/base.py @@ -15,9 +15,10 @@ from .controlsystem import ControlSystem from .location import Location from .schema import SCH_FULL_CONFIG, SCH_USER_ACCOUNT +from .schema.const import SZ_USER_ID if TYPE_CHECKING: - from .schema import _EvoDictT, _EvoListT, _LocationIdT, _ScheduleT, _SystemIdT + from .schema import _EvoDictT, _EvoListT, _ScheduleT _LOGGER: Final = logging.getLogger(__name__.rpartition(".")[0]) @@ -46,12 +47,10 @@ def access_token(self) -> NoReturn: def access_token_expires(self) -> NoReturn: raise exc.DeprecationError(f"{self}: access_token_expires attrs is deprecated") - async def full_installation( - self, location_id: None | _LocationIdT = None - ) -> NoReturn: - # if location_id is None: - # location_id = self.installation_info[0]["locationInfo"]["locationId"] - # url = f"location/{location_id}/installationInfo?" # Specific location + async def full_installation(self, loc_id: str | None = None) -> NoReturn: + # if loc_id is None: + # loc_id = self.installation_info[0][SZ_LOCATION_INFO][SZ_LOCATION_ID] + # url = f"location/{loc_id}/installationInfo?" # Specific location raise exc.DeprecationError( f"{self}: .full_installation() is deprecated, use .installation()" @@ -228,7 +227,7 @@ async def _installation(self, refresh_status: bool = True) -> _EvoListT: # FIXME: shouldn't really be starting again with new objects? self.locations = [] # for now, need to clear this before GET - url = f"location/installationInfo?userId={self.account_info['userId']}" + url = f"location/installationInfo?userId={self.account_info[SZ_USER_ID]}" url += "&includeTemperatureControlSystems=True" self._full_config = await self.broker.get(url, schema=SCH_FULL_CONFIG) # type: ignore[assignment] @@ -268,8 +267,8 @@ def _get_single_tcs(self) -> ControlSystem: return self.locations[0]._gateways[0]._control_systems[0] @property - def system_id(self) -> _SystemIdT: # an evohome-client anachronism, deprecate? - """Return the ID of the default TCS (assumes only one loc/gwy/TCS).""" + def system_id(self) -> str: # an evohome-client anachronism, deprecate? + """Return the id of the default TCS (assumes only one loc/gwy/TCS).""" return self._get_single_tcs().systemId async def reset_mode(self) -> None: diff --git a/src/evohomeasync2/broker.py b/src/evohomeasync2/broker.py index 8db9e2ad..6affabc9 100644 --- a/src/evohomeasync2/broker.py +++ b/src/evohomeasync2/broker.py @@ -56,6 +56,9 @@ HTTPStatus.UNAUTHORIZED: "Unauthorized (expired access token/unknown entity id?)", } +SZ_USERNAME: Final = "Username" # is PascalCase, not camelCase? +SZ_PASSWORD: Final = "Password" + class OAuthTokenData(TypedDict): access_token: str @@ -85,8 +88,8 @@ def __init__( """Initialize the token manager.""" self._user_credentials = { - "Username": username, - "Password": password, + SZ_USERNAME: username, + SZ_PASSWORD: password, } # TODO: are only ever PascalCase? self.websession = websession @@ -99,12 +102,12 @@ def __str__(self) -> str: @property def username(self) -> str: """Return the username.""" - return self._user_credentials["Username"] + return self._user_credentials[SZ_USERNAME] @property # TODO: remove this whan no longer needed def _password(self) -> str: """Return the username.""" - return self._user_credentials["Password"] + return self._user_credentials[SZ_PASSWORD] def _token_data_reset(self) -> None: """Reset the token data to its falsy state.""" diff --git a/src/evohomeasync2/client.py b/src/evohomeasync2/client.py index 158d561a..01092220 100644 --- a/src/evohomeasync2/client.py +++ b/src/evohomeasync2/client.py @@ -301,7 +301,7 @@ async def get_schedule( evo = ctx.obj[SZ_EVO] zon: HotWater | Zone = _get_tcs(evo, loc_idx).zones_by_id[zone_id] - schedule = {zon._id: {SZ_NAME: zon.name, SZ_SCHEDULE: await zon.get_schedule()}} + schedule = {zon.id: {SZ_NAME: zon.name, SZ_SCHEDULE: await zon.get_schedule()}} await _write(filename, json.dumps(schedule, indent=4) + "\r\n\r\n") diff --git a/src/evohomeasync2/controlsystem.py b/src/evohomeasync2/controlsystem.py index e480f48d..a1ecd2c5 100644 --- a/src/evohomeasync2/controlsystem.py +++ b/src/evohomeasync2/controlsystem.py @@ -24,7 +24,6 @@ from .hotwater import HotWater from .schema import SCH_TCS_STATUS from .schema.const import ( - SZ_ACTIVE_FAULTS, SZ_ALLOWED_SYSTEM_MODES, SZ_DHW, SZ_DHW_ID, @@ -37,10 +36,10 @@ SZ_SYSTEM_MODE_STATUS, SZ_TARGET_HEAT_TEMPERATURE, SZ_TEMPERATURE, - SZ_TEMPERATURE_CONTROL_SYSTEM, SZ_TIME_UNTIL, SZ_ZONE_ID, SZ_ZONES, + EntityType, ) from .zone import ActiveFaultsBase, Zone @@ -48,12 +47,16 @@ import voluptuous as vol from . import Gateway, Location - from .schema import _DhwIdT, _EvoDictT, _EvoListT, _ScheduleT, _SystemIdT, _ZoneIdT + from .schema import _EvoDictT, _EvoListT, _ScheduleT class _ControlSystemDeprecated: # pragma: no cover """Deprecated attributes and methods removed from the evohome-client namespace.""" + @property + def systemId(self) -> NoReturn: + raise exc.DeprecationError(f"{self}: .systemId is deprecated, use .id") + async def set_status_reset(self, *args, **kwargs) -> NoReturn: # type: ignore[no-untyped-def] raise exc.DeprecationError( f"{self}: .set_status_reset() is deprecrated, use .reset_mode()" @@ -119,12 +122,12 @@ class ControlSystem(ActiveFaultsBase, _ControlSystemDeprecated): """Instance of a gateway's TCS (temperatureControlSystem).""" STATUS_SCHEMA: Final[vol.Schema] = SCH_TCS_STATUS - TYPE: Final = SZ_TEMPERATURE_CONTROL_SYSTEM # type: ignore[misc] + TYPE: Final = EntityType.TCS # type: ignore[misc] def __init__(self, gateway: Gateway, config: _EvoDictT) -> None: super().__init__(config[SZ_SYSTEM_ID], gateway._broker, gateway._logger) - self.gateway = gateway + self.gateway = gateway # parent self.location: Location = gateway.location self._config: Final[_EvoDictT] = { @@ -132,6 +135,7 @@ def __init__(self, gateway: Gateway, config: _EvoDictT) -> None: } self._status: _EvoDictT = {} + # children self._zones: list[Zone] = [] self.zones: dict[str, Zone] = {} # zone by name! what to do if name changed? self.zones_by_id: dict[str, Zone] = {} @@ -148,16 +152,12 @@ def __init__(self, gateway: Gateway, config: _EvoDictT) -> None: else: self._zones.append(zone) self.zones[zone.name] = zone - self.zones_by_id[zone.zoneId] = zone + self.zones_by_id[zone.id] = zone dhw_config: _EvoDictT if dhw_config := config.get(SZ_DHW): # type: ignore[assignment] self.hotwater = HotWater(self, dhw_config) - @property - def systemId(self) -> _SystemIdT: - return self._id - @property def allowedSystemModes(self) -> _EvoListT: ret: _EvoListT = self._config[SZ_ALLOWED_SYSTEM_MODES] @@ -178,7 +178,7 @@ def _update_status(self, status: _EvoDictT) -> None: self._status = status if dhw_status := self._status.get(SZ_DHW): - if self.hotwater and self.hotwater._id == dhw_status[SZ_DHW_ID]: + if self.hotwater and self.hotwater.id == dhw_status[SZ_DHW_ID]: self.hotwater._update_status(dhw_status) else: @@ -197,10 +197,6 @@ def _update_status(self, status: _EvoDictT) -> None: ", (has the system configuration been changed?)" ) - @property - def activeFaults(self) -> _EvoListT | None: - return self._status.get(SZ_ACTIVE_FAULTS) - @property def systemModeStatus(self) -> _EvoDictT | None: return self._status.get(SZ_SYSTEM_MODE_STATUS) @@ -214,7 +210,7 @@ def system_mode(self) -> str | None: async def _set_mode(self, mode: dict[str, str | bool]) -> None: """Set the TCS mode.""" # {'mode': 'Auto', 'isPermanent': True} - _ = await self._broker.put(f"{self.TYPE}/{self._id}/mode", json=mode) + _ = await self._broker.put(f"{self.TYPE}/{self.id}/mode", json=mode) async def reset_mode(self) -> None: """Set the TCS to auto mode (and DHW/all zones to FollowSchedule mode).""" @@ -277,7 +273,7 @@ async def temperatures(self) -> _EvoListT: if dhw := self.hotwater: dhw_status = { SZ_THERMOSTAT: "DOMESTIC_HOT_WATER", - SZ_ID: dhw.dhwId, + SZ_ID: dhw.id, SZ_NAME: dhw.name, SZ_TEMP: None, } @@ -293,7 +289,7 @@ async def temperatures(self) -> _EvoListT: for zone in self._zones: zone_status = { SZ_THERMOSTAT: "EMEA_ZONE", - SZ_ID: zone.zoneId, + SZ_ID: zone.id, SZ_NAME: zone.name, SZ_SETPOINT: None, SZ_TEMP: None, @@ -322,24 +318,24 @@ async def get_schedule(child: HotWater | Zone) -> _ScheduleT: return await child.get_schedule() except exc.InvalidSchedule: self._logger.warning( - f"Ignoring schedule of {child._id} ({child.name}): missing/invalid" + f"Ignoring schedule of {child.id} ({child.name}): missing/invalid" ) return {} self._logger.info( - f"Schedules: Backing up from {self.systemId} ({self.location.name})" + f"Schedules: Backing up from {self.id} ({self.location.name})" ) schedules = {} for zone in self._zones: - schedules[zone.zoneId] = { + schedules[zone.id] = { SZ_NAME: zone.name, SZ_SCHEDULE: await get_schedule(zone), } if self.hotwater: - schedules[self.hotwater.dhwId] = { + schedules[self.hotwater.id] = { SZ_NAME: self.hotwater.name, SZ_SCHEDULE: await get_schedule(self.hotwater), } @@ -354,12 +350,12 @@ async def set_schedules( The default is to match a schedule to its zone/dhw by id. """ - async def restore_by_id(id: _ZoneIdT | _DhwIdT, schedule: _ScheduleT) -> bool: + async def restore_by_id(id: str, schedule: _ScheduleT) -> bool: """Restore schedule by id and return False if there was no match.""" name = schedule.get(SZ_NAME) - if self.hotwater and self.hotwater.dhwId == id: + if self.hotwater and self.hotwater.id == id: await self.hotwater.set_schedule(json.dumps(schedule[SZ_SCHEDULE])) elif zone := self.zones_by_id.get(id): @@ -374,7 +370,7 @@ async def restore_by_id(id: _ZoneIdT | _DhwIdT, schedule: _ScheduleT) -> bool: return True - async def restore_by_name(id: _ZoneIdT | _DhwIdT, schedule: _ScheduleT) -> bool: + async def restore_by_name(id: str, schedule: _ScheduleT) -> bool: """Restore schedule by name and return False if there was no match.""" name: str = schedule[SZ_NAME] # type: ignore[assignment] @@ -396,7 +392,7 @@ async def restore_by_name(id: _ZoneIdT | _DhwIdT, schedule: _ScheduleT) -> bool: self._logger.info( f"Schedules: Restoring (matched by {'name' if match_by_name else 'id'})" - f" to {self.systemId} ({self.location.name})" + f" to {self.id} ({self.location.name})" ) with_errors = False diff --git a/src/evohomeasync2/exceptions.py b/src/evohomeasync2/exceptions.py index f42b62eb..9b877dc8 100644 --- a/src/evohomeasync2/exceptions.py +++ b/src/evohomeasync2/exceptions.py @@ -21,7 +21,7 @@ class DeprecationError(EvohomeBaseError): class InvalidSchema(EvohomeError): - """The config/status JSON is invalid (e.g. missing an entity Id).""" + """The config/status JSON is invalid (e.g. missing an entity id).""" class InvalidParameter(EvohomeError): diff --git a/src/evohomeasync2/gateway.py b/src/evohomeasync2/gateway.py index ded7b80b..7a465b06 100644 --- a/src/evohomeasync2/gateway.py +++ b/src/evohomeasync2/gateway.py @@ -3,18 +3,21 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING, Final, NoReturn +import voluptuous as vol + +from . import exceptions as exc from .controlsystem import ControlSystem from .schema import SCH_GWY_STATUS from .schema.const import ( - SZ_GATEWAY, SZ_GATEWAY_ID, SZ_GATEWAY_INFO, SZ_IS_WI_FI, SZ_MAC, SZ_SYSTEM_ID, SZ_TEMPERATURE_CONTROL_SYSTEMS, + EntityType, ) from .zone import ActiveFaultsBase @@ -22,25 +25,34 @@ import voluptuous as vol from . import Location - from .schema import _EvoDictT, _GatewayIdT + from .schema import _EvoDictT + +class _GatewayDeprecated: # pragma: no cover + """Deprecated attributes and methods removed from the evohome-client namespace.""" + + @property + def gatewayId(self) -> NoReturn: + raise exc.DeprecationError(f"{self}: .gatewayId is deprecated, use .id") -class Gateway(ActiveFaultsBase): + +class Gateway(_GatewayDeprecated, ActiveFaultsBase): """Instance of a location's gateway.""" STATUS_SCHEMA: Final[vol.Schema] = SCH_GWY_STATUS - TYPE: Final = SZ_GATEWAY # type: ignore[misc] + TYPE: Final = EntityType.GWY # type: ignore[misc] def __init__(self, location: Location, config: _EvoDictT) -> None: super().__init__( config[SZ_GATEWAY_INFO][SZ_GATEWAY_ID], location._broker, location._logger ) - self.location = location + self.location = location # parent self._config: Final[_EvoDictT] = config[SZ_GATEWAY_INFO] self._status: _EvoDictT = {} + # children self._control_systems: list[ControlSystem] = [] self.control_systems: dict[str, ControlSystem] = {} # tcs by id @@ -49,11 +61,7 @@ def __init__(self, location: Location, config: _EvoDictT) -> None: tcs = ControlSystem(self, tcs_config) self._control_systems.append(tcs) - self.control_systems[tcs.systemId] = tcs - - @property - def gatewayId(self) -> _GatewayIdT: - return self._id + self.control_systems[tcs.id] = tcs @property def mac(self) -> str: diff --git a/src/evohomeasync2/hotwater.py b/src/evohomeasync2/hotwater.py index 8ea2180e..21fdf830 100644 --- a/src/evohomeasync2/hotwater.py +++ b/src/evohomeasync2/hotwater.py @@ -15,13 +15,13 @@ SZ_ALLOWED_MODES, SZ_DHW_ID, SZ_DHW_STATE_CAPABILITIES_RESPONSE, - SZ_DOMESTIC_HOT_WATER, SZ_MODE, SZ_SCHEDULE_CAPABILITIES_RESPONSE, SZ_STATE, SZ_STATE_STATUS, SZ_UNTIL_TIME, DhwState, + EntityType, ZoneMode, ) from .zone import _ZoneBase @@ -30,17 +30,15 @@ import voluptuous as vol from . import ControlSystem - from .schema import _DhwIdT, _EvoDictT, _EvoListT + from .schema import _EvoDictT, _EvoListT class HotWaterDeprecated: # pragma: no cover """Deprecated attributes and methods removed from the evohome-client namespace.""" @property - def zoneId(self) -> NoReturn: - raise exc.DeprecationError( - f"{self}: .zoneId is deprecated, use .dhwId (or ._id)" - ) + def dhwId(self) -> NoReturn: + raise exc.DeprecationError(f"{self}: .dhwId is deprecated, use .id") async def get_dhw_state(self, *args, **kwargs) -> NoReturn: # type: ignore[no-untyped-def] raise exc.DeprecationError( @@ -67,7 +65,7 @@ class HotWater(HotWaterDeprecated, _ZoneBase): """Instance of a TCS's DHW zone (domesticHotWater).""" STATUS_SCHEMA: Final = SCH_DHW_STATUS # type: ignore[misc] - TYPE: Final = SZ_DOMESTIC_HOT_WATER # type: ignore[misc] + TYPE: Final = EntityType.DHW # type: ignore[misc] SCH_SCHEDULE_GET: Final[vol.Schema] = SCH_GET_SCHEDULE_DHW # type: ignore[misc] SCH_SCHEDULE_PUT: Final[vol.Schema] = SCH_PUT_SCHEDULE_DHW # type: ignore[misc] @@ -75,10 +73,6 @@ class HotWater(HotWaterDeprecated, _ZoneBase): def __init__(self, tcs: ControlSystem, config: _EvoDictT) -> None: super().__init__(config[SZ_DHW_ID], tcs, config) - @property - def dhwId(self) -> _DhwIdT: - return self._id - @property def dhwStateCapabilitiesResponse(self) -> _EvoDictT: ret: _EvoDictT = self._config[SZ_DHW_STATE_CAPABILITIES_RESPONSE] @@ -122,7 +116,7 @@ def _next_setpoint(self) -> tuple[dt, str] | None: # WIP: for convenience (new) async def _set_mode(self, mode: dict[str, str | None]) -> None: """Set the DHW mode (state).""" - _ = await self._broker.put(f"{self.TYPE}/{self._id}/state", json=mode) + _ = await self._broker.put(f"{self.TYPE}/{self.id}/state", json=mode) async def reset_mode(self) -> None: """Cancel any override and allow the DHW to follow its schedule.""" diff --git a/src/evohomeasync2/location.py b/src/evohomeasync2/location.py index e86f9596..e50533f7 100644 --- a/src/evohomeasync2/location.py +++ b/src/evohomeasync2/location.py @@ -12,7 +12,6 @@ SZ_COUNTRY, SZ_GATEWAY_ID, SZ_GATEWAYS, - SZ_LOCATION, SZ_LOCATION_ID, SZ_LOCATION_INFO, SZ_LOCATION_OWNER, @@ -20,43 +19,47 @@ SZ_NAME, SZ_TIME_ZONE, SZ_USE_DAYLIGHT_SAVE_SWITCHING, + EntityType, ) +from .zone import EntityBase if TYPE_CHECKING: - import logging - import voluptuous as vol - from . import Broker, EvohomeClient - from .schema import _EvoDictT, _LocationIdT + from .base import EvohomeClient + from .schema import _EvoDictT class _LocationDeprecated: # pragma: no cover """Deprecated attributes and methods removed from the evohome-client namespace.""" + @property + def locationId(self) -> NoReturn: + raise exc.DeprecationError(f"{self}: .locationId is deprecated, use .id") + async def status(self, *args, **kwargs) -> NoReturn: # type: ignore[no-untyped-def] raise exc.DeprecationError( f"{self}: .status() is deprecated, use .refresh_status()" ) -class Location(_LocationDeprecated): +class Location(_LocationDeprecated, EntityBase): """Instance of an account's location.""" STATUS_SCHEMA: Final[vol.Schema] = SCH_LOCN_STATUS - TYPE: Final = SZ_LOCATION + TYPE: Final = EntityType.LOC # type: ignore[misc] def __init__(self, client: EvohomeClient, config: _EvoDictT) -> None: - self.client = client - - self._broker: Broker = client.broker - self._logger: logging.Logger = client._logger + super().__init__( + config[SZ_LOCATION_INFO][SZ_LOCATION_ID], client.broker, client._logger + ) - self._id: Final[_LocationIdT] = config[SZ_LOCATION_INFO][SZ_LOCATION_ID] + self.client = client # proxy for parent self._config: Final[_EvoDictT] = config[SZ_LOCATION_INFO] self._status: _EvoDictT = {} + # children self._gateways: list[Gateway] = [] self.gateways: dict[str, Gateway] = {} # gwy by id @@ -65,14 +68,7 @@ def __init__(self, client: EvohomeClient, config: _EvoDictT) -> None: gwy = Gateway(self, gwy_config) self._gateways.append(gwy) - self.gateways[gwy.gatewayId] = gwy - - def __str__(self) -> str: - return f"{self.__class__.__name__}(id='{self._id}')" - - @property - def locationId(self) -> _LocationIdT: - return self._id + self.gateways[gwy.id] = gwy @property def country(self) -> str: @@ -108,7 +104,7 @@ async def refresh_status(self) -> _EvoDictT: """Update the entire Location with its latest status (returns the status).""" status: _EvoDictT = await self._broker.get( - f"{self.TYPE}/{self._id}/status?includeTemperatureControlSystems=True", + f"{self.TYPE}/{self.id}/status?includeTemperatureControlSystems=True", schema=self.STATUS_SCHEMA, ) # type: ignore[assignment] diff --git a/src/evohomeasync2/schema/__init__.py b/src/evohomeasync2/schema/__init__.py index 81cda7d7..856bda90 100644 --- a/src/evohomeasync2/schema/__init__.py +++ b/src/evohomeasync2/schema/__init__.py @@ -41,16 +41,4 @@ SCH_TEMPERATURE_CONTROL_SYSTEM as SCH_TCS_STATUS, SCH_ZONE as SCH_ZONE_STATUS, ) -from .typing import ( # noqa: F401 - _DhwIdT, - _EvoDictT, - _EvoLeafT, - _EvoListT, - _EvoSchemaT, - _GatewayIdT, - _LocationIdT, - _ModeT, - _SystemIdT, - _UserIdT, - _ZoneIdT, -) +from .typing import _EvoDictT, _EvoLeafT, _EvoListT, _EvoSchemaT, _ModeT # noqa: F401 diff --git a/src/evohomeasync2/schema/const.py b/src/evohomeasync2/schema/const.py index e43b981b..b0065da4 100644 --- a/src/evohomeasync2/schema/const.py +++ b/src/evohomeasync2/schema/const.py @@ -182,96 +182,105 @@ @verify(EnumCheck.UNIQUE) class DhwState(StrEnum): - OFF: Final = SZ_OFF - ON: Final = SZ_ON + OFF = SZ_OFF + ON = SZ_ON @verify(EnumCheck.UNIQUE) class FanMode(StrEnum): - AUTO: Final = SZ_AUTO - ON: Final = SZ_ON + AUTO = SZ_AUTO + ON = SZ_ON @verify(EnumCheck.UNIQUE) class FaultType(StrEnum): # NOTE: This list is incomplete - SYS_B_CL: Final = "BoilerCommunicationLost" - SYS_C_CL: Final = "ChValveCommunicationLost" - DHW_A_FL: Final = "DHWActuatorFailure" - # W_A_CL: Final = "DHWActuatorCommunicationLost" # extrapolated - DHW_S_CL: Final = "DHWSensorCommunicationLost" - DHW_S_FL: Final = "DHWSensorFailure" - DHW_S_LB: Final = "DHWSensorLowBattery" # extrapolated - GWY_X_CL: Final = "GatewayCommunicationLost" - # S_X_LB: Final = "TemperatureControlSystemLowBattery" # extrapolated - ZON_A_CL: Final = "TempZoneActuatorCommunicationLost" - ZON_A_LB: Final = "TempZoneActuatorLowBattery" - ZON_S_CL: Final = "TempZoneSensorCommunicationLost" - ZON_S_LB: Final = "TempZoneSensorLowBattery" + SYS_B_CL = "BoilerCommunicationLost" + SYS_C_CL = "ChValveCommunicationLost" + DHW_A_FL = "DHWActuatorFailure" + # W_A_CL = "DHWActuatorCommunicationLost" # extrapolated + DHW_S_CL = "DHWSensorCommunicationLost" + DHW_S_FL = "DHWSensorFailure" + DHW_S_LB = "DHWSensorLowBattery" # extrapolated + GWY_X_CL = "GatewayCommunicationLost" + # S_X_LB = "TemperatureControlSystemLowBattery" # extrapolated + ZON_A_CL = "TempZoneActuatorCommunicationLost" + ZON_A_LB = "TempZoneActuatorLowBattery" + ZON_S_CL = "TempZoneSensorCommunicationLost" + ZON_S_LB = "TempZoneSensorLowBattery" @verify(EnumCheck.UNIQUE) class SystemMode(StrEnum): - AUTO: Final = SZ_AUTO - AUTO_WITH_ECO: Final = SZ_AUTO_WITH_ECO - AUTO_WITH_RESET: Final = SZ_AUTO_WITH_RESET - AWAY: Final = SZ_AWAY - CUSTOM: Final = SZ_CUSTOM - DAY_OFF: Final = SZ_DAY_OFF - HEATING_OFF: Final = SZ_HEATING_OFF - OFF: Final = SZ_OFF # not evohome (VisionProWifiRetail) - HEAT: Final = SZ_HEAT # not evohome (VisionProWifiRetail) - COOL: Final = SZ_COOL # not evohome (VisionProWifiRetail) + AUTO = SZ_AUTO + AUTO_WITH_ECO = SZ_AUTO_WITH_ECO + AUTO_WITH_RESET = SZ_AUTO_WITH_RESET + AWAY = SZ_AWAY + CUSTOM = SZ_CUSTOM + DAY_OFF = SZ_DAY_OFF + HEATING_OFF = SZ_HEATING_OFF + OFF = SZ_OFF # not evohome (VisionProWifiRetail) + HEAT = SZ_HEAT # not evohome (VisionProWifiRetail) + COOL = SZ_COOL # not evohome (VisionProWifiRetail) + + +@verify(EnumCheck.UNIQUE) +class EntityType(StrEnum): + LOC = SZ_LOCATION + GWY = SZ_GATEWAY + TCS = SZ_TEMPERATURE_CONTROL_SYSTEM + ZON = SZ_TEMPERATURE_ZONE + DHW = SZ_DOMESTIC_HOT_WATER @verify(EnumCheck.UNIQUE) class TcsModelType(StrEnum): - EVO_TOUCH: Final = "EvoTouch" - FOCUS_PRO_WIFI_RETAIL: Final = "FocusProWifiRetail" - VISION_PRO_WIFI_RETAIL: Final = "VisionProWifiRetail" + EVO_TOUCH = "EvoTouch" + FOCUS_PRO_WIFI_RETAIL = "FocusProWifiRetail" + VISION_PRO_WIFI_RETAIL = "VisionProWifiRetail" -SZ_FOLLOW_SCHEDULE: Final = "FollowSchedule" -SZ_PERMANENT_OVERRIDE: Final = "PermanentOverride" -SZ_TEMPORARY_OVERRIDE: Final = "TemporaryOverride" -SZ_VACATION_HOLD: Final = "VacationHold" +SZ_FOLLOW_SCHEDULE = "FollowSchedule" +SZ_PERMANENT_OVERRIDE = "PermanentOverride" +SZ_TEMPORARY_OVERRIDE = "TemporaryOverride" +SZ_VACATION_HOLD = "VacationHold" @verify(EnumCheck.UNIQUE) class ZoneMode(StrEnum): - FOLLOW_SCHEDULE: Final = SZ_FOLLOW_SCHEDULE - PERMANENT_OVERRIDE: Final = SZ_PERMANENT_OVERRIDE - TEMPORARY_OVERRIDE: Final = SZ_TEMPORARY_OVERRIDE - VACATION_HOLD: Final = SZ_VACATION_HOLD # not evohome (VisionProWifiRetail) + FOLLOW_SCHEDULE = SZ_FOLLOW_SCHEDULE + PERMANENT_OVERRIDE = SZ_PERMANENT_OVERRIDE + TEMPORARY_OVERRIDE = SZ_TEMPORARY_OVERRIDE + VACATION_HOLD = SZ_VACATION_HOLD # not evohome (VisionProWifiRetail) @verify(EnumCheck.UNIQUE) class ZoneModelType(StrEnum): - FOCUS_PRO_WIFI_RETAIL: Final = "FocusProWifiRetail" - HEATING_ZONE: Final = "HeatingZone" - ROUND_MODULATION: Final = "RoundModulation" - ROUND_WIRELESS: Final = "RoundWireless" - UNKNOWN: Final = SZ_UNKNOWN - VISION_PRO_WIFI_RETAIL: Final = "VisionProWifiRetail" + FOCUS_PRO_WIFI_RETAIL = "FocusProWifiRetail" + HEATING_ZONE = "HeatingZone" + ROUND_MODULATION = "RoundModulation" + ROUND_WIRELESS = "RoundWireless" + UNKNOWN = SZ_UNKNOWN + VISION_PRO_WIFI_RETAIL = "VisionProWifiRetail" -SZ_ELECTRIC_HEAT: Final = "ElectricHeat" # TODO: needs confirming -SZ_MIXING_VALVE: Final = "MixingValve" -SZ_RADIATOR_ZONE: Final = "RadiatorZone" -SZ_THERMOSTAT: Final = "Thermostat" -SZ_UNDERFLOOR_HEATING: Final = "UnderfloorHeating" -SZ_ZONE_VALVES: Final = "ZoneValves" # is not ZoneValve -SZ_ZONE_TEMPERATURE_CONTROL: Final = "ZoneTemperatureControl" +SZ_ELECTRIC_HEAT = "ElectricHeat" # TODO: needs confirming +SZ_MIXING_VALVE = "MixingValve" +SZ_RADIATOR_ZONE = "RadiatorZone" +SZ_THERMOSTAT = "Thermostat" +SZ_UNDERFLOOR_HEATING = "UnderfloorHeating" +SZ_ZONE_VALVES = "ZoneValves" # is not ZoneValve +SZ_ZONE_TEMPERATURE_CONTROL = "ZoneTemperatureControl" @verify(EnumCheck.UNIQUE) class ZoneType(StrEnum): - MIXING_VALVE: Final = SZ_MIXING_VALVE - RADIATOR_ZONE: Final = SZ_RADIATOR_ZONE - THERMOSTAT: Final = SZ_THERMOSTAT - UNDERFLOOR_HEATING: Final = SZ_UNDERFLOOR_HEATING - UNKNOWN: Final = SZ_UNKNOWN - ZONE_TEMPERATURE_CONTROL: Final = SZ_ZONE_TEMPERATURE_CONTROL - ZONE_VALVES: Final = SZ_ZONE_VALVES + MIXING_VALVE = SZ_MIXING_VALVE + RADIATOR_ZONE = SZ_RADIATOR_ZONE + THERMOSTAT = SZ_THERMOSTAT + UNDERFLOOR_HEATING = SZ_UNDERFLOOR_HEATING + UNKNOWN = SZ_UNKNOWN + ZONE_TEMPERATURE_CONTROL = SZ_ZONE_TEMPERATURE_CONTROL + ZONE_VALVES = SZ_ZONE_VALVES # these may not be required with Python 3.12+ (used for 'if mode in ZONE_MODES'...) diff --git a/src/evohomeasync2/schema/schedule.py b/src/evohomeasync2/schema/schedule.py index 21aee705..917fea62 100644 --- a/src/evohomeasync2/schema/schedule.py +++ b/src/evohomeasync2/schema/schedule.py @@ -74,7 +74,7 @@ extra=vol.PREVENT_EXTRA, ) -SCH_GET_SCHEDULE: Final = vol.Schema( # PUT /{self.TYPE}/{self._id}/schedule +SCH_GET_SCHEDULE: Final = vol.Schema( # PUT /{self.TYPE}/{self.id}/schedule vol.Any(SCH_GET_SCHEDULE_DHW, SCH_GET_SCHEDULE_ZONE), extra=vol.PREVENT_EXTRA, ) @@ -133,7 +133,7 @@ extra=vol.PREVENT_EXTRA, ) -SCH_PUT_SCHEDULE: Final = vol.Schema( # PUT /{self.TYPE}/{self._id}/schedule +SCH_PUT_SCHEDULE: Final = vol.Schema( # PUT /{self.TYPE}/{self.id}/schedule vol.Any(SCH_PUT_SCHEDULE_DHW, SCH_PUT_SCHEDULE_ZONE), extra=vol.PREVENT_EXTRA, ) diff --git a/src/evohomeasync2/schema/typing.py b/src/evohomeasync2/schema/typing.py index 17f02a8d..ab5a8212 100644 --- a/src/evohomeasync2/schema/typing.py +++ b/src/evohomeasync2/schema/typing.py @@ -9,13 +9,5 @@ _EvoListT = list[_EvoDictT] _EvoSchemaT = _EvoDictT | _EvoListT -# TCC identifierss = strings of digits -_DhwIdT = str -_GatewayIdT = str -_LocationIdT = str -_SystemIdT = str -_UserIdT = str -_ZoneIdT = str - # TCC other _ModeT = str diff --git a/src/evohomeasync2/zone.py b/src/evohomeasync2/zone.py index 3f8d1523..245789f4 100644 --- a/src/evohomeasync2/zone.py +++ b/src/evohomeasync2/zone.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Provides handling of TCC zones (heating and DHW).""" -# TODO: add provision for cooling zones +# TODO: add provision for cooling zones, when vendor's API adds support for such # TODO: add set_mode() for non-evohome modes (e.g. "VacationHold") from __future__ import annotations @@ -40,12 +40,12 @@ SZ_TARGET_HEAT_TEMPERATURE, SZ_TEMPERATURE, SZ_TEMPERATURE_STATUS, - SZ_TEMPERATURE_ZONE, SZ_TIME_UNTIL, SZ_ZONE_ID, SZ_ZONE_TYPE, ZONE_MODEL_TYPES, ZONE_TYPES, + EntityType, ZoneModelType, ZoneType, ) @@ -54,29 +54,36 @@ import logging from . import Broker, ControlSystem - from .schema import _DhwIdT, _EvoDictT, _EvoListT, _ZoneIdT + from .schema import _EvoDictT, _EvoListT _ONE_DAY = td(days=1) -class ActiveFaultsBase: - TYPE: _DhwIdT | _ZoneIdT # "temperatureZone", "domesticHotWater" +class EntityBase: + TYPE: EntityType # e.g. "temperatureControlSystem", "domesticHotWater" - def __init__( - self, id: _DhwIdT | _ZoneIdT, broker: Broker, logger: logging.Logger - ) -> None: + def __init__(self, id: str, broker: Broker, logger: logging.Logger) -> None: self._id: Final = id self._broker = broker self._logger = logger + def __str__(self) -> str: + return f"{self.__class__.__name__}(id='{self.id}')" + + @property + def id(self) -> str: + return self._id + + +class ActiveFaultsBase(EntityBase): + def __init__(self, id: str, broker: Broker, logger: logging.Logger) -> None: + super().__init__(id, broker, logger) + self._active_faults: _EvoListT = [] self._last_logged: dict[str, dt] = {} - def __str__(self) -> str: - return f"{self.__class__.__name__}(id='{self._id}')" - @property def active_faults(self) -> _EvoListT: return self._active_faults @@ -121,6 +128,16 @@ def log_as_resolved(fault: _EvoDictT) -> None: class _ZoneBaseDeprecated: # pragma: no cover """Deprecated attributes and methods removed from the evohome-client namespace.""" + @property + def zoneId(self) -> NoReturn: + raise exc.DeprecationError(f"{self}: .zoneId is deprecated, use .id") + + @property + def activeFaults(self) -> NoReturn: + raise exc.DeprecationError( + f"{self}: .activeFaults is deprecated, use .active_faults" + ) + @property def zone_type(self) -> NoReturn: raise exc.DeprecationError(f"{self}: .zone_type is deprecated, use .TYPE") @@ -142,7 +159,7 @@ class _ZoneBase(ActiveFaultsBase, _ZoneBaseDeprecated): def __init__(self, id: str, tcs: ControlSystem, config: _EvoDictT) -> None: super().__init__(id, tcs._broker, tcs._logger) - self.tcs = tcs + self.tcs = tcs # parent self._config: Final[_EvoDictT] = config self._schedule: _EvoDictT = {} @@ -156,7 +173,7 @@ async def _refresh_status(self) -> _EvoDictT: """ status: _EvoDictT = await self._broker.get( - f"{self.TYPE}/{self._id}/status", schema=self.STATUS_SCHEMA + f"{self.TYPE}/{self.id}/status", schema=self.STATUS_SCHEMA ) # type: ignore[assignment] self._update_status(status) @@ -167,10 +184,6 @@ def _update_status(self, status: _EvoDictT) -> None: self._status = status - @property - def activeFaults(self) -> _EvoListT | None: - return self._status.get(SZ_ACTIVE_FAULTS) - @property def temperatureStatus(self) -> _EvoDictT | None: return self._status.get(SZ_TEMPERATURE_STATUS) @@ -190,7 +203,7 @@ async def get_schedule(self) -> _EvoDictT: try: schedule: _EvoDictT = await self._broker.get( - f"{self.TYPE}/{self._id}/schedule", schema=self.SCH_SCHEDULE_GET + f"{self.TYPE}/{self.id}/schedule", schema=self.SCH_SCHEDULE_GET ) # type: ignore[assignment] except exc.RequestFailed as err: @@ -233,7 +246,7 @@ async def set_schedule(self, schedule: _EvoDictT | str) -> None: assert isinstance(schedule, dict) # mypy check _ = await self._broker.put( - f"{self.TYPE}/{self._id}/schedule", + f"{self.TYPE}/{self.id}/schedule", json=schedule, schema=self.SCH_SCHEDULE_PUT, ) @@ -254,7 +267,7 @@ class Zone(_ZoneDeprecated, _ZoneBase): """Instance of a TCS's heating zone (temperatureZone).""" STATUS_SCHEMA: Final = SCH_ZONE_STATUS # type: ignore[misc] - TYPE: Final = SZ_TEMPERATURE_ZONE # type: ignore[misc] + TYPE: Final = EntityType.ZON # type: ignore[misc] SCH_SCHEDULE_GET: Final = SCH_GET_SCHEDULE_ZONE # type: ignore[misc] SCH_SCHEDULE_PUT: Final = SCH_PUT_SCHEDULE_ZONE # type: ignore[misc] @@ -280,10 +293,6 @@ def __init__(self, tcs: ControlSystem, config: _EvoDictT) -> None: "%s: Unknown zone type '%s' (YMMV)", self, self.zoneType ) - @property - def zoneId(self) -> _ZoneIdT: - return self._id - @property def modelType(self) -> str: ret: str = self._config[SZ_MODEL_TYPE] @@ -357,7 +366,7 @@ def target_heat_temperature(self) -> float | None: async def _set_mode(self, mode: dict[str, str | float]) -> None: """Set the zone mode (heat_setpoint, cooling is TBD).""" # TODO: also coolSetpoint - _ = await self._broker.put(f"{self.TYPE}/{self._id}/heatSetpoint", json=mode) + _ = await self._broker.put(f"{self.TYPE}/{self.id}/heatSetpoint", json=mode) async def reset_mode(self) -> None: """Cancel any override and allow the zone to follow its schedule""" diff --git a/tests/tests/__snapshots__/test_installs.ambr b/tests/tests/__snapshots__/test_installs.ambr index 6eb4be1b..fb6b944e 100644 --- a/tests/tests/__snapshots__/test_installs.ambr +++ b/tests/tests/__snapshots__/test_installs.ambr @@ -1,8 +1,7 @@ # serializer version: 1 # name: test_system_snapshot[default][control_system] ''' - activeFaults: &id001 [] - active_faults: *id001 + active_faults: [] allowedSystemModes: - canBePermanent: true canBeTemporary: false @@ -37,8 +36,8 @@ systemMode: Custom timingMode: Period timingResolution: 1.00:00:00 + id: '3432522' modelType: EvoTouch - systemId: '3432522' systemModeStatus: isPermanent: true mode: AutoWithEco @@ -49,7 +48,7 @@ # name: test_system_snapshot[default][gateway] ''' active_faults: [] - gatewayId: '2499896' + id: '2499896' isWiFi: false mac: 00D02DEE4E56 @@ -57,20 +56,19 @@ # --- # name: test_system_snapshot[default][hot_water] ''' - activeFaults: &id001 [] - active_faults: *id001 - allowedModes: &id002 + active_faults: [] + allowedModes: &id001 - FollowSchedule - PermanentOverride - TemporaryOverride - dhwId: '3933910' dhwStateCapabilitiesResponse: - allowedModes: *id002 + allowedModes: *id001 allowedStates: - 'On' - 'Off' maxDuration: 1.00:00:00 timingResolution: 00:10:00 + id: '3933910' mode: PermanentOverride name: Domestic Hot Water scheduleCapabilitiesResponse: @@ -91,7 +89,7 @@ # name: test_system_snapshot[default][location] ''' country: UnitedKingdom - locationId: '2738909' + id: '2738909' locationOwner: firstname: David lastname: '********' @@ -112,12 +110,12 @@ # name: test_system_snapshot[default][zones] ''' '3432521': - activeFaults: &id001 [] - active_faults: *id001 - allowedSetpointModes: &id002 + active_faults: [] + allowedSetpointModes: &id001 - PermanentOverride - FollowSchedule - TemporaryOverride + id: '3432521' max_heat_setpoint: 35.0 min_heat_setpoint: 5.0 mode: PermanentOverride @@ -129,7 +127,7 @@ setpointValueResolution: 0.5 timingResolution: 00:10:00 setpointCapabilities: - allowedSetpointModes: *id002 + allowedSetpointModes: *id001 canControlCool: false canControlHeat: true maxDuration: 1.00:00:00 @@ -147,12 +145,12 @@ isAvailable: false zoneType: RadiatorZone '3432576': - activeFaults: &id003 [] - active_faults: *id003 - allowedSetpointModes: &id004 + active_faults: [] + allowedSetpointModes: &id002 - PermanentOverride - FollowSchedule - TemporaryOverride + id: '3432576' max_heat_setpoint: 35.0 min_heat_setpoint: 5.0 mode: FollowSchedule @@ -164,7 +162,7 @@ setpointValueResolution: 0.5 timingResolution: 00:10:00 setpointCapabilities: - allowedSetpointModes: *id004 + allowedSetpointModes: *id002 canControlCool: false canControlHeat: true maxDuration: 1.00:00:00 @@ -183,12 +181,12 @@ temperature: 19.0 zoneType: RadiatorZone '3432577': - activeFaults: &id005 [] - active_faults: *id005 - allowedSetpointModes: &id006 + active_faults: [] + allowedSetpointModes: &id003 - PermanentOverride - FollowSchedule - TemporaryOverride + id: '3432577' max_heat_setpoint: 35.0 min_heat_setpoint: 5.0 mode: FollowSchedule @@ -200,7 +198,7 @@ setpointValueResolution: 0.5 timingResolution: 00:10:00 setpointCapabilities: - allowedSetpointModes: *id006 + allowedSetpointModes: *id003 canControlCool: false canControlHeat: true maxDuration: 1.00:00:00 @@ -219,12 +217,12 @@ temperature: 19.0 zoneType: RadiatorZone '3432578': - activeFaults: &id007 [] - active_faults: *id007 - allowedSetpointModes: &id008 + active_faults: [] + allowedSetpointModes: &id004 - PermanentOverride - FollowSchedule - TemporaryOverride + id: '3432578' max_heat_setpoint: 35.0 min_heat_setpoint: 5.0 mode: FollowSchedule @@ -236,7 +234,7 @@ setpointValueResolution: 0.5 timingResolution: 00:10:00 setpointCapabilities: - allowedSetpointModes: *id008 + allowedSetpointModes: *id004 canControlCool: false canControlHeat: true maxDuration: 1.00:00:00 @@ -255,12 +253,12 @@ temperature: 20.0 zoneType: RadiatorZone '3432579': - activeFaults: &id009 [] - active_faults: *id009 - allowedSetpointModes: &id010 + active_faults: [] + allowedSetpointModes: &id005 - PermanentOverride - FollowSchedule - TemporaryOverride + id: '3432579' max_heat_setpoint: 35.0 min_heat_setpoint: 5.0 mode: FollowSchedule @@ -272,7 +270,7 @@ setpointValueResolution: 0.5 timingResolution: 00:10:00 setpointCapabilities: - allowedSetpointModes: *id010 + allowedSetpointModes: *id005 canControlCool: false canControlHeat: true maxDuration: 1.00:00:00 @@ -291,12 +289,12 @@ temperature: 20.0 zoneType: RadiatorZone '3432580': - activeFaults: &id011 [] - active_faults: *id011 - allowedSetpointModes: &id012 + active_faults: [] + allowedSetpointModes: &id006 - PermanentOverride - FollowSchedule - TemporaryOverride + id: '3432580' max_heat_setpoint: 35.0 min_heat_setpoint: 5.0 mode: FollowSchedule @@ -308,7 +306,7 @@ setpointValueResolution: 0.5 timingResolution: 00:10:00 setpointCapabilities: - allowedSetpointModes: *id012 + allowedSetpointModes: *id006 canControlCool: false canControlHeat: true maxDuration: 1.00:00:00 @@ -327,12 +325,12 @@ temperature: 21.0 zoneType: RadiatorZone '3449703': - activeFaults: &id013 [] - active_faults: *id013 - allowedSetpointModes: &id014 + active_faults: [] + allowedSetpointModes: &id007 - PermanentOverride - FollowSchedule - TemporaryOverride + id: '3449703' max_heat_setpoint: 35.0 min_heat_setpoint: 5.0 mode: FollowSchedule @@ -344,7 +342,7 @@ setpointValueResolution: 0.5 timingResolution: 00:10:00 setpointCapabilities: - allowedSetpointModes: *id014 + allowedSetpointModes: *id007 canControlCool: false canControlHeat: true maxDuration: 1.00:00:00 @@ -363,12 +361,12 @@ temperature: 19.5 zoneType: RadiatorZone '3449740': - activeFaults: &id015 [] - active_faults: *id015 - allowedSetpointModes: &id016 + active_faults: [] + allowedSetpointModes: &id008 - PermanentOverride - FollowSchedule - TemporaryOverride + id: '3449740' max_heat_setpoint: 35.0 min_heat_setpoint: 5.0 mode: FollowSchedule @@ -380,7 +378,7 @@ setpointValueResolution: 0.5 timingResolution: 00:10:00 setpointCapabilities: - allowedSetpointModes: *id016 + allowedSetpointModes: *id008 canControlCool: false canControlHeat: true maxDuration: 1.00:00:00 @@ -399,12 +397,12 @@ temperature: 21.5 zoneType: RadiatorZone '3450733': - activeFaults: &id017 [] - active_faults: *id017 - allowedSetpointModes: &id018 + active_faults: [] + allowedSetpointModes: &id009 - PermanentOverride - FollowSchedule - TemporaryOverride + id: '3450733' max_heat_setpoint: 35.0 min_heat_setpoint: 5.0 mode: PermanentOverride @@ -416,7 +414,7 @@ setpointValueResolution: 0.5 timingResolution: 00:10:00 setpointCapabilities: - allowedSetpointModes: *id018 + allowedSetpointModes: *id009 canControlCool: false canControlHeat: true maxDuration: 1.00:00:00 @@ -439,8 +437,7 @@ # --- # name: test_system_snapshot[hass_118169][control_system] ''' - activeFaults: &id001 [] - active_faults: *id001 + active_faults: [] allowedSystemModes: - canBePermanent: true canBeTemporary: false @@ -475,8 +472,8 @@ systemMode: Custom timingMode: Period timingResolution: 1.00:00:00 + id: '3432522' modelType: EvoTouch - systemId: '3432522' systemModeStatus: isPermanent: true mode: AutoWithEco @@ -487,7 +484,7 @@ # name: test_system_snapshot[hass_118169][gateway] ''' active_faults: [] - gatewayId: '2499896' + id: '2499896' isWiFi: false mac: 00D02DEE4E56 @@ -495,20 +492,19 @@ # --- # name: test_system_snapshot[hass_118169][hot_water] ''' - activeFaults: &id001 [] - active_faults: *id001 - allowedModes: &id002 + active_faults: [] + allowedModes: &id001 - FollowSchedule - PermanentOverride - TemporaryOverride - dhwId: '3933910' dhwStateCapabilitiesResponse: - allowedModes: *id002 + allowedModes: *id001 allowedStates: - 'On' - 'Off' maxDuration: 1.00:00:00 timingResolution: 00:10:00 + id: '3933910' mode: PermanentOverride name: Domestic Hot Water scheduleCapabilitiesResponse: @@ -529,7 +525,7 @@ # name: test_system_snapshot[hass_118169][location] ''' country: UnitedKingdom - locationId: '2738909' + id: '2738909' locationOwner: firstname: David lastname: '********' @@ -550,12 +546,12 @@ # name: test_system_snapshot[hass_118169][zones] ''' '3432521': - activeFaults: &id001 [] - active_faults: *id001 - allowedSetpointModes: &id002 + active_faults: [] + allowedSetpointModes: &id001 - PermanentOverride - FollowSchedule - TemporaryOverride + id: '3432521' max_heat_setpoint: 35.0 min_heat_setpoint: 5.0 mode: PermanentOverride @@ -567,7 +563,7 @@ setpointValueResolution: 0.5 timingResolution: 00:10:00 setpointCapabilities: - allowedSetpointModes: *id002 + allowedSetpointModes: *id001 canControlCool: false canControlHeat: true maxDuration: 1.00:00:00 @@ -585,12 +581,12 @@ isAvailable: false zoneType: RadiatorZone '3432576': - activeFaults: &id003 [] - active_faults: *id003 - allowedSetpointModes: &id004 + active_faults: [] + allowedSetpointModes: &id002 - PermanentOverride - FollowSchedule - TemporaryOverride + id: '3432576' max_heat_setpoint: 35.0 min_heat_setpoint: 5.0 mode: FollowSchedule @@ -602,7 +598,7 @@ setpointValueResolution: 0.5 timingResolution: 00:10:00 setpointCapabilities: - allowedSetpointModes: *id004 + allowedSetpointModes: *id002 canControlCool: false canControlHeat: true maxDuration: 1.00:00:00 @@ -621,12 +617,12 @@ temperature: 19.0 zoneType: RadiatorZone '3432577': - activeFaults: &id005 [] - active_faults: *id005 - allowedSetpointModes: &id006 + active_faults: [] + allowedSetpointModes: &id003 - PermanentOverride - FollowSchedule - TemporaryOverride + id: '3432577' max_heat_setpoint: 35.0 min_heat_setpoint: 5.0 mode: FollowSchedule @@ -638,7 +634,7 @@ setpointValueResolution: 0.5 timingResolution: 00:10:00 setpointCapabilities: - allowedSetpointModes: *id006 + allowedSetpointModes: *id003 canControlCool: false canControlHeat: true maxDuration: 1.00:00:00 @@ -657,12 +653,12 @@ temperature: 19.0 zoneType: RadiatorZone '3432578': - activeFaults: &id007 [] - active_faults: *id007 - allowedSetpointModes: &id008 + active_faults: [] + allowedSetpointModes: &id004 - PermanentOverride - FollowSchedule - TemporaryOverride + id: '3432578' max_heat_setpoint: 35.0 min_heat_setpoint: 5.0 mode: FollowSchedule @@ -674,7 +670,7 @@ setpointValueResolution: 0.5 timingResolution: 00:10:00 setpointCapabilities: - allowedSetpointModes: *id008 + allowedSetpointModes: *id004 canControlCool: false canControlHeat: true maxDuration: 1.00:00:00 @@ -693,12 +689,12 @@ temperature: 20.0 zoneType: RadiatorZone '3432579': - activeFaults: &id009 [] - active_faults: *id009 - allowedSetpointModes: &id010 + active_faults: [] + allowedSetpointModes: &id005 - PermanentOverride - FollowSchedule - TemporaryOverride + id: '3432579' max_heat_setpoint: 35.0 min_heat_setpoint: 5.0 mode: FollowSchedule @@ -710,7 +706,7 @@ setpointValueResolution: 0.5 timingResolution: 00:10:00 setpointCapabilities: - allowedSetpointModes: *id010 + allowedSetpointModes: *id005 canControlCool: false canControlHeat: true maxDuration: 1.00:00:00 @@ -729,12 +725,12 @@ temperature: 20.0 zoneType: RadiatorZone '3432580': - activeFaults: &id011 [] - active_faults: *id011 - allowedSetpointModes: &id012 + active_faults: [] + allowedSetpointModes: &id006 - PermanentOverride - FollowSchedule - TemporaryOverride + id: '3432580' max_heat_setpoint: 35.0 min_heat_setpoint: 5.0 mode: FollowSchedule @@ -746,7 +742,7 @@ setpointValueResolution: 0.5 timingResolution: 00:10:00 setpointCapabilities: - allowedSetpointModes: *id012 + allowedSetpointModes: *id006 canControlCool: false canControlHeat: true maxDuration: 1.00:00:00 @@ -765,12 +761,12 @@ temperature: 21.0 zoneType: RadiatorZone '3449703': - activeFaults: &id013 [] - active_faults: *id013 - allowedSetpointModes: &id014 + active_faults: [] + allowedSetpointModes: &id007 - PermanentOverride - FollowSchedule - TemporaryOverride + id: '3449703' max_heat_setpoint: 35.0 min_heat_setpoint: 5.0 mode: FollowSchedule @@ -782,7 +778,7 @@ setpointValueResolution: 0.5 timingResolution: 00:10:00 setpointCapabilities: - allowedSetpointModes: *id014 + allowedSetpointModes: *id007 canControlCool: false canControlHeat: true maxDuration: 1.00:00:00 @@ -801,12 +797,12 @@ temperature: 19.5 zoneType: RadiatorZone '3449740': - activeFaults: &id015 [] - active_faults: *id015 - allowedSetpointModes: &id016 + active_faults: [] + allowedSetpointModes: &id008 - PermanentOverride - FollowSchedule - TemporaryOverride + id: '3449740' max_heat_setpoint: 35.0 min_heat_setpoint: 5.0 mode: FollowSchedule @@ -818,7 +814,7 @@ setpointValueResolution: 0.5 timingResolution: 00:10:00 setpointCapabilities: - allowedSetpointModes: *id016 + allowedSetpointModes: *id008 canControlCool: false canControlHeat: true maxDuration: 1.00:00:00 @@ -837,12 +833,12 @@ temperature: 21.5 zoneType: RadiatorZone '3450733': - activeFaults: &id017 [] - active_faults: *id017 - allowedSetpointModes: &id018 + active_faults: [] + allowedSetpointModes: &id009 - PermanentOverride - FollowSchedule - TemporaryOverride + id: '3450733' max_heat_setpoint: 35.0 min_heat_setpoint: 5.0 mode: PermanentOverride @@ -854,7 +850,7 @@ setpointValueResolution: 0.5 timingResolution: 00:10:00 setpointCapabilities: - allowedSetpointModes: *id018 + allowedSetpointModes: *id009 canControlCool: false canControlHeat: true maxDuration: 1.00:00:00 @@ -877,8 +873,7 @@ # --- # name: test_system_snapshot[system_002][control_system] ''' - activeFaults: &id001 [] - active_faults: *id001 + active_faults: [] allowedSystemModes: - canBePermanent: true canBeTemporary: false @@ -913,8 +908,8 @@ systemMode: Custom timingMode: Period timingResolution: 1.00:00:00 + id: '3432522' modelType: EvoTouch - systemId: '3432522' systemModeStatus: isPermanent: true mode: Auto @@ -925,7 +920,7 @@ # name: test_system_snapshot[system_002][gateway] ''' active_faults: [] - gatewayId: '2499896' + id: '2499896' isWiFi: false mac: 00D02DEE4E56 @@ -933,22 +928,21 @@ # --- # name: test_system_snapshot[system_002][hot_water] ''' - activeFaults: &id001 + active_faults: - faultType: DHWSensorCommunicationLost since: '2023-11-30T18:48:40' - active_faults: *id001 - allowedModes: &id002 + allowedModes: &id001 - FollowSchedule - PermanentOverride - TemporaryOverride - dhwId: '3933910' dhwStateCapabilitiesResponse: - allowedModes: *id002 + allowedModes: *id001 allowedStates: - 'On' - 'Off' maxDuration: 1.00:00:00 timingResolution: 00:10:00 + id: '3933910' mode: FollowSchedule name: Domestic Hot Water scheduleCapabilitiesResponse: @@ -968,7 +962,7 @@ # name: test_system_snapshot[system_002][location] ''' country: UnitedKingdom - locationId: '2738909' + id: '2738909' locationOwner: firstname: David lastname: '********' @@ -989,12 +983,12 @@ # name: test_system_snapshot[system_002][zones] ''' '3432521': - activeFaults: &id001 [] - active_faults: *id001 - allowedSetpointModes: &id002 + active_faults: [] + allowedSetpointModes: &id001 - PermanentOverride - FollowSchedule - TemporaryOverride + id: '3432521' max_heat_setpoint: 35.0 min_heat_setpoint: 5.0 mode: PermanentOverride @@ -1006,7 +1000,7 @@ setpointValueResolution: 0.5 timingResolution: 00:10:00 setpointCapabilities: - allowedSetpointModes: *id002 + allowedSetpointModes: *id001 canControlCool: false canControlHeat: true maxDuration: 1.00:00:00 @@ -1024,12 +1018,12 @@ isAvailable: false zoneType: RadiatorZone '3432576': - activeFaults: &id003 [] - active_faults: *id003 - allowedSetpointModes: &id004 + active_faults: [] + allowedSetpointModes: &id002 - PermanentOverride - FollowSchedule - TemporaryOverride + id: '3432576' max_heat_setpoint: 35.0 min_heat_setpoint: 5.0 mode: PermanentOverride @@ -1041,7 +1035,7 @@ setpointValueResolution: 0.5 timingResolution: 00:10:00 setpointCapabilities: - allowedSetpointModes: *id004 + allowedSetpointModes: *id002 canControlCool: false canControlHeat: true maxDuration: 1.00:00:00 @@ -1060,12 +1054,12 @@ temperature: 17.0 zoneType: RadiatorZone '3432577': - activeFaults: &id005 [] - active_faults: *id005 - allowedSetpointModes: &id006 + active_faults: [] + allowedSetpointModes: &id003 - PermanentOverride - FollowSchedule - TemporaryOverride + id: '3432577' max_heat_setpoint: 35.0 min_heat_setpoint: 5.0 mode: FollowSchedule @@ -1077,7 +1071,7 @@ setpointValueResolution: 0.5 timingResolution: 00:10:00 setpointCapabilities: - allowedSetpointModes: *id006 + allowedSetpointModes: *id003 canControlCool: false canControlHeat: true maxDuration: 1.00:00:00 @@ -1096,12 +1090,12 @@ temperature: 20.5 zoneType: RadiatorZone '3432578': - activeFaults: &id007 [] - active_faults: *id007 - allowedSetpointModes: &id008 + active_faults: [] + allowedSetpointModes: &id004 - PermanentOverride - FollowSchedule - TemporaryOverride + id: '3432578' max_heat_setpoint: 35.0 min_heat_setpoint: 5.0 mode: FollowSchedule @@ -1113,7 +1107,7 @@ setpointValueResolution: 0.5 timingResolution: 00:10:00 setpointCapabilities: - allowedSetpointModes: *id008 + allowedSetpointModes: *id004 canControlCool: false canControlHeat: true maxDuration: 1.00:00:00 @@ -1132,12 +1126,12 @@ temperature: 20.0 zoneType: RadiatorZone '3432579': - activeFaults: &id009 [] - active_faults: *id009 - allowedSetpointModes: &id010 + active_faults: [] + allowedSetpointModes: &id005 - PermanentOverride - FollowSchedule - TemporaryOverride + id: '3432579' max_heat_setpoint: 35.0 min_heat_setpoint: 5.0 mode: FollowSchedule @@ -1149,7 +1143,7 @@ setpointValueResolution: 0.5 timingResolution: 00:10:00 setpointCapabilities: - allowedSetpointModes: *id010 + allowedSetpointModes: *id005 canControlCool: false canControlHeat: true maxDuration: 1.00:00:00 @@ -1168,12 +1162,12 @@ temperature: 17.5 zoneType: RadiatorZone '3432580': - activeFaults: &id011 [] - active_faults: *id011 - allowedSetpointModes: &id012 + active_faults: [] + allowedSetpointModes: &id006 - PermanentOverride - FollowSchedule - TemporaryOverride + id: '3432580' max_heat_setpoint: 35.0 min_heat_setpoint: 5.0 mode: TemporaryOverride @@ -1185,7 +1179,7 @@ setpointValueResolution: 0.5 timingResolution: 00:10:00 setpointCapabilities: - allowedSetpointModes: *id012 + allowedSetpointModes: *id006 canControlCool: false canControlHeat: true maxDuration: 1.00:00:00 @@ -1205,12 +1199,12 @@ temperature: 21.0 zoneType: RadiatorZone '3449703': - activeFaults: &id013 [] - active_faults: *id013 - allowedSetpointModes: &id014 + active_faults: [] + allowedSetpointModes: &id007 - PermanentOverride - FollowSchedule - TemporaryOverride + id: '3449703' max_heat_setpoint: 35.0 min_heat_setpoint: 5.0 mode: FollowSchedule @@ -1222,7 +1216,7 @@ setpointValueResolution: 0.5 timingResolution: 00:10:00 setpointCapabilities: - allowedSetpointModes: *id014 + allowedSetpointModes: *id007 canControlCool: false canControlHeat: true maxDuration: 1.00:00:00 @@ -1241,12 +1235,12 @@ temperature: 19.5 zoneType: RadiatorZone '3449740': - activeFaults: &id015 [] - active_faults: *id015 - allowedSetpointModes: &id016 + active_faults: [] + allowedSetpointModes: &id008 - PermanentOverride - FollowSchedule - TemporaryOverride + id: '3449740' max_heat_setpoint: 35.0 min_heat_setpoint: 5.0 mode: FollowSchedule @@ -1258,7 +1252,7 @@ setpointValueResolution: 0.5 timingResolution: 00:10:00 setpointCapabilities: - allowedSetpointModes: *id016 + allowedSetpointModes: *id008 canControlCool: false canControlHeat: true maxDuration: 1.00:00:00 @@ -1277,12 +1271,12 @@ temperature: 19.0 zoneType: RadiatorZone '3450733': - activeFaults: &id017 [] - active_faults: *id017 - allowedSetpointModes: &id018 + active_faults: [] + allowedSetpointModes: &id009 - PermanentOverride - FollowSchedule - TemporaryOverride + id: '3450733' max_heat_setpoint: 35.0 min_heat_setpoint: 5.0 mode: PermanentOverride @@ -1294,7 +1288,7 @@ setpointValueResolution: 0.5 timingResolution: 00:10:00 setpointCapabilities: - allowedSetpointModes: *id018 + allowedSetpointModes: *id009 canControlCool: false canControlHeat: true maxDuration: 1.00:00:00 @@ -1317,8 +1311,7 @@ # --- # name: test_system_snapshot[system_004][control_system] ''' - activeFaults: &id001 [] - active_faults: *id001 + active_faults: [] allowedSystemModes: - canBePermanent: true canBeTemporary: false @@ -1338,8 +1331,8 @@ - canBePermanent: true canBeTemporary: false systemMode: HeatingOff + id: '3935923' modelType: EvoTouch - systemId: '3935923' systemModeStatus: isPermanent: true mode: Auto @@ -1350,7 +1343,7 @@ # name: test_system_snapshot[system_004][gateway] ''' active_faults: [] - gatewayId: '2820628' + id: '2820628' isWiFi: false mac: 00D02DF114FD @@ -1365,7 +1358,7 @@ # name: test_system_snapshot[system_004][location] ''' country: CzechRepublic - locationId: '2664492' + id: '2664492' locationOwner: firstname: Jan lastname: '********' @@ -1387,12 +1380,12 @@ # name: test_system_snapshot[system_004][zones] ''' '3935922': - activeFaults: &id001 [] - active_faults: *id001 - allowedSetpointModes: &id002 + active_faults: [] + allowedSetpointModes: &id001 - PermanentOverride - FollowSchedule - TemporaryOverride + id: '3935922' max_heat_setpoint: 35.0 min_heat_setpoint: 5.0 mode: PermanentOverride @@ -1404,7 +1397,7 @@ setpointValueResolution: 0.5 timingResolution: 00:10:00 setpointCapabilities: - allowedSetpointModes: *id002 + allowedSetpointModes: *id001 canControlCool: false canControlHeat: true maxDuration: 1.00:00:00 @@ -1423,12 +1416,12 @@ temperature: 21.5 zoneType: Thermostat '4368406': - activeFaults: &id003 [] - active_faults: *id003 - allowedSetpointModes: &id004 + active_faults: [] + allowedSetpointModes: &id002 - PermanentOverride - FollowSchedule - TemporaryOverride + id: '4368406' max_heat_setpoint: 35.0 min_heat_setpoint: 5.0 mode: PermanentOverride @@ -1440,7 +1433,7 @@ setpointValueResolution: 0.5 timingResolution: 00:10:00 setpointCapabilities: - allowedSetpointModes: *id004 + allowedSetpointModes: *id002 canControlCool: false canControlHeat: true maxDuration: 1.00:00:00 diff --git a/tests/tests/conftest.py b/tests/tests/conftest.py index b2a204bc..8bdd310e 100644 --- a/tests/tests/conftest.py +++ b/tests/tests/conftest.py @@ -14,6 +14,7 @@ import pytest import pytest_asyncio import voluptuous as vol +from aioresponses import aioresponses from evohomeasync2.broker import AbstractTokenManager from evohomeasync2.schema import SCH_FULL_CONFIG, SCH_LOCN_STATUS, SCH_USER_ACCOUNT @@ -53,6 +54,13 @@ async def save_access_token(self) -> None: pass +@pytest.fixture(autouse=True) +def block_aiohttp(): + """Prevent any actual I/O: will raise ClientConnectionError(Connection refused).""" + with aioresponses() as m: + yield m + + @lru_cache def load_fixture(install: str, file_name: str) -> JsonArrayType | JsonObjectType: """Load a file fixture.""" diff --git a/tests/tests/test_installs.py b/tests/tests/test_installs.py index f8b439da..74ad6dd7 100644 --- a/tests/tests/test_installs.py +++ b/tests/tests/test_installs.py @@ -32,10 +32,19 @@ async def test_system_snapshot( """Test the user account schema against the corresponding JSON.""" def obj_to_dict(obj: object) -> dict[str, Any]: + DEPRECATED_ATTRS = ( # attrs only in _Deprecated classes + "activeFaults", + "dhwId", + "gatewayId", + "locationId", + "systemId", + "zone_type", + "zoneId", + ) return { attr: getattr(obj, attr) for attr in get_property_methods(obj) - if attr not in ("zoneId", "zone_type") # excl. deprecated attrs + if attr not in DEPRECATED_ATTRS } with patch("evohomeasync2.broker.Broker.get", broker_get(install)): @@ -57,5 +66,5 @@ def obj_to_dict(obj: object) -> dict[str, Any]: dhw = tcs.hotwater assert yaml.dump(obj_to_dict(dhw), indent=4) == snapshot(name="hot_water") - zones = {z.zoneId: obj_to_dict(z) for z in tcs._zones} + zones = {z.id: obj_to_dict(z) for z in tcs._zones} assert yaml.dump(zones, indent=4) == snapshot(name="zones") diff --git a/tests/tests_rf/conftest.py b/tests/tests_rf/conftest.py index ca761275..f4d963ab 100644 --- a/tests/tests_rf/conftest.py +++ b/tests/tests_rf/conftest.py @@ -36,8 +36,10 @@ @pytest.fixture(autouse=True) def patches_for_tests(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr("evohomeasync2.aiohttp", aiohttp) monkeypatch.setattr("evohomeasync2.base.aiohttp", aiohttp) monkeypatch.setattr("evohomeasync2.broker.aiohttp", aiohttp) + monkeypatch.setattr("evohomeasync2.client.aiohttp", aiohttp) monkeypatch.setattr("evohomeasync.base.aiohttp", aiohttp) monkeypatch.setattr("evohomeasync.broker.aiohttp", aiohttp) diff --git a/tests/tests_rf/faked_server/__init__.py b/tests/tests_rf/faked_server/__init__.py index 08ca11e7..652f74ff 100644 --- a/tests/tests_rf/faked_server/__init__.py +++ b/tests/tests_rf/faked_server/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations -# from . import aiohttp # noqa: F401 from .aiohttp import ClientSession # noqa: F401 from .const import GHOST_ZONE_ID # noqa: F401 from .vendor import FakedServer # noqa: F401 diff --git a/tests/tests_rf/faked_server/aiohttp.py b/tests/tests_rf/faked_server/aiohttp.py index 7e3099d7..56a5c4a9 100644 --- a/tests/tests_rf/faked_server/aiohttp.py +++ b/tests/tests_rf/faked_server/aiohttp.py @@ -74,6 +74,10 @@ def __init__(self, msg, /, *, status: int | None = None, **kwargs) -> None: self.status = status +class ContentTypeError(ClientResponseError): + """ContentType found is not valid.""" + + class ClientTimeout: """A faked ClientTimeout.""" diff --git a/tests/tests_rf/faked_server/const.py b/tests/tests_rf/faked_server/const.py index 1e4ffaec..6a257dec 100644 --- a/tests/tests_rf/faked_server/const.py +++ b/tests/tests_rf/faked_server/const.py @@ -499,7 +499,7 @@ } -MOCK_SCHEDULE_ZONE = { # of any zone (no zoneId) +MOCK_SCHEDULE_ZONE = { # of any zone (i.e. no zone id) "dailySchedules": [ { "dayOfWeek": "Monday", @@ -568,7 +568,7 @@ } -MOCK_SCHEDULE_DHW = { # of any zone (no dhwId) +MOCK_SCHEDULE_DHW = { # of any zone (i.e. no dhw id) "dailySchedules": [ { "dayOfWeek": "Monday", diff --git a/tests/tests_rf/faked_server/vendor.py b/tests/tests_rf/faked_server/vendor.py index ca1df224..bbeb01e5 100644 --- a/tests/tests_rf/faked_server/vendor.py +++ b/tests/tests_rf/faked_server/vendor.py @@ -24,6 +24,7 @@ SZ_TEMPERATURE_CONTROL_SYSTEM, SZ_TEMPERATURE_CONTROL_SYSTEMS, SZ_TEMPERATURE_ZONE, + SZ_USER_ID, SZ_ZONE_ID, SZ_ZONES, ) @@ -52,34 +53,34 @@ def _dhw_id(url: _urlT) -> _DhwIdT: - """Extract a DHW ID from a URL.""" + """Extract a DHW id from a URL.""" return url.split(f"{SZ_DOMESTIC_HOT_WATER}/")[1].split("/")[0] def _loc_id(url: _urlT) -> _LocationIdT: - """Extract a Location ID from a URL.""" + """Extract a Location id from a URL.""" return url.split(f"{SZ_LOCATION}/")[1].split("/")[0] def _tcs_id(url: _urlT) -> _SystemIdT: - """Extract a TCS ID from a URL.""" + """Extract a TCS id from a URL.""" return url.split(f"{SZ_TEMPERATURE_CONTROL_SYSTEM}/")[1].split("/")[0] def _usr_id(url: _urlT) -> _UserIdT: - """Extract a TCS ID from a URL.""" + """Extract a User id from a URL.""" return url.split("?userId=")[1].split("&")[0] def _zon_id(url: _urlT) -> _ZoneIdT: - """Extract a Zone ID from a URL.""" + """Extract a Zone id from a URL.""" return url.split(f"{SZ_TEMPERATURE_ZONE}/")[1].split("/")[0] def validate_id_of_url( id_fnc: Callable[[_urlT], str], ) -> Callable[..., Callable[[FakedServer], _bodyT]]: - """Validate the ID in the URL and set the status accordingly.""" + """Validate the id in the URL and set the status accordingly.""" def decorator( fnc: Callable[[FakedServer], _bodyT | None], @@ -184,7 +185,7 @@ def usr_account(self) -> _bodyT: def all_config(self) -> _bodyT | None: # full_locn usr_id = _usr_id(self._url) # type: ignore[arg-type] - if self._user_config["userId"] == usr_id: + if self._user_config[SZ_USER_ID] == usr_id: return self._full_config return None diff --git a/tests/tests_rf/test_v2_apis.py b/tests/tests_rf/test_v2_apis.py index af330384..94b28ecc 100644 --- a/tests/tests_rf/test_v2_apis.py +++ b/tests/tests_rf/test_v2_apis.py @@ -74,7 +74,7 @@ async def _test_basics_apis(evo: evo2.EvohomeClient) -> None: # await evo.user_account(force_update=True) # will update as forced # - # STEP 3: Installation, GET /location/installationInfo?userId={userId} + # STEP 3: Installation, GET /location/installationInfo?userId={user_id} assert evo.locations == [] assert evo.installation_info is None @@ -90,7 +90,7 @@ async def _test_basics_apis(evo: evo2.EvohomeClient) -> None: # await evo.installation(force_update=True) # will update as forced # - # STEP 4: Status, GET /location/{locationId}/status + # STEP 4: Status, GET /location/{loc.id}/status for loc in evo.locations: loc_status = await loc.refresh_status() assert SCH_LOCN_STATUS(loc_status) @@ -107,7 +107,7 @@ async def _test_sched__apis(evo: evo2.EvohomeClient) -> None: await evo.installation() # - # STEP 2: GET & PUT /{_type}/{_id}/schedule + # STEP 2: GET & PUT /{x.TYPE}/{x.id}/schedule if dhw := evo._get_single_tcs().hotwater: schedule = await dhw.get_schedule() assert SCH_PUT_SCHEDULE_DHW(schedule) @@ -115,7 +115,7 @@ async def _test_sched__apis(evo: evo2.EvohomeClient) -> None: zone: Zone | None - if (zone := evo._get_single_tcs()._zones[0]) and zone._id != faked.GHOST_ZONE_ID: + if (zone := evo._get_single_tcs()._zones[0]) and zone.id != faked.GHOST_ZONE_ID: schedule = await zone.get_schedule() assert SCH_PUT_SCHEDULE_ZONE(schedule) await zone.set_schedule(schedule) @@ -138,7 +138,7 @@ async def _test_status_apis(evo: evo2.EvohomeClient) -> None: await evo.installation() # - # STEP 2: GET /{_type}/{_id}/status + # STEP 2: GET /{x.TYPE}/{x.id}/status if dhw := evo._get_single_tcs().hotwater: dhw_status = await dhw._refresh_status() assert SCH_DHW_STATUS(dhw_status) @@ -159,7 +159,7 @@ async def _test_system_apis(evo: evo2.EvohomeClient) -> None: await evo.installation() # - # STEP 2: GET /{_type}/{_id}/status + # STEP 2: GET /{x.TYPE}/{x.id}/status try: tcs = evo._get_single_tcs() except evo2.NoSingleTcsError: diff --git a/tests/tests_rf/test_v2_task.py b/tests/tests_rf/test_v2_task.py index 3855b877..df7cdd4d 100644 --- a/tests/tests_rf/test_v2_task.py +++ b/tests/tests_rf/test_v2_task.py @@ -50,8 +50,8 @@ async def _test_task_id(evo: evo2.EvohomeClient) -> None: # pytest.skip("No available DHW found") # - GET_URL = f"{dhw.TYPE}/{dhw._id}/status" - PUT_URL = f"{dhw.TYPE}/{dhw._id}/state" + GET_URL = f"{dhw.TYPE}/{dhw.id}/status" + PUT_URL = f"{dhw.TYPE}/{dhw.id}/state" # # PART 0: Get initial state... @@ -191,7 +191,7 @@ async def test_task_id( user_credentials: tuple[str, str], session: aiohttp.ClientSession, ) -> None: - """Test /location/{locationId}/status""" + """Test /location/{loc.id}/status""" if not _DBG_USE_REAL_AIOHTTP: pytest.skip("Test is only valid with a real server") diff --git a/tests/tests_rf/test_v2_urls.py b/tests/tests_rf/test_v2_urls.py index 1a257e7a..6da413ba 100644 --- a/tests/tests_rf/test_v2_urls.py +++ b/tests/tests_rf/test_v2_urls.py @@ -32,6 +32,7 @@ SZ_SYSTEM_MODE, SZ_TEMPERATURE, SZ_TIME_UNTIL, + SZ_USER_ID, ) from evohomeasync2.schema.schedule import convert_to_put_schedule @@ -66,12 +67,12 @@ async def _test_usr_account(evo: evo2.EvohomeClient) -> None: async def _test_all_config(evo: evo2.EvohomeClient) -> None: - """Test /location/installationInfo?userId={userId}""" + """Test /location/installationInfo?userId={user_id}""" _ = await evo.user_account() # - url = f"location/installationInfo?userId={evo.account_info['userId']}" + url = f"location/installationInfo?userId={evo.account_info[SZ_USER_ID]}" await should_work(evo, HTTPMethod.GET, url) url += "&includeTemperatureControlSystems=True" @@ -94,7 +95,7 @@ async def _test_all_config(evo: evo2.EvohomeClient) -> None: async def _test_loc_status(evo: evo2.EvohomeClient) -> None: - """Test /location/{locationId}/status""" + """Test /location/{loc.id}/status""" _ = await evo.user_account() @@ -102,7 +103,7 @@ async def _test_loc_status(evo: evo2.EvohomeClient) -> None: loc = evo.locations[0] # - url = f"location/{loc.locationId}/status" + url = f"location/{loc.id}/status" _ = await should_work(evo, HTTPMethod.GET, url) url += "?includeTemperatureControlSystems=True" @@ -111,7 +112,7 @@ async def _test_loc_status(evo: evo2.EvohomeClient) -> None: evo, HTTPMethod.PUT, url, status=HTTPStatus.METHOD_NOT_ALLOWED ) - url = f"location/{loc.locationId}" + url = f"location/{loc.id}" await should_fail( evo, HTTPMethod.GET, @@ -126,7 +127,7 @@ async def _test_loc_status(evo: evo2.EvohomeClient) -> None: url = "location/xxxxxxx/status" _ = await should_fail(evo, HTTPMethod.GET, url, status=HTTPStatus.BAD_REQUEST) - url = f"location/{loc.locationId}/xxxxxxx" + url = f"location/{loc.id}/xxxxxxx" _ = await should_fail( evo, HTTPMethod.GET, @@ -137,7 +138,7 @@ async def _test_loc_status(evo: evo2.EvohomeClient) -> None: async def _test_tcs_mode(evo: evo2.EvohomeClient) -> None: - """Test /temperatureControlSystem/{systemId}/mode""" + """Test /temperatureControlSystem/{tcs.id}/mode""" _ = await evo.user_account() _ = await evo._installation(refresh_status=False) @@ -150,10 +151,10 @@ async def _test_tcs_mode(evo: evo2.EvohomeClient) -> None: _ = await tcs.location.refresh_status() # could use: await tcs._refresh_status() old_mode: _EvoDictT = tcs.systemModeStatus # type: ignore[assignment] - url = f"{tcs.TYPE}/{tcs._id}/status" + url = f"{tcs.TYPE}/{tcs.id}/status" _ = await should_work(evo, HTTPMethod.GET, url, schema=SCH_TCS_STATUS) - url = f"{tcs.TYPE}/{tcs._id}/mode" + url = f"{tcs.TYPE}/{tcs.id}/mode" _ = await should_fail( evo, HTTPMethod.GET, url, status=HTTPStatus.METHOD_NOT_ALLOWED ) @@ -186,7 +187,7 @@ async def _test_tcs_mode(evo: evo2.EvohomeClient) -> None: evo, HTTPMethod.PUT, url, json=old_mode, status=HTTPStatus.UNAUTHORIZED ) - url = f"{tcs.TYPE}/{tcs._id}/systemMode" + url = f"{tcs.TYPE}/{tcs.id}/systemMode" _ = await should_fail( evo, HTTPMethod.PUT, @@ -200,7 +201,7 @@ async def _test_tcs_mode(evo: evo2.EvohomeClient) -> None: async def _test_zone_mode(evo: evo2.EvohomeClient) -> None: - """Test /temperatureZone/{zoneId}/heatSetpoint""" + """Test /temperatureZone/{zone.id}/heatSetpoint""" _ = await evo.user_account() _ = await evo._installation(refresh_status=False) @@ -213,10 +214,10 @@ async def _test_zone_mode(evo: evo2.EvohomeClient) -> None: pytest.skip("No available zones found") # - url = f"{zone.TYPE}/{zone._id}/status" + url = f"{zone.TYPE}/{zone.id}/status" _ = await should_work(evo, HTTPMethod.GET, url, schema=SCH_ZONE_STATUS) - url = f"{zone.TYPE}/{zone._id}/heatSetpoint" + url = f"{zone.TYPE}/{zone.id}/heatSetpoint" heat_setpoint = { SZ_SETPOINT_MODE: ZoneMode.PERMANENT_OVERRIDE, @@ -257,7 +258,7 @@ async def _test_zone_mode(evo: evo2.EvohomeClient) -> None: # TODO: Test sending bad schedule # TODO: Try with/without convert_to_put_schedule() async def _test_schedule(evo: evo2.EvohomeClient) -> None: - """Test /{x.TYPE}/{x._id}/schedule (of a zone)""" + """Test /{x.TYPE}/{x.id}/schedule (of a zone)""" _ = await evo.user_account() @@ -265,12 +266,12 @@ async def _test_schedule(evo: evo2.EvohomeClient) -> None: zone = evo.locations[0]._gateways[0]._control_systems[0]._zones[0] # - if zone._id == faked.GHOST_ZONE_ID: + if zone.id == faked.GHOST_ZONE_ID: url = f"{zone.TYPE}/{faked.GHOST_ZONE_ID}/schedule" _ = await should_fail(evo, HTTPMethod.GET, url, status=HTTPStatus.BAD_REQUEST) return - url = f"{zone.TYPE}/{zone._id}/schedule" + url = f"{zone.TYPE}/{zone.id}/schedule" schedule = await should_work(evo, HTTPMethod.GET, url, schema=SCH_GET_SCHEDULE) temp = schedule[SZ_DAILY_SCHEDULES][0][SZ_SWITCHPOINTS][0][SZ_HEAT_SETPOINT] # type: ignore[call-overload] @@ -342,7 +343,7 @@ async def test_loc_status( user_credentials: tuple[str, str], session: aiohttp.ClientSession, ) -> None: - """Test /location/{locationId}/status""" + """Test /location/{loc.id}/status""" try: await _test_loc_status(await instantiate_client_v2(user_credentials, session)) @@ -357,7 +358,7 @@ async def test_tcs_mode( user_credentials: tuple[str, str], session: aiohttp.ClientSession, ) -> None: - """Test /temperatureControlSystem/{systemId}/mode""" + """Test /temperatureControlSystem/{tcs.id}/mode""" try: await _test_tcs_mode(await instantiate_client_v2(user_credentials, session)) @@ -377,7 +378,7 @@ async def test_zone_mode( user_credentials: tuple[str, str], session: aiohttp.ClientSession, ) -> None: - """Test /temperatureZone/{zoneId}/heatSetpoint""" + """Test /temperatureZone/{zone.id}/heatSetpoint""" try: await _test_zone_mode(await instantiate_client_v2(user_credentials, session)) @@ -397,7 +398,7 @@ async def test_schedule( user_credentials: tuple[str, str], session: aiohttp.ClientSession, ) -> None: - """Test /{x.TYPE}/{x_id}/schedule""" + """Test /{x.TYPE}/{x.id}/schedule""" try: await _test_schedule(await instantiate_client_v2(user_credentials, session))