diff --git a/CHANGELOG.md b/CHANGELOG.md index 827b77ca6..71f7d9766 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 1ff06fb92..833c3f8c3 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -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, diff --git a/tests/stick_test_data.py b/tests/stick_test_data.py index b70ca5239..1b1a18f39 100644 --- a/tests/stick_test_data.py +++ b/tests/stick_test_data.py @@ -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 diff --git a/tests/test_usb.py b/tests/test_usb.py index 7de3159ab..b8a6e94fc 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -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()