Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 Sunday (6) while frozen time is Saturday (5), Jan 31
def mock_clock_get_send(self):
response = pw_responses.CircleClockResponse()
response.timestamp = dt.now(tz=UTC)
# Set day_of_week to Sunday (6), requiring +1 day from Saturday 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 = 6 # Sunday
response.time.value = dt.now(tz=UTC).time()
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()