Skip to content

Commit 6855fd6

Browse files
authored
Configurable timezone for Datetime device using localtime (#1663)
* Configurable timezone for `Datetime` device using `localtime` * simplify
1 parent 12ad7c9 commit 6855fd6

File tree

4 files changed

+68
-11
lines changed

4 files changed

+68
-11
lines changed

docs/changelog.md

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ nav_order: 2
66

77
# Changelog
88

9+
### Devices
10+
11+
- Datetime: Accept `datetime.tzinfo` for `timezone` argument to send time information for specific timezone. Boolean works like before: If `True` use system localtime. If `False` an arbitrary time can be sent.
12+
913
### DPT
1014

1115
- Add `DPTBase.dpt_number_str` and `DPTBase.dpt_name` classmethods for human readable DPT number (eg. "9.001") and class name (eg. "DPTTemperature (9.001)").

docs/time.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ await xknx.devices['TimeTest'].sync()
2828
* `xknx` is the XKNX object.
2929
* `name` is the name of the object.
3030
* `group_address` is the KNX group address of the sensor device.
31-
* `localtime` If set `True` sync() and GroupValueRead requests always return the current local time and it is also sent every 60 minutes. On `False` the set value will be sent. Default: `True`
31+
* `localtime` If set `True` sync() and GroupValueRead requests always return the current systems local time and it is also sent every 60 minutes. Same if set to a `datetime.tzinfo` object, but the time for that timezone information will be used. On `False` the set value will be sent, no automatic sending will be scheduled. Default: `True`
3232
* `device_updated_cb` Callback for each update.
3333

3434
## [](#header-2)Local time

test/devices_tests/datetime_test.py

+49-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import datetime as dt
44
from unittest.mock import AsyncMock, patch
5+
from zoneinfo import ZoneInfo
56

67
from freezegun import freeze_time
78
import pytest
@@ -104,10 +105,21 @@ async def test_sync_time_custom(self) -> None:
104105
assert telegram.destination_address == GroupAddress("1/2/4")
105106
assert isinstance(telegram.payload, GroupValueRead)
106107

107-
async def test_process_read_localtime(self) -> None:
108+
@pytest.mark.parametrize(
109+
("expected", "localtime"),
110+
[
111+
((0xC9, 0xD, 0xE), True),
112+
((0xCA, 0xD, 0xE), ZoneInfo("Europe/Vienna")),
113+
],
114+
)
115+
async def test_process_read_localtime_time(
116+
self, expected: tuple[int, int, int], localtime: bool | dt.tzinfo
117+
) -> None:
108118
"""Test test process a read telegram from KNX bus."""
109119
xknx = XKNX()
110-
test_device = TimeDevice(xknx, "TestDateTime", group_address="1/2/3")
120+
test_device = TimeDevice(
121+
xknx, "TestTime", group_address="1/2/3", localtime=localtime
122+
)
111123

112124
telegram_read = Telegram(
113125
destination_address=GroupAddress("1/2/3"), payload=GroupValueRead()
@@ -118,7 +130,41 @@ async def test_process_read_localtime(self) -> None:
118130
telegram = xknx.telegrams.get_nowait()
119131
assert telegram == Telegram(
120132
destination_address=GroupAddress("1/2/3"),
121-
payload=GroupValueResponse(DPTArray((0xC9, 0xD, 0xE))),
133+
payload=GroupValueResponse(DPTArray(expected)),
134+
)
135+
136+
@pytest.mark.parametrize(
137+
("expected", "localtime"),
138+
[
139+
(
140+
(0x75, 0x07, 0x0B, 0x49, 0x0D, 0x0E, 0x20, 0xC0),
141+
True,
142+
),
143+
(
144+
(0x75, 0x07, 0x0B, 0x4B, 0x0D, 0x0E, 0x21, 0xC0),
145+
ZoneInfo("Europe/Vienna"),
146+
),
147+
],
148+
)
149+
async def test_process_read_localtime_datetime(
150+
self, expected: tuple[int, int, int], localtime: bool | dt.tzinfo
151+
) -> None:
152+
"""Test test process a read telegram from KNX bus."""
153+
xknx = XKNX()
154+
test_device = DateTimeDevice(
155+
xknx, "TestDateTime", group_address="1/2/3", localtime=localtime
156+
)
157+
158+
telegram_read = Telegram(
159+
destination_address=GroupAddress("1/2/3"), payload=GroupValueRead()
160+
)
161+
with freeze_time("2017-07-11 09:13:14"): # summer time
162+
test_device.process(telegram_read)
163+
164+
telegram = xknx.telegrams.get_nowait()
165+
assert telegram == Telegram(
166+
destination_address=GroupAddress("1/2/3"),
167+
payload=GroupValueResponse(DPTArray(expected)),
122168
)
123169

124170
async def test_process_read_custom_time(self) -> None:

xknx/devices/datetime.py

+14-7
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def __init__(
5151
self,
5252
xknx: XKNX,
5353
name: str,
54-
localtime: bool = True,
54+
localtime: bool | datetime.tzinfo = True,
5555
group_address: GroupAddressesType = None,
5656
group_address_state: GroupAddressesType = None,
5757
respond_to_read: bool = False,
@@ -60,7 +60,10 @@ def __init__(
6060
) -> None:
6161
"""Initialize DateTime class."""
6262
super().__init__(xknx, name, device_updated_cb)
63-
self.localtime = localtime
63+
self.localtime = bool(localtime)
64+
self._localtime_zone = (
65+
localtime if isinstance(localtime, datetime.tzinfo) else None
66+
)
6467
if localtime and group_address_state is not None:
6568
logger.warning(
6669
"State address invalid in %s device when using `localtime=True`. Ignoring `group_address_state=%s` argument.",
@@ -169,7 +172,7 @@ async def set(self, value: KNXTime | datetime.time) -> None:
169172

170173
def broadcast_localtime(self, response: bool = False) -> None:
171174
"""Broadcast the local time to KNX bus."""
172-
now = datetime.datetime.now()
175+
now = datetime.datetime.now(self._localtime_zone)
173176
knx_time = KNXTime.from_time(now.time())
174177
knx_time.day = KNXDay(now.weekday() + 1)
175178
self.remote_value.set(knx_time, response=response)
@@ -193,7 +196,7 @@ async def set(self, value: KNXDate | datetime.date) -> None:
193196

194197
def broadcast_localtime(self, response: bool = False) -> None:
195198
"""Broadcast the local date to KNX bus."""
196-
now = datetime.datetime.now()
199+
now = datetime.datetime.now(self._localtime_zone)
197200
self.remote_value.set(KNXDate.from_date(now.date()), response=response)
198201

199202

@@ -219,11 +222,15 @@ async def set(self, value: KNXDateTime | datetime.datetime) -> None:
219222

220223
def broadcast_localtime(self, response: bool = False) -> None:
221224
"""Broadcast the local date/time to KNX bus."""
222-
time_now = time.localtime()
223-
now = datetime.datetime.now()
225+
now = datetime.datetime.now(self._localtime_zone)
224226
knx_datetime = KNXDateTime.from_datetime(now)
225227
knx_datetime.day_of_week = KNXDayOfWeek(now.weekday() + 1)
226-
knx_datetime.dst = time_now.tm_isdst > 0
228+
if self._localtime_zone is not None:
229+
dst = self._localtime_zone.dst(now)
230+
knx_datetime.dst = dst > datetime.timedelta(0) if dst is not None else False
231+
else:
232+
time_now = time.localtime()
233+
knx_datetime.dst = time_now.tm_isdst > 0
227234
knx_datetime.external_sync = True
228235
knx_datetime.source_reliable = True
229236
self.remote_value.set(knx_datetime, response=response)

0 commit comments

Comments
 (0)