Skip to content

Commit

Permalink
work on attrs
Browse files Browse the repository at this point in the history
  • Loading branch information
zxdavb committed Dec 22, 2024
1 parent 8f59a27 commit d5456d9
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 60 deletions.
17 changes: 13 additions & 4 deletions src/evohomeasync2/schemas/typedefs.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class EvoUsrConfigResponseT(TypedDict):

# GET /locations?userId={user_id}&allData=True returns list of these dicts
class EvoLocConfigResponseT(TypedDict):
"""Response to GET /locations?userId={user_id}&allData=True.
"""Response to GET /locations?userId={user_id}&allData=True
The response is a list of these dicts.
"""
Expand Down Expand Up @@ -103,10 +103,10 @@ class EvoGwyConfigEntryT(TypedDict):
class EvoTcsConfigEntryT(TypedDict):
system_id: str
model_type: TcsModelType
allowed_system_modes: list[EvoAllowedSystemModeT]
allowed_system_modes: list[EvoAllowedSystemModeResponseT]


class EvoAllowedSystemModeT(TypedDict):
class EvoAllowedSystemModeResponseT(TypedDict):
system_mode: SystemMode
can_be_permanent: Literal[True]
can_be_temporary: bool
Expand Down Expand Up @@ -180,6 +180,11 @@ class EvoDhwConfigEntryT(EvoDhwConfigResponseT):

# GET /location/{loc_id}/status?includeTemperatureControlSystems=True returns this dict
class EvoLocStatusResponseT(TypedDict):
"""Response to /location/{loc_id}/status?includeTemperatureControlSystems=True
The response is a dict of of a single location.
"""

location_id: str
gateways: list[EvoGwyStatusResponseT]

Expand All @@ -206,7 +211,7 @@ class EvoTcsStatusResponseT(TypedDict):
class EvoSystemModeStatusResponseT(TypedDict):
mode: SystemMode
is_permanent: bool
until: NotRequired[str]
time_until: NotRequired[str]


class EvoZonStatusResponseT(TypedDict):
Expand Down Expand Up @@ -241,19 +246,23 @@ class EvoDhwStateStatusResponseT(TypedDict):


#######################################################################################
# WIP: These are setters, PUT, url, jason=json...


# PUT
class EvoZoneStatusT(TypedDict):
mode: str
is_permanent: bool


# PUT
class EvoSystemModeStatusT(TypedDict):
mode: SystemMode
is_permanent: bool
time_until: NotRequired[str]


# PUT
class EvoTcsStatusT(TypedDict):
system_id: str
system_mode_status: dict[str, Any] # TODO
Expand Down
56 changes: 33 additions & 23 deletions src/evohomeasync2/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
SZ_DHW,
SZ_DHW_ID,
SZ_ID, # remove?
SZ_IS_PERMANENT,
SZ_MODE,
SZ_MODEL_TYPE,
SZ_NAME,
Expand All @@ -27,6 +28,7 @@
SZ_SYSTEM_MODE_STATUS,
SZ_TEMPERATURE, # remove?
SZ_THERMOSTAT, # remove?
SZ_TIME_UNTIL,
SZ_ZONE_ID,
SZ_ZONES,
)
Expand All @@ -39,7 +41,7 @@
EntityType,
TcsModelType,
)
from .zone import ActiveFaultsBase, Zone
from .zone import ActiveFaultsBase, Zone, as_local_time

if TYPE_CHECKING:
from datetime import datetime as dt
Expand All @@ -50,10 +52,10 @@
from .schemas.typedefs import (
DayOfWeekDhwT,
DayOfWeekZoneT,
EvoAllowedSystemModeT,
EvoAllowedSystemModeResponseT,
EvoScheduleDhwT,
EvoScheduleZoneT,
EvoSystemModeStatusT,
EvoSystemModeStatusResponseT,
EvoTcsConfigEntryT,
EvoTcsConfigResponseT,
EvoTcsStatusResponseT,
Expand Down Expand Up @@ -131,7 +133,16 @@ def systemId(self) -> str: # noqa: N802
return self._id

@property
def allowed_system_modes(self) -> tuple[EvoAllowedSystemModeT, ...]:
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, ...]:
"""
"allowedSystemModes": [
{"systemMode": "HeatingOff", "canBePermanent": true, "canBeTemporary": false},
Expand All @@ -146,36 +157,33 @@ def allowed_system_modes(self) -> tuple[EvoAllowedSystemModeT, ...]:

return tuple(self._config[SZ_ALLOWED_SYSTEM_MODES])

@property # a convenience attr
def mode(self) -> SystemMode | None:
if self.system_mode_status is None:
return None
return self.system_mode_status[SZ_MODE]

@property # a convenience attr
def modes(self) -> tuple[SystemMode, ...]:
return tuple(d[SZ_SYSTEM_MODE] for d in self.allowed_system_modes)

@property
def model(self) -> TcsModelType:
return self._config[SZ_MODEL_TYPE]

@property
def system_mode_status(self) -> EvoSystemModeStatusT | None:
def system_mode_status(self) -> EvoSystemModeStatusResponseT:
"""
"systemModeStatus": {
"mode": "AutoWithEco",
"isPermanent": true
}
"systemModeStatus": {"mode": "AutoWithEco", "isPermanent": true}
"systemModeStatus": {'mode': 'AutoWithEco', 'isPermanent': false, 'timeUntil': '2024-12-21T15:55:00Z'}}
"""
if self._status is None:
raise exc.InvalidStatusError("No system mode status, has it been fetched?")
return self._status[SZ_SYSTEM_MODE_STATUS]

@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 # a convenience attr (could have a setter in future)
def mode(self) -> SystemMode | None:
return self.system_mode_status[SZ_MODE]

@property # a convenience attr (could have a setter in future)
def is_permanent(self) -> bool | None:
return self.system_mode_status[SZ_IS_PERMANENT]

@property # a convenience attr (could have a setter in future)
def until(self) -> dt | None: # aka timeUntil
if (until := self.system_mode_status.get(SZ_TIME_UNTIL)) is None:
return None
return as_local_time(until, self.location.tzinfo)

async def _set_mode(self, mode: dict[str, str | bool]) -> None:
"""Set the TCS mode.""" # {'mode': 'Auto', 'isPermanent': True}
Expand All @@ -185,6 +193,8 @@ async def _set_mode(self, mode: dict[str, str | bool]) -> None:
f"{self}: Unsupported/unknown {S2_SYSTEM_MODE}: {mode}"
)

mode[S2_SYSTEM_MODE] = str(mode[S2_SYSTEM_MODE])

await self._auth.put(f"{self._TYPE}/{self.id}/mode", json=mode)

async def reset(self) -> None:
Expand Down
79 changes: 46 additions & 33 deletions src/evohomeasync2/zone.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@

import voluptuous as vol

from . import ControlSystem
from . import ControlSystem, Location
from .auth import Auth
from .schemas.typedefs import (
DailySchedulesT,
Expand Down Expand Up @@ -149,11 +149,13 @@ def status(
class ActiveFaultsBase(EntityBase):
"""Provide the base for active faults."""

location: Location

def __init__(self, entity_id: str, broker: Auth, logger: logging.Logger) -> None:
super().__init__(entity_id, broker, logger)

self._active_faults: list[EvoActiveFaultResponseT] = []
self._last_logged: dict[str, dt] = {}
self._last_logged: dict[str, dt] = {} # OK to use a tz=UTC datetimes

def _update_faults(
self,
Expand Down Expand Up @@ -187,6 +189,10 @@ def log_as_resolved(fault: EvoActiveFaultResponseT) -> None:
elif dt.now(tz=UTC) - self._last_logged[hash_(fault)] > _ONE_DAY:
log_as_active(fault)

# self._active_faults = [
# {**fault, "since": as_local_time(fault["since"], self.location.tzinfo)}
# for fault in active_faults
# ]
self._active_faults = active_faults
self._last_logged |= last_logged

Expand All @@ -204,12 +210,14 @@ def active_faults(self) -> tuple[EvoActiveFaultResponseT, ...]:
return tuple(self._active_faults)


def as_local_time(dtm: dt, tzinfo: tzinfo) -> dt:
def as_local_time(dtm: dt | str, tzinfo: tzinfo) -> dt:
"""Convert a datetime into a aware datetime in the given TZ."""
if isinstance(dtm, str):
dtm = dt.fromisoformat(dtm)
return dtm.replace(tzinfo=tzinfo) if dtm.tzinfo is None else dtm.astimezone(tzinfo)


def dt_to_dow_and_tod(dtm: dt, tzinfo: tzinfo) -> tuple[DayOfWeek, str]:
def _dt_to_dow_and_tod(dtm: dt, tzinfo: tzinfo) -> tuple[DayOfWeek, str]:
"""Return a pair of strings representing the local day of week and time of day."""
dtm = as_local_time(dtm, tzinfo)
return dtm.strftime("%A"), dtm.strftime("%H:%M") # type: ignore[return-value]
Expand All @@ -222,7 +230,9 @@ def _find_switchpoints(
) -> tuple[SwitchpointT, int, SwitchpointT, int]:
"""Find this/next switchpoints for a given day of week and time of day."""

# assumes >1 switchpoint per day, which could be this_sp or next_sp only
# TODO: schedule can be [], i.e. response was {'DailySchedules': []}

# assumes 1+ switchpoint per day, which could be this_sp or next_sp only

try:
day_idx = list(DayOfWeek).index(day_of_week)
Expand Down Expand Up @@ -347,7 +357,7 @@ def _find_switchpoints(self, dtm: dt | str) -> dict[dt, float | str]:
dtm = as_local_time(dtm, self.location.tzinfo)

this_sp, this_offset, next_sp, next_offset = _find_switchpoints(
self._schedule, *dt_to_dow_and_tod(dtm, self.location.tzinfo)
self._schedule, *_dt_to_dow_and_tod(dtm, self.location.tzinfo)
)

this_tod = dt.strptime(this_sp["time_of_day"], "%H:%M").time() # noqa: DTZ007
Expand All @@ -356,6 +366,7 @@ def _find_switchpoints(self, dtm: dt | str) -> dict[dt, float | str]:
this_dtm = dt.combine(dtm + td(days=this_offset), this_tod)
next_dtm = dt.combine(dtm + td(days=next_offset), next_tod)

# either "dhw_state" or "heat_setpoint" _will_ be present...
this_val = this_sp.get("dhw_state") or this_sp["heat_setpoint"]
next_val = next_sp.get("dhw_state") or next_sp["heat_setpoint"]

Expand Down Expand Up @@ -456,33 +467,13 @@ def __init__(self, tcs: ControlSystem, config: EvoZonConfigResponseT) -> None:
def zoneId(self) -> str: # noqa: N802
return self._id

@property # convenience attr
def max_heat_setpoint(self) -> float:
return self.setpoint_capabilities[SZ_MAX_HEAT_SETPOINT]

@property # convenience attr
def min_heat_setpoint(self) -> float:
return self.setpoint_capabilities[SZ_MIN_HEAT_SETPOINT]

@property
def model(self) -> ZoneModelType:
return self._config[SZ_MODEL_TYPE]

@property # a convenience attr
def mode(self) -> ZoneMode | None:
if not self.setpoint_status:
return None
return self.setpoint_status[SZ_SETPOINT_MODE]

@property # a convenience attr
def modes(self) -> tuple[ZoneMode, ...]:
"""
"allowedSetpointModes": [
"PermanentOverride", "FollowSchedule", "TemporaryOverride"
]
"""

return tuple(self.setpoint_capabilities[SZ_ALLOWED_SETPOINT_MODES])
@property
def type(self) -> ZoneType:
return self._config[SZ_ZONE_TYPE]

@property
def name(self) -> str:
Expand Down Expand Up @@ -519,6 +510,26 @@ def setpoint_capabilities(self) -> EvoZonSetpointCapabilitiesResponseT:
"""
return self._config[SZ_SETPOINT_CAPABILITIES]

@property # convenience attr
def max_heat_setpoint(self) -> float:
# consider: if not self.setpoint_capabilities["can_control_heat"]: return None
return self.setpoint_capabilities[SZ_MAX_HEAT_SETPOINT]

@property # convenience attr
def min_heat_setpoint(self) -> float:
# consider: if not self.setpoint_capabilities["can_control_heat"]: return None
return self.setpoint_capabilities[SZ_MIN_HEAT_SETPOINT]

@property # a convenience attr
def modes(self) -> tuple[ZoneMode, ...]:
"""
"allowedSetpointModes": [
"PermanentOverride", "FollowSchedule", "TemporaryOverride"
]
"""

return tuple(self.setpoint_capabilities[SZ_ALLOWED_SETPOINT_MODES])

@property
def setpoint_status(self) -> EvoZonSetpointStatusResponseT | None:
"""
Expand All @@ -532,15 +543,17 @@ def setpoint_status(self) -> EvoZonSetpointStatusResponseT | None:
return self._status[SZ_SETPOINT_STATUS]

@property # a convenience attr
def mode(self) -> ZoneMode | None:
if not self.setpoint_status:
return None
return self.setpoint_status[SZ_SETPOINT_MODE]

@property # a convenience attr (one day a target_cool_temperature may be added?)
def target_heat_temperature(self) -> float | None:
if self.setpoint_status is None:
return None
return self.setpoint_status[SZ_TARGET_HEAT_TEMPERATURE]

@property
def type(self) -> ZoneType:
return self._config[SZ_ZONE_TYPE]

async def _set_mode(self, mode: dict[str, str | float]) -> None:
"""Set the zone mode (heat_setpoint, cooling is TBD)."""

Expand Down

0 comments on commit d5456d9

Please sign in to comment.