Skip to content

Commit

Permalink
refactoring, fix tests
Browse files Browse the repository at this point in the history
  • Loading branch information
zxdavb committed Dec 24, 2024
1 parent 9da3ecc commit d0f62d0
Show file tree
Hide file tree
Showing 9 changed files with 233 additions and 142 deletions.
3 changes: 1 addition & 2 deletions src/evohomeasync/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,8 @@ async def update(
/,
*,
_reset_config: bool = False,
# _dont_update_status: bool = False,
) -> list[EvoLocConfigDictT] | None:
"""Retrieve the latest state of the installation and it's locations.
"""Retrieve the latest state of the user's locations.
If required, or when `_reset_config` is true, first retrieves the user
information.
Expand Down
8 changes: 4 additions & 4 deletions src/evohomeasync/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import annotations

from enum import EnumCheck, StrEnum, verify
from typing import TYPE_CHECKING, Any, Final, NewType, TypedDict, TypeVar
from typing import TYPE_CHECKING, Any, Final, NewType, NotRequired, TypedDict, TypeVar

import voluptuous as vol

Expand Down Expand Up @@ -164,7 +164,7 @@ def _factory_location_response(
vol.Required(fnc("locationOwnerName")): str,
vol.Required(fnc("locationOwnerUserName")): vol.All(str, vol.Length(min=1)),
vol.Required(fnc("canSearchForContractors")): bool,
vol.Required(fnc("contractor")): {str: dict}, # ContractorResponse
vol.Optional(fnc("contractor")): {str: dict}, # ContractorResponse
},
extra=vol.ALLOW_EXTRA,
)
Expand Down Expand Up @@ -311,7 +311,7 @@ class TccLocationResponseT(TypedDict):
locationOwnerName: str
locationOwnerUserName: str
canSearchforcontractors: bool
contractor: dict[str, Any] # ContractorResponse
contractor: NotRequired[dict[str, Any]] # ContractorResponse


class TccDeviceResponseT(TypedDict):
Expand Down Expand Up @@ -456,7 +456,7 @@ class EvoLocConfigDictT(TypedDict):
location_owner_name: str
location_owner_user_name: str
can_searchforcontractors: bool
contractor: dict[str, Any] # ContractorResponse
contractor: NotRequired[dict[str, Any]] # ContractorResponse


class EvoGwyConfigDictT(TypedDict):
Expand Down
13 changes: 12 additions & 1 deletion src/evohomeasync2/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from typing import TYPE_CHECKING, Final
from typing import TYPE_CHECKING, Final, NoReturn

from evohome.helpers import camel_to_snake

Expand Down Expand Up @@ -66,7 +66,18 @@ def gatewayId(self) -> str: # noqa: N802
def mac_address(self) -> str:
return self._config[SZ_MAC]

async def _get_status(self) -> NoReturn:
"""Get the latest state of the gateway and update its status attr.
It is more efficient to call Location.update() as all descendants are updated
with a single GET. Returns the raw JSON of the latest state.
"""

raise NotImplementedError

def _update_status(self, status: EvoGwyStatusResponseT) -> None:
"""Update the GWY's status and cascade to its descendants."""

self._update_faults(status["active_faults"])
self._status = status

Expand Down
55 changes: 36 additions & 19 deletions src/evohomeasync2/location.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,28 @@ def country(self) -> str:
def name(self) -> str:
return self._config[SZ_NAME]

async def _get_config(self) -> EvoLocConfigResponseT:
"""Get the latest state of the gateway and update its status attr.
Usually called when DST starts/stops or the location's DST config changes (i.e.
there is no _update_config() method). Returns the raw JSON of the latest config.
"""

# it is assumed that only the location's TZ/DST info can change
# so no ?includeTemperatureControlSystems=True

config: EvoLocConfigResponseT = await self._auth.get(
f"location/{self._id}/installationInfo" # TODO: add schema
) # type: ignore[assignment]

self._config = config[SZ_LOCATION_INFO]

# new TzInfo object, or update the existing one?
self._tzinfo = _create_tzinfo(self.time_zone_info, dst_enabled=self.dst_enabled)
# lf._tzinfo._update(time_zone_info=time_zone_info, use_dst_switching=use_dst_switching)

return config

@property
def time_zone_info(self) -> EvoTimeZoneInfoT:
"""Return the time zone information for the location.
Expand Down Expand Up @@ -142,42 +164,37 @@ def now(self) -> dt: # always returns a TZ-aware dtm
async def update(
self, *, _update_time_zone_info: bool = False
) -> EvoLocStatusResponseT:
"""Get the latest state of the location and update its status.
"""Get the latest state of the location and update its status attrs.
Will also update the status of its gateways, their TCSs, and their DHW/zones.
Returns the raw JSON of the latest state.
"""

if _update_time_zone_info:
await self._update_config()
await self._get_config()

status: EvoLocStatusResponseT = await self._auth.get(
f"{self._TYPE}/{self.id}/status?includeTemperatureControlSystems=True",
schema=self.SCH_STATUS,
) # type: ignore[assignment]
status = await self._get_status()

self._update_status(status)
return status

async def _update_config(self) -> None:
"""Usually called when DST starts/stops or the location's DST config changes."""
async def _get_status(self) -> EvoLocStatusResponseT:
"""Get the latest state of the location and update its status attr.
# it is assumed that only the location's TZ/DST info can change
# so no ?includeTemperatureControlSystems=True
Returns the raw JSON of the latest state.
"""

config: EvoLocConfigResponseT = await self._auth.get(
f"location/{self._id}/installationInfo" # TODO: add schema
status: EvoLocStatusResponseT = await self._auth.get(
f"{self._TYPE}/{self.id}/status?includeTemperatureControlSystems=True",
schema=self.SCH_STATUS,
) # type: ignore[assignment]

self._config = config[SZ_LOCATION_INFO]

# new TzInfo object, or update the existing one?
self._tzinfo = _create_tzinfo(self.time_zone_info, dst_enabled=self.dst_enabled)
# lf._tzinfo._update(time_zone_info=time_zone_info, use_dst_switching=use_dst_switching)
self._update_status(status)
return status

def _update_status(self, status: EvoLocStatusResponseT) -> None:
"""Update the location's latest status (and its gateways and their TCSs)."""
"""Update the LOC's status and cascade to its descendants."""

# No ActiveFaults in location node of status
self._status = status

Expand Down
34 changes: 24 additions & 10 deletions src/evohomeasync2/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,16 +71,16 @@ def __init__(
def __str__(self) -> str:
return f"{self.__class__.__name__}(auth='{self.auth}')"

async def update( # noqa: C901
async def update(
self,
/,
*,
_reset_config: bool = False,
_dont_update_status: bool = False,
) -> list[EvoLocConfigResponseT]:
"""Retrieve the latest state of the installation and it's locations.
"""Retrieve the latest state of the user's' locations.
If required, or when `_reset_config` is true, first retrieves the user
If required, or whenever `_reset_config` is true, first retrieves the user
information & installation configuration.
If `_disable_status_update` is True, does not update the status of each location
Expand All @@ -89,7 +89,27 @@ async def update( # noqa: C901

if _reset_config:
self._user_info = None
self._user_locs = None
self._user_locs = None # and thus self._locations, etc.

await self._get_config(_dont_update_status=_dont_update_status)

assert self._locations is not None # mypy (internal hint)

if not _dont_update_status: # see warning, above
for loc in self._locations:
await loc.update()

assert self._user_locs is not None # mypy (internal hint)

return self._user_locs

async def _get_config(
self, /, *, _dont_update_status: bool = False
) -> list[EvoLocConfigResponseT]:
"""Ensures the config of the user and their locations.
If required, first retrieves the user information & installation configuration.
"""

if self._user_info is None: # will handle a bad access_token
url = "userAccount"
Expand Down Expand Up @@ -137,12 +157,6 @@ async def update( # noqa: C901
"limits by individually updating only the necessary locations."
)

assert self._locations is not None # mypy (internal hint)

if not _dont_update_status: # see warning, above
for loc in self._locations:
await loc.update()

return self._user_locs

@property
Expand Down
71 changes: 41 additions & 30 deletions src/evohomeasync2/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from __future__ import annotations

import json
from typing import TYPE_CHECKING, Final
from typing import TYPE_CHECKING, Final, NoReturn

from evohome.helpers import camel_to_snake

Expand Down Expand Up @@ -104,30 +104,6 @@ def __init__(self, gateway: Gateway, config: EvoTcsConfigResponseT) -> None:
if dhw_entry := config.get(SZ_DHW):
self.hotwater = HotWater(self, dhw_entry)

def _update_status(self, status: EvoTcsStatusResponseT) -> None:
self._update_faults(status["active_faults"])
self._status = status

for zon_status in self._status[SZ_ZONES]:
if zone := self.zones_by_id.get(zon_status[SZ_ZONE_ID]):
zone._update_status(zon_status)

else:
self._logger.warning(
f"{self}: zone_id='{zon_status[SZ_ZONE_ID]}' not known"
", (has the system configuration been changed?)"
)

if dhw_status := self._status.get(SZ_DHW):
if self.hotwater and self.hotwater.id == dhw_status[SZ_DHW_ID]:
self.hotwater._update_status(dhw_status)

else:
self._logger.warning(
f"{self}: dhw_id='{dhw_status[SZ_DHW_ID]}' not known"
", (has the system configuration been changed?)"
)

@property # TODO: deprecate in favour of .id attr
def systemId(self) -> str: # noqa: N802
return self._id
Expand All @@ -136,11 +112,6 @@ def systemId(self) -> str: # noqa: N802
def model(self) -> TcsModelType:
return self._config[SZ_MODEL_TYPE]

@property
def zones_by_name(self) -> dict[str, Zone]:
"""Return the zones by name (names are not fixed attrs)."""
return {zone.name: zone for zone in self.zones}

@property
def allowed_system_modes(self) -> tuple[EvoAllowedSystemModeResponseT, ...]:
"""
Expand All @@ -161,6 +132,46 @@ def allowed_system_modes(self) -> tuple[EvoAllowedSystemModeResponseT, ...]:
def modes(self) -> tuple[SystemMode, ...]:
return tuple(d[SZ_SYSTEM_MODE] for d in self.allowed_system_modes)

@property
def zones_by_name(self) -> dict[str, Zone]:
"""Return the zones by name (names are not fixed attrs)."""
return {zone.name: zone for zone in self.zones}

async def _get_status(self) -> NoReturn:
"""Get the latest state of the control system and update its status attrs.
It is more efficient to call Location.update() as all descendants are updated
with a single GET. Returns the raw JSON of the latest state.
"""

raise NotImplementedError

def _update_status(self, status: EvoTcsStatusResponseT) -> None:
"""Update the TCS's status and cascade to its descendants."""

self._update_faults(status["active_faults"])
self._status = status

for zon_status in self._status[SZ_ZONES]:
if zone := self.zones_by_id.get(zon_status[SZ_ZONE_ID]):
zone._update_status(zon_status)

else:
self._logger.warning(
f"{self}: zone_id='{zon_status[SZ_ZONE_ID]}' not known"
", (has the system configuration been changed?)"
)

if dhw_status := self._status.get(SZ_DHW):
if self.hotwater and self.hotwater.id == dhw_status[SZ_DHW_ID]:
self.hotwater._update_status(dhw_status)

else:
self._logger.warning(
f"{self}: dhw_id='{dhw_status[SZ_DHW_ID]}' not known"
", (has the system configuration been changed?)"
)

@property
def system_mode_status(self) -> EvoSystemModeStatusResponseT:
"""
Expand Down
12 changes: 6 additions & 6 deletions src/evohomeasync2/zone.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,13 +391,11 @@ def __init__(self, entity_id: str, tcs: ControlSystem) -> None:
self.location = tcs.location
self.tcs = tcs

async def _update(self) -> EvoDhwStatusResponseT | EvoZonStatusResponseT:
"""Get the latest state of the DHW/zone and update its status.
async def _get_status(self) -> EvoDhwStatusResponseT | EvoZonStatusResponseT:
"""Get the latest state of the DHW/zone and update its status attrs.
It is more efficient to call Location.update() as all zones are updated
with a single GET.
Returns the raw JSON of the latest state.
It is more efficient to call Location.update() as all descendants are updated
with a single GET. Returns the raw JSON of the latest state.
"""

status: EvoDhwStatusResponseT | EvoZonStatusResponseT = await self._auth.get( # type: ignore[assignment]
Expand All @@ -410,6 +408,8 @@ async def _update(self) -> EvoDhwStatusResponseT | EvoZonStatusResponseT:
def _update_status(
self, status: EvoDhwStatusResponseT | EvoZonStatusResponseT
) -> None:
"""Update the DHW/ZON's status."""

self._update_faults(status["active_faults"])
self._status = status

Expand Down
4 changes: 2 additions & 2 deletions tests/tests/test_v2_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,10 +199,10 @@ async def test_token_manager(
assert await cache_manager.get_access_token() == "new_access_token..."

assert caplog.records[0].message == (
"Null/Expired/Invalid access_token, re-authenticating."
"Null/Expired/Invalid access_token, will re-authenticate..."
)
assert caplog.records[1].message == (
"Authenticating with the refresh_token..."
"Authenticating with the refresh_token"
)
assert caplog.records[2].message.startswith(
"POST https://tccna.resideo.com/Auth/OAuth/Token"
Expand Down
Loading

0 comments on commit d0f62d0

Please sign in to comment.