Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Ongoing

- PR [400](https://github.com/plugwise/python-plugwise-usb/pull/400): Fix for Issue [#399](https://github.com/plugwise/python-plugwise-usb/issues/399)
- Test/validate for Python 3.14

## v0.47.1 - 2025-09-27
Expand Down
12 changes: 10 additions & 2 deletions plugwise_usb/nodes/circle.py
Original file line number Diff line number Diff line change
Expand Up @@ -880,9 +880,14 @@ async def clock_synchronize(self) -> bool:
return False

dt_now = datetime.now(tz=UTC)
days_diff = (response.day_of_week.value - dt_now.weekday()) % 7
if dt_now.weekday() != response.day_of_week.value:
_LOGGER.info(
"Sync clock of node %s because time has drifted more than 1 day",
self._mac_in_str,
)
return await self._send_clock_set_req()
Comment thread
coderabbitai[bot] marked this conversation as resolved.

circle_timestamp: datetime = dt_now.replace(
day=dt_now.day + days_diff,
hour=response.time.value.hour,
minute=response.time.value.minute,
second=response.time.value.second,
Expand All @@ -902,7 +907,10 @@ async def clock_synchronize(self) -> bool:
raise NodeError(
"Unable to synchronize clock when protocol version is unknown"
)
return await self._send_clock_set_req()

async def _send_clock_set_req(self) -> bool:
"""Send CirclePlusRealTimeClockSetRequest."""
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
set_request = CircleClockSetRequest(
self._send,
self._mac_in_bytes,
Expand Down
5 changes: 5 additions & 0 deletions tests/stick_test_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,11 @@
b"000000C1", # Success ack
b"0000" + b"00D7" + b"0098765432101234", # msg_id, clock_ack, mac
),
b"\x05\x05\x03\x0300281111111111111111003010053101261F3D\r\n": (
"Circle+ Realtime set clock at month-end for 1111111111111111",
b"000000C1", # Success ack
b"0000" + b"00D7" + b"1111111111111111", # msg_id, clock_ack, mac
),
b"\x05\x05\x03\x03003E11111111111111111B8A\r\n": (
"clock for 0011111111111111",
b"000000C1", # Success ack
Expand Down
39 changes: 39 additions & 0 deletions tests/test_usb.py
Original file line number Diff line number Diff line change
Expand Up @@ -3039,3 +3039,42 @@ def fake_cache_bool(dummy: object, setting: str) -> bool | None:
with patch("aiofiles.threadpool.sync_open", return_value=mock_file_stream):
await stick.disconnect()
await asyncio.sleep(1)

@freeze_time("2026-01-31 10:30:00", real_asyncio=True)
@pytest.mark.asyncio
async def test_clock_synchronize_month_overflow(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Test clock_synchronize handles month-end date rollover correctly.

Regression test for issue `#399`: ensures that when the Circle's day_of_week
differs from the current weekday near month-end, the date calculation
doesn't attempt an invalid day value (e.g., Jan 32).
"""
mock_serial = MockSerial(None)
monkeypatch.setattr(
pw_connection_manager,
"create_serial_connection",
mock_serial.mock_connection,
)
monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.2)
monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 2.0)

stick = pw_stick.Stick("test_port", cache_enabled=False)
await stick.connect()
await stick.initialize()
await stick.discover_nodes(load=False)
await self._wait_for_scan(stick)

# Get a Circle node
circle = stick.nodes.get("1111111111111111")
assert circle is not None
result = True
try:
await circle.load()
except ValueError:
result = False

assert result

await stick.disconnect()