-
Notifications
You must be signed in to change notification settings - Fork 2
Solve monthly overflow as reported in #399 #400
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
📝 WalkthroughWalkthroughUpdates changelog with PR 400; modifies Circle node clock sync to compute target date via direct day difference (timedelta) instead of a modulo wrap; adds tests and test data to cover a month-end/day-of-week rollover scenario. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #400 +/- ##
==========================================
+ Coverage 82.05% 82.07% +0.02%
==========================================
Files 36 36
Lines 8156 8182 +26
==========================================
+ Hits 6692 6715 +23
- Misses 1464 1467 +3 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@plugwise_usb/nodes/circle.py`:
- Around line 885-891: The month overflow handling around calculating
circle_timestamp is wrong: when (difference := days_diff - days_to_end_of_month)
> 0 you adjust corrected_day but do not advance the month, producing incorrect
dates; replace the manual day correction logic (the block using
last_day_of_month, days_to_end_of_month, corrected_day and
dt_now.replace(day=...)) by computing circle_timestamp as dt_now +
timedelta(days=days_diff) (using the existing dt_now and days_diff variables) so
month/year boundaries are handled correctly and remove the fragile
corrected_day/replace logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Fixes clock synchronization date calculation when the target weekday crosses a month boundary (avoiding invalid datetime.replace(day=...) behavior at month-end).
Changes:
- Update
clock_synchronize()to compute the target date viadt_now + timedelta(days=...)to handle month rollovers safely. - Add an “Ongoing” changelog entry referencing the PR/issue.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
plugwise_usb/nodes/circle.py |
Switches clock-sync date computation to timedelta arithmetic to prevent month-end overflow. |
CHANGELOG.md |
Documents the fix in the ongoing changelog section. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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, | ||
| target_date = dt_now + timedelta(days=days_diff) | ||
| circle_timestamp = target_date.replace( |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change fixes a tricky end-of-month boundary case, but there’s no regression test covering the date rollover logic. Consider adding a focused async unit test that freezes time near month-end (e.g., Jan 31) and mocks the clock-get response so clock_synchronize() exercises the (dt_now + timedelta(...)) path without raising and computes the expected circle_timestamp.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@coderabbitai please suggest a simple regression test that covers this update.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find existing test files for the circle.py module
fd -e py test.*circle tests
Length of output: 0
🏁 Script executed:
#!/bin/bash
# Check the project structure and look for test directory
fd -t d test
Length of output: 6
🏁 Script executed:
#!/bin/bash
# Look for pytest configuration or test patterns
find . -name "pytest.ini" -o -name "setup.cfg" -o -name "pyproject.toml" | head -5
Length of output: 16
🏁 Script executed:
#!/bin/bash
# Search for existing clock_synchronize tests to understand the pattern
rg -n "clock_synchronize" --type py -g '*test*' -C 3
Length of output: 0
🏁 Script executed:
#!/bin/bash
# Check if freezegun or other mocking libraries are already used
rg -n "freezegun|freeze_time|mock_datetime" --type py -g '*test*'
Length of output: 594
🏁 Script executed:
#!/bin/bash
# Check dependencies to see what testing libraries are available
cat requirements*.txt pyproject.toml setup.py setup.cfg 2>/dev/null | grep -i "test\|mock\|freeze"
Length of output: 339
🏁 Script executed:
#!/bin/bash
# Look at the test file structure to understand how Circle tests are organized
rg -n "class.*Circle|def test.*circle" --type py tests/test_usb.py -A 2 | head -50
Length of output: 171
🏁 Script executed:
#!/bin/bash
# Check how CircleClockGetRequest is mocked in existing tests
rg -n "CircleClockGetRequest|clock" --type py tests/test_usb.py -B 2 -A 5 | head -80
Length of output: 3329
🏁 Script executed:
#!/bin/bash
# Look at the imports and how responses are mocked
head -50 tests/test_usb.py
Length of output: 2263
🏁 Script executed:
#!/bin/bash
# Look for how CircleClockGetRequest responses are created/mocked
rg -n "CircleClockGetResponse|ClockGetResponse" --type py -B 2 -A 5 | head -60
Length of output: 0
🏁 Script executed:
#!/bin/bash
# Check the responses module to see what CircleClockGet response looks like
rg -n "class.*ClockGet" plugwise_usb/messages/responses.py -A 15 | head -40
Length of output: 0
🏁 Script executed:
#!/bin/bash
# Look at how day_of_week and time are structured in responses
rg -n "day_of_week|DayOfWeek" plugwise_usb/messages/ -A 2 | head -40
Length of output: 1809
🏁 Script executed:
#!/bin/bash
# Look at how async methods are tested with mocked responses
rg -n "async def test.*circle" tests/test_usb.py -A 30 | head -80
Length of output: 1312
🏁 Script executed:
#!/bin/bash
# Check response structure and how to create mock responses
rg -n "class.*CircleClock" plugwise_usb/messages/responses.py -B 2 -A 20 | head -60
Length of output: 690
🏁 Script executed:
#!/bin/bash
# Look at how monkeypatch is used to mock request.send()
rg -n "monkeypatch.*send|mock.*Request" tests/test_usb.py -B 2 -A 5 | head -50
Length of output: 2050
@bouwew Here's a simple regression test that covers the month overflow fix:
`@freeze_time`("2026-01-31 10:30:00", real_asyncio=True)
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()
# 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()
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()Key aspects of this test:
- Freezes time to Jan 31 - the last day of the month where adding days could cause overflow
- Mocks the clock response to return
day_of_week=5(Saturday) when the frozen date is Friday, requiring a +1 day calculation - Verifies no exception is raised and the method returns
True - The old code would fail with
ValueError: day is out of range for monthwhen tryingreplace(day=32) - The new code succeeds by using
timedelta(days=1)which correctly rolls over to Feb 1
You can add this test to your tests/test_usb.py file in the appropriate test class. Would you like me to adjust the test to match a specific node setup in your test suite?
🧠 Learnings used
Learnt from: bouwew
Repo: plugwise/python-plugwise-usb PR: 255
File: plugwise_usb/nodes/circle.py:477-482
Timestamp: 2025-06-19T06:38:04.702Z
Learning: In plugwise_usb/nodes/circle.py, the timestamp comparison `prev_address_timestamp - self._last_collected_energy_timestamp` in the `get_missing_energy_logs` method is intentional. The code iterates backwards through time-ordered energy log addresses, where `prev_address_timestamp` contains the more recent timestamp from the previous iteration, and this subtraction correctly calculates the time gap to determine when to stop collecting outdated data.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@tests/test_usb.py`:
- Around line 3043-3083: The test_test_clock_synchronize_month_overflow case
doesn't force a month rollover because the frozen date "2026-01-31" is already
Saturday (weekday 5) and mock_clock_get_send sets response.day_of_week.value = 5
so days_diff == 0; update the test so the mocked day_of_week differs by +1 to
force crossing into February (e.g., set response.day_of_week.value = 6) or
change the frozen time in the `@freeze_time` decorator to a Friday date so that
mock_clock_get_send's day_of_week=5 results in a +1 day rollover; modify the
mock_clock_get_send implementation (and/or the freeze_time value) inside
test_clock_synchronize_month_overflow to ensure days_diff != 0 and the code path
that adds timedelta(days=1) is exercised.
|
dirixmjm
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
Bedankt voor de actie.
|
@dirixmjm Thanks, but I'm not there yet, trying to recreate the |




Summary by CodeRabbit
Bug Fixes
Tests
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.