Skip to content

Commit 49eb912

Browse files
committed
refactor how TZ info is managed, bump to 1.0.1b0
1 parent 34544ed commit 49eb912

File tree

7 files changed

+230
-167
lines changed

7 files changed

+230
-167
lines changed

src/evohome/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
from __future__ import annotations
44

5-
__version__ = "1.0.0b0"
5+
__version__ = "1.0.1b0"

src/evohome/time_zone.py

+177
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
"""evohomeasync provides an async client for the Resideo TCC API."""
2+
3+
from __future__ import annotations
4+
5+
from datetime import datetime as dt, timedelta as td, tzinfo
6+
from typing import TYPE_CHECKING, Final
7+
8+
from .windows_zones import WINDOWS_TO_IANA_LOOKUP
9+
10+
if TYPE_CHECKING:
11+
from evohomeasync2.schemas.typedefs import EvoTimeZoneInfoT
12+
13+
SZ_CURRENT_OFFSET_MINUTES: Final = "current_offset_minutes"
14+
SZ_OFFSET_MINUTES: Final = "offset_minutes"
15+
SZ_TIME_ZONE_ID: Final = "time_zone_id"
16+
17+
18+
def iana_tz_from_windows_tz(time_zone: str) -> str:
19+
"""Return the IANA TZ identifier from the Windows TZ id."""
20+
return WINDOWS_TO_IANA_LOOKUP[time_zone.replace(" ", "").replace(".", "")]
21+
22+
23+
# it is ostensibly optional to provide this data to the EvoZoneInfo class
24+
_DEFAULT_TIME_ZONE_INFO: EvoTimeZoneInfoT = {
25+
SZ_TIME_ZONE_ID: "GMTStandardTime",
26+
"display_name": "(UTC+00:00) Dublin, Edinburgh, Lisbon, London",
27+
SZ_OFFSET_MINUTES: 0,
28+
SZ_CURRENT_OFFSET_MINUTES: 0,
29+
"supports_daylight_saving": False,
30+
}
31+
32+
33+
# Currently used only by the newer API (minor changes needed for older API)...
34+
class EvoZoneInfo(tzinfo):
35+
"""Return a tzinfo object based on a TCC location's time zone information.
36+
37+
The location does not know its IANA time zone, only its offsets, so:
38+
- this tzinfo object must be informed when the DST has started/stopped
39+
- the `tzname` name is based upon the Windows scheme
40+
"""
41+
42+
_time_zone_info: EvoTimeZoneInfoT
43+
_use_dst_switching: bool
44+
45+
_utcoffset: td
46+
_dst: td
47+
48+
# NOTE: from https://docs.python.org/3/library/datetime.html#datetime.tzinfo:
49+
# Special requirement for pickling: A tzinfo subclass must have an __init__()
50+
# method that can be called with no arguments, otherwise it can be pickled but
51+
# possibly not unpickled again. This is a technical requirement that may be
52+
# relaxed in the future.
53+
54+
def __init__( #
55+
self,
56+
*,
57+
time_zone_info: EvoTimeZoneInfoT | None = _DEFAULT_TIME_ZONE_INFO,
58+
use_dst_switching: bool | None = False,
59+
) -> None:
60+
"""Initialise the class."""
61+
super().__init__()
62+
63+
if time_zone_info is None:
64+
time_zone_info = _DEFAULT_TIME_ZONE_INFO
65+
66+
self._update(
67+
time_zone_info=time_zone_info, use_dst_switching=bool(use_dst_switching)
68+
)
69+
70+
# tzinfo.utcoffset(dt | None)
71+
# tzinfo.dst(dt | None)
72+
# tzinfo.tzname(dt | None)
73+
# tzinfo.fromutc(now())
74+
# now().astimezone(tzinfo | None)
75+
76+
def __repr__(self) -> str:
77+
return (
78+
f"{self.__class__.__name__}({self._utcoffset!r}, "
79+
f"'{self._time_zone_info[SZ_TIME_ZONE_ID]}', is_dst={bool(self._dst)})"
80+
)
81+
82+
def _update(
83+
self,
84+
*,
85+
time_zone_info: EvoTimeZoneInfoT | None = None,
86+
use_dst_switching: bool | None = None,
87+
) -> None:
88+
"""Update the TZ information and DST configuration.
89+
90+
This is not a standard method for tzinfo objects, but a custom one for this
91+
class.
92+
93+
So this object can correctly maintain its `utcoffset` and `dst` attrs, this
94+
method should be called (on instantiation and):
95+
- when the time zone enters or leaves DST
96+
- when the location starts or stops using DST switching (not expected)
97+
"""
98+
99+
if time_zone_info is not None: # TZ shouldn't change, only the current offset
100+
self._time_zone_info = time_zone_info
101+
102+
self._utcoffset = td(minutes=time_zone_info[SZ_CURRENT_OFFSET_MINUTES])
103+
self._dst = self._utcoffset - td(minutes=time_zone_info[SZ_OFFSET_MINUTES])
104+
105+
self._tzname = time_zone_info[SZ_TIME_ZONE_ID] + (
106+
" (DST)" if self._dst else " (STD)"
107+
)
108+
109+
if use_dst_switching is not None:
110+
self._use_dst_switching = use_dst_switching
111+
112+
# if not self._use_dst_switching:
113+
# assert self._dst == td(0), "DST is not enabled, but the offset is non-zero"
114+
115+
def dst(self, dtm: dt | None) -> td:
116+
"""Return the daylight saving time adjustment, as a timedelta object.
117+
118+
Return 0 if DST not in effect. utcoffset() must include the DST offset.
119+
"""
120+
121+
if dtm and self._dst: # we don't know when DST starts/stops
122+
raise NotImplementedError("DST transitions are not implemented")
123+
124+
return self._dst
125+
126+
def tzname(self, dtm: dt | None) -> str:
127+
"datetime -> string name of time zone."
128+
return self._tzname
129+
130+
def utcoffset(self, dtm: dt | None) -> td:
131+
"""Return offset of local time from UTC, as a timedelta object.
132+
133+
The timedelta is positive east of UTC. If local time is west of UTC, this
134+
should be negative.
135+
"""
136+
137+
return self._utcoffset
138+
139+
140+
### TZ data for v2 location (newer API)...
141+
142+
# _ = {
143+
# "locationInfo": {
144+
# "locationId": "2738909",
145+
# "useDaylightSaveSwitching": True,
146+
# "timeZone": {
147+
# "timeZoneId": "GMTStandardTime",
148+
# "displayName": "(UTC+00:00) Dublin, Edinburgh, Lisbon, London",
149+
# "offsetMinutes": 0,
150+
# "currentOffsetMinutes": 60,
151+
# "supportsDaylightSaving": True
152+
# }
153+
# }
154+
# }
155+
156+
# examples of known display_name: key-value pairs:
157+
# "GMTStandardTime": "(UTC+00:00) Dublin, Edinburgh, Lisbon, London",
158+
# "CentralEuropeStandardTime": "(UTC+01:00) Praha, Bratislava, Budapešť, Bělehrad, Lublaň",
159+
# "RomanceStandardTime": "(UTC+01:00) Brussels, Copenhagen, Madrid, Paris",
160+
# "WEuropeStandardTime": "(UTC+01:00) Amsterdam, Berlijn, Bern, Rome, Stockholm, Wenen",
161+
# "FLEStandardTime": "(UTC+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius",
162+
# "AUSEasternStandardTime": "(UTC+10:00) Canberra, Melbourne, Sydney",
163+
164+
165+
### TZ data for v1 location (older API)...
166+
167+
# _ = {
168+
# "locationID": 2738909, # NOTE: is an integer
169+
# "daylightSavingTimeEnabled": True, # NOTE: different key
170+
# "timeZone": {
171+
# "id": "GMT Standard Time", # NOTE: different key, spaces in value
172+
# "displayName": "(UTC+00:00) Dublin, Edinburgh, Lisbon, London",
173+
# "offsetMinutes": 0,
174+
# "currentOffsetMinutes": 60,
175+
# "usingDaylightSavingTime": True # NOTE: different key
176+
# }
177+
# }

src/evohome/windows_zones.py

+11
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"Samoa Standard Time": "Pacific/Apia",
4141
"Line Islands Standard Time": "Pacific/Kiritimati",
4242
}
43+
4344
WINDOWS_TO_IANA_ASIA = {
4445
"Jordan Standard Time": "Asia/Amman",
4546
"Middle East Standard Time": "Asia/Beirut",
@@ -87,6 +88,7 @@
8788
"Sakhalin Standard Time": "Asia/Sakhalin",
8889
"Russia Time Zone 11": "Asia/Kamchatka",
8990
}
91+
9092
WINDOWS_TO_IANA_NASA = {
9193
"Aleutian Standard Time": "America/Adak",
9294
"Alaskan Standard Time": "America/Anchorage",
@@ -124,6 +126,7 @@
124126
"Saint Pierre Standard Time": "America/Miquelon",
125127
"Bahia Standard Time": "America/Bahia",
126128
}
129+
127130
WINDOWS_TO_IANA_EMEA = {
128131
"Azores Standard Time": "Atlantic/Azores",
129132
"Cape Verde Standard Time": "Atlantic/Cape_Verde",
@@ -156,3 +159,11 @@
156159
"Saratov Standard Time": "Europe/Saratov",
157160
"Volgograd Standard Time": "Europe/Volgograd",
158161
}
162+
163+
# TCC is seen in EMEA, ASIA and APAC but not in the Americas
164+
WINDOWS_TO_IANA_LOOKUP = {
165+
k.replace(" ", "").replace(".", ""): v
166+
for k, v in (
167+
WINDOWS_TO_IANA_EMEA | WINDOWS_TO_IANA_ASIA | WINDOWS_TO_IANA_APAC
168+
).items()
169+
}

src/evohomeasync/entities.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import logging
3636
from datetime import datetime as dt
3737

38+
# some methods emulate the v2 API...
3839
from evohomeasync2.schemas.typedefs import EvoTemperatureStatusResponseT
3940

4041
from . import EvohomeClient
@@ -124,7 +125,7 @@ def idx(self) -> str:
124125

125126
# Status (state) attrs & methods...
126127

127-
@property # emulate v2 API...
128+
@property # emulate the v2 API...
128129
def temperature_status(self) -> EvoTemperatureStatusResponseT:
129130
"""Expose the temperature_status as per the v2 API."""
130131

@@ -220,7 +221,7 @@ async def get_zone_modes(self) -> list[str]:
220221

221222
# Status (state) attrs & methods...
222223

223-
@property # emulate v2 API...
224+
@property # emulate the v2 API...
224225
def temperature_status(self) -> EvoTemperatureStatusResponseT:
225226
"""Expose the temperature_status as per the v2 API."""
226227

0 commit comments

Comments
 (0)