Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
6 changes: 3 additions & 3 deletions plugwise_usb/nodes/circle.py
Original file line number Diff line number Diff line change
Expand Up @@ -880,9 +880,9 @@ 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
circle_timestamp: datetime = dt_now.replace(
day=dt_now.day + days_diff,
days_diff = response.day_of_week.value - dt_now.weekday()
target_date = dt_now + timedelta(days=days_diff)
circle_timestamp = target_date.replace(
hour=response.time.value.hour,
minute=response.time.value.minute,
second=response.time.value.second,
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\x0300280098765432101234003010053101261F3D\r\n": (
"Circle+ Realtime set clock at month-end for 0098765432101234",
b"000000C1", # Success ack
b"0000" + b"00D7" + b"0098765432101234", # msg_id, clock_ack, mac
),
b"\x05\x05\x03\x03003E11111111111111111B8A\r\n": (
"clock for 0011111111111111",
b"000000C1", # Success ack
Expand Down
55 changes: 55 additions & 0 deletions tests/test_usb.py
Original file line number Diff line number Diff line change
Expand Up @@ -3039,3 +3039,58 @@ 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_node = stick.nodes.get("0098765432101234")
assert circle_node is not None
await circle_node.load()

# Mock CircleClockGetRequest.send() to return a response where
# day_of_week is Saturday (5) while frozen time is Friday (4), Jan 31
async def mock_clock_get_send(self):
response = pw_responses.CircleClockResponse()
response.timestamp = dt.now(tz=UTC)
# Set day_of_week to Saturday (5), requiring +1 day from Friday Jan 31
# Old code: Jan 31 + 1 = day 32 (ValueError)
# New code: Jan 31 + timedelta(days=1) = Feb 1 (correct)
response.day_of_week.value = 5 # Saturday
response.time.value = dt.now(tz=UTC).time()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
return response

monkeypatch.setattr(
pw_requests.CircleClockGetRequest,
"send",
mock_clock_get_send,
)

# This should not raise ValueError about invalid day
result = await circle_node.clock_synchronize()
assert result is True

await stick.disconnect()