Skip to content

Commit 6513d39

Browse files
authored
Fix datetime.datetime.utcnow() deprecation warning (#4158)
* Do not use `utcnow` * Avoid manual `datetime` patching * Fix unit tests * Use freezegun for tests * Revert "Do not use `utcnow`" This reverts commit f47b4f2. * Reapply "Do not use `utcnow`" This reverts commit 46aa296. * Switch to `time-machine` * Use function-level mocks * Mock things exactly * Pass around entire `datetime` object, not just years
1 parent be5f15c commit 6513d39

File tree

11 files changed

+43
-79
lines changed

11 files changed

+43
-79
lines changed

requirements_test.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ pytest-asyncio
1515
pytest>=7.1.3
1616
zigpy>=0.80.0
1717
ruff==0.9.4 # Keep this in sync with .pre-commit-config.yaml
18+
time-machine<3,>=2

tests/common.py

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Quirks common helpers."""
22

33
import asyncio
4-
import datetime
54

65
ZCL_IAS_MOTION_COMMAND = b"\t!\x00\x01\x00\x00\x00\x00\x00"
76
ZCL_OCC_ATTR_RPT_OCC = b"\x18d\n\x00\x00\x18\x01"
@@ -25,22 +24,6 @@ def cluster_command(self, tsn, command_id, args):
2524
self.cluster_commands.append((tsn, command_id, args))
2625

2726

28-
class MockDatetime(datetime.datetime):
29-
"""Override for datetime functions."""
30-
31-
@classmethod
32-
def now(cls):
33-
"""Return testvalue."""
34-
35-
return cls(1970, 1, 1, 1, 0, 0)
36-
37-
@classmethod
38-
def utcnow(cls):
39-
"""Return testvalue."""
40-
41-
return cls(1970, 1, 1, 2, 0, 0)
42-
43-
4427
async def wait_for_zigpy_tasks() -> None:
4528
"""Wait for all running zigpy tasks to finish."""
4629
tasks = []

tests/test_tuya.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
import struct
66
from typing import Final
77
from unittest import mock
8+
from zoneinfo import ZoneInfo
89

910
import pytest
11+
import time_machine
1012
from zigpy.device import Device
1113
from zigpy.profiles import zha
1214
from zigpy.quirks import CustomDevice, get_device
@@ -16,7 +18,7 @@
1618
from zigpy.zcl.clusters.security import IasZone, ZoneStatus
1719
from zigpy.zcl.foundation import ZCLAttributeDef
1820

19-
from tests.common import ClusterListener, MockDatetime, wait_for_zigpy_tasks
21+
from tests.common import ClusterListener, wait_for_zigpy_tasks
2022
import zhaquirks
2123
from zhaquirks.const import (
2224
DEVICE_TYPE,
@@ -723,6 +725,7 @@ async def async_success(*args, **kwargs):
723725
assert status == foundation.Status.UNSUP_CLUSTER_COMMAND
724726

725727

728+
@time_machine.travel(datetime.datetime(1970, 1, 1, 1, 0, tzinfo=ZoneInfo("Etc/GMT+1")))
726729
@pytest.mark.parametrize("quirk", (zhaquirks.tuya.ts0601_trv.MoesHY368_Type1,))
727730
async def test_moes(zigpy_device_from_quirk, quirk):
728731
"""Test thermostatic valve outgoing commands."""
@@ -1312,9 +1315,6 @@ async def async_success(*args, **kwargs):
13121315
_, status = await onoff_cluster.command(0x0009)
13131316
assert status == foundation.Status.UNSUP_CLUSTER_COMMAND
13141317

1315-
origdatetime = datetime.datetime
1316-
datetime.datetime = MockDatetime
1317-
13181318
hdr, args = tuya_cluster.deserialize(ZCL_TUYA_SET_TIME_REQUEST)
13191319
tuya_cluster.handle_message(hdr, args)
13201320
await wait_for_zigpy_tasks()
@@ -1329,7 +1329,6 @@ async def async_success(*args, **kwargs):
13291329
ask_for_ack=None,
13301330
priority=None,
13311331
)
1332-
datetime.datetime = origdatetime
13331332

13341333

13351334
@pytest.mark.parametrize("quirk", (zhaquirks.tuya.ts0601_electric_heating.MoesBHT,))

tests/test_tuya_builder.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,18 @@
22

33
import datetime
44
from unittest import mock
5+
from zoneinfo import ZoneInfo
56

67
import pytest
8+
import time_machine
79
from zigpy.quirks.registry import DeviceRegistry
810
from zigpy.quirks.v2 import CustomDeviceV2
911
import zigpy.types as t
1012
from zigpy.zcl import foundation
1113
from zigpy.zcl.clusters.general import Basic
1214
from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement
1315

14-
from tests.common import ClusterListener, MockDatetime, wait_for_zigpy_tasks
16+
from tests.common import ClusterListener, wait_for_zigpy_tasks
1517
import zhaquirks
1618
from zhaquirks.const import BatterySize
1719
from zhaquirks.tuya import (
@@ -407,6 +409,7 @@ async def test_tuya_spell(device_mock, read_attr_spell, data_query_spell):
407409
request_mock.reset_mock()
408410

409411

412+
@time_machine.travel(datetime.datetime(1970, 1, 1, 1, 0, tzinfo=ZoneInfo("Etc/GMT+1")))
410413
async def test_tuya_mcu_set_time(device_mock):
411414
"""Test TuyaQuirkBuilder replacement cluster, set_time requests (0x24) messages for MCU devices."""
412415

@@ -430,10 +433,6 @@ async def test_tuya_mcu_set_time(device_mock):
430433
TUYA_SET_TIME
431434
].is_manufacturer_specific
432435

433-
# Mock datetime
434-
origdatetime = datetime.datetime
435-
datetime.datetime = MockDatetime
436-
437436
# simulate a SET_TIME message
438437
hdr, args = ep.tuya_manufacturer.deserialize(ZCL_TUYA_SET_TIME)
439438
assert hdr.command_id == TUYA_SET_TIME
@@ -450,8 +449,6 @@ async def test_tuya_mcu_set_time(device_mock):
450449
assert not res_hdr[0].manufacturer
451450
assert not res_hdr[0].frame_control.is_manufacturer_specific
452451

453-
datetime.datetime = origdatetime # restore datetime
454-
455452

456453
@pytest.mark.parametrize(
457454
"force",

tests/test_tuya_mcu.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
import datetime
44
from unittest import mock
5+
from zoneinfo import ZoneInfo
56

67
import pytest
8+
import time_machine
79
from zigpy.zcl import foundation
810

9-
from tests.common import ClusterListener, MockDatetime
11+
from tests.common import ClusterListener
1012
import zhaquirks
1113
from zhaquirks.tuya import TUYA_MCU_VERSION_RSP, TUYA_SET_TIME, TuyaDPType
1214
from zhaquirks.tuya.mcu import (
@@ -175,6 +177,7 @@ async def test_tuya_version(zigpy_device_from_quirk, quirk):
175177
assert succ["mcu_version"] == "2.0.2"
176178

177179

180+
@time_machine.travel(datetime.datetime(1970, 1, 1, 1, 0, tzinfo=ZoneInfo("Etc/GMT+1")))
178181
@pytest.mark.parametrize(
179182
"quirk", (zhaquirks.tuya.ts0601_dimmer.TuyaDoubleSwitchDimmer,)
180183
)
@@ -186,10 +189,6 @@ async def test_tuya_mcu_set_time(zigpy_device_from_quirk, quirk):
186189
tuya_cluster = tuya_device.endpoints[1].tuya_manufacturer
187190
cluster_listener = ClusterListener(tuya_cluster)
188191

189-
# Mock datetime
190-
origdatetime = datetime.datetime
191-
datetime.datetime = MockDatetime
192-
193192
# simulate a SET_TIME message
194193
hdr, args = tuya_cluster.deserialize(ZCL_TUYA_SET_TIME)
195194
assert hdr.command_id == TUYA_SET_TIME
@@ -206,9 +205,6 @@ async def test_tuya_mcu_set_time(zigpy_device_from_quirk, quirk):
206205
TUYA_SET_TIME, [0, 0, 28, 32, 0, 0, 14, 16], expect_reply=False
207206
)
208207

209-
# restore datetime
210-
datetime.datetime = origdatetime # restore datetime
211-
212208

213209
@pytest.mark.parametrize(
214210
"quirk", (zhaquirks.tuya.ts0601_dimmer.TuyaDoubleSwitchDimmer,)

tests/test_tuya_valve.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
"""Tests for Tuya quirks."""
22

3-
from datetime import datetime, timezone
3+
from datetime import datetime
44
from unittest import mock
5-
from unittest.mock import patch
65

76
import pytest
7+
import time_machine
88
from zigpy.quirks.v2 import EntityMetadata
99
from zigpy.zcl import ClusterType, foundation
1010

@@ -182,16 +182,7 @@ async def test_giex_functions():
182182
assert zhaquirks.tuya.tuya_valve.giex_string_to_td("12:01:05,3") == 43265
183183
assert zhaquirks.tuya.tuya_valve.giex_string_to_dt("--:--:--") is None
184184

185-
class MockDatetime:
186-
def now(self, tz: timezone):
187-
"""Mock now."""
188-
return datetime(2024, 10, 2, 12, 10, 23, tzinfo=tz)
189-
190-
def strptime(self, v: str, fmt: str):
191-
"""Mock strptime."""
192-
return datetime.strptime(v, fmt)
193-
194-
with patch("zhaquirks.tuya.tuya_valve.datetime", MockDatetime()):
185+
with time_machine.travel("2024-10-02 12:10:23 +0100"):
195186
assert zhaquirks.tuya.tuya_valve.giex_string_to_dt(
196187
"20:12:01"
197188
) == datetime.fromisoformat("2024-10-02T20:12:01+04:00")

zhaquirks/tuya/__init__.py

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -366,8 +366,8 @@ class TuyaManufCluster(CustomCluster):
366366
name = "Tuya Manufacturer Specicific"
367367
cluster_id = TUYA_CLUSTER_ID
368368
ep_attribute = "tuya_manufacturer"
369-
set_time_offset = 0
370-
set_time_local_offset = None
369+
set_time_offset: datetime.datetime | None = None
370+
set_time_local_offset: datetime.datetime | None = None
371371

372372
# remove manufacturer id for cluster, important for `TUYA_SET_DATA` commands
373373
manufacturer_id_override: t.uint16_t = foundation.ZCLHeader.NO_MANUFACTURER_ID
@@ -450,7 +450,7 @@ def handle_cluster_request(
450450
) -> None:
451451
"""Handle time request."""
452452

453-
if hdr.command_id != 0x0024 or self.set_time_offset == 0:
453+
if hdr.command_id != 0x0024 or self.set_time_offset is None:
454454
return super().handle_cluster_request(
455455
hdr, args, dst_addressing=dst_addressing
456456
)
@@ -466,20 +466,15 @@ def handle_cluster_request(
466466
self.cluster_id,
467467
hdr.command_id,
468468
)
469+
470+
assert self.set_time_local_offset is not None
471+
469472
payload = TuyaTimePayload()
470473
utc_timestamp = int(
471-
(
472-
datetime.datetime.utcnow() # noqa: DTZ003
473-
- datetime.datetime(self.set_time_offset, 1, 1)
474-
).total_seconds()
474+
(datetime.datetime.now(datetime.UTC) - self.set_time_offset).total_seconds()
475475
)
476476
local_timestamp = int(
477-
(
478-
datetime.datetime.now()
479-
- datetime.datetime(
480-
self.set_time_local_offset or self.set_time_offset, 1, 1
481-
)
482-
).total_seconds()
477+
(datetime.datetime.now() - self.set_time_local_offset).total_seconds()
483478
)
484479
payload.extend(utc_timestamp.to_bytes(4, "big", signed=False))
485480
payload.extend(local_timestamp.to_bytes(4, "big", signed=False))

zhaquirks/tuya/mcu/__init__.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,8 @@ class TuyaConnectionStatus(t.Struct):
152152
class TuyaMCUCluster(TuyaAttributesCluster, TuyaNewManufCluster):
153153
"""Manufacturer specific cluster for sending Tuya MCU commands."""
154154

155-
set_time_offset = 1970 # MCU timestamp from 1/1/1970
156-
set_time_local_offset = None
155+
set_time_offset = datetime.datetime(1970, 1, 1, tzinfo=datetime.UTC)
156+
set_time_local_offset = datetime.datetime(1970, 1, 1)
157157

158158
# TODO: Backwards compatibility, remove
159159
MCUVersion = MCUVersion
@@ -311,16 +311,12 @@ def handle_set_time_request(self, payload: t.uint16_t) -> foundation.Status:
311311
self.debug("handle_set_time_request payload: %s", payload)
312312
payload_rsp = TuyaTimePayload()
313313

314-
utc_now = datetime.datetime.utcnow() # noqa: DTZ003
315-
now = datetime.datetime.now()
316-
317-
offset_time = datetime.datetime(self.set_time_offset, 1, 1)
318-
offset_time_local = datetime.datetime(
319-
self.set_time_local_offset or self.set_time_offset, 1, 1
314+
utc_timestamp = int(
315+
(datetime.datetime.now(datetime.UTC) - self.set_time_offset).total_seconds()
316+
)
317+
local_timestamp = int(
318+
(datetime.datetime.now() - self.set_time_local_offset).total_seconds()
320319
)
321-
322-
utc_timestamp = int((utc_now - offset_time).total_seconds())
323-
local_timestamp = int((now - offset_time_local).total_seconds())
324320

325321
payload_rsp.extend(utc_timestamp.to_bytes(4, "big", signed=False))
326322
payload_rsp.extend(local_timestamp.to_bytes(4, "big", signed=False))

zhaquirks/tuya/ts0601_haozee.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Map from manufacturer to standard clusters for thermostatic valves."""
22

3+
import datetime
34
from typing import Final
45

56
import zigpy.profiles.zha
@@ -79,8 +80,8 @@ class HY08WEManufCluster(TuyaManufClusterAttributes):
7980
"""Manufacturer Specific Cluster of some thermostatic valves."""
8081

8182
# Important! This device uses offset from 2000 year for UTC time and offset from 1970 for local time
82-
set_time_offset = 2000
83-
set_time_local_offset = 1970
83+
set_time_offset = datetime.datetime(2000, 1, 1, tzinfo=datetime.UTC)
84+
set_time_local_offset = datetime.datetime(1970, 1, 1)
8485

8586
class AttributeDefs(TuyaManufClusterAttributes.AttributeDefs):
8687
"""Attribute definitions."""

zhaquirks/tuya/ts0601_trv.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Map from manufacturer to standard clusters for thermostatic valves."""
22

3+
import datetime
34
import logging
45
from typing import Final, Optional, Union
56

@@ -54,7 +55,8 @@
5455
class SiterwellManufCluster(TuyaManufClusterAttributes):
5556
"""Manufacturer Specific Cluster of some thermostatic valves."""
5657

57-
set_time_offset = 1970
58+
set_time_offset = datetime.datetime(1970, 1, 1, tzinfo=datetime.UTC)
59+
set_time_local_offset = datetime.datetime(1970, 1, 1)
5860

5961
class AttributeDefs(TuyaManufClusterAttributes.AttributeDefs):
6062
"""Attribute definitions."""
@@ -216,7 +218,8 @@ class data144(t.FixedList, item_type=t.uint8_t, length=18):
216218
class MoesManufCluster(TuyaManufClusterAttributes):
217219
"""Manufacturer Specific Cluster of some thermostatic valves."""
218220

219-
set_time_offset = 1970
221+
set_time_offset = datetime.datetime(1970, 1, 1, tzinfo=datetime.UTC)
222+
set_time_local_offset = datetime.datetime(1970, 1, 1)
220223

221224
class AttributeDefs(TuyaManufClusterAttributes.AttributeDefs):
222225
"""Attribute definitions."""

0 commit comments

Comments
 (0)