Skip to content

Conversation

@bouwew
Copy link
Contributor

@bouwew bouwew commented Jan 27, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Fixed day/date calculation for scheduled times to avoid unintended 7-day wrapping, ensuring the intended target date and time are preserved across week/month boundaries.
  • Tests

    • Added a test for month-end clock synchronization and expanded test data to cover the edge case.
  • Documentation

    • Added an ongoing changelog entry referencing the related fix.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 27, 2026

📝 Walkthrough

Walkthrough

Updates 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

Cohort / File(s) Summary
Changelog Update
CHANGELOG.md
Added an Ongoing entry for PR 400 referencing fix for issue #399.
Clock synchronization logic
plugwise_usb/nodes/circle.py
Replaced modulo-based day wrapping with days_diff = response.day_of_week.value - dt_now.weekday() and target_date = dt_now + timedelta(days=days_diff); build circle_timestamp from target_date, preserving time and avoiding 0–6 modulo wrap.
Tests (month-end rollover)
tests/test_usb.py, tests/stick_test_data.py
Added test_clock_synchronize_month_overflow and a new RESPONSE_MESSAGES entry simulating a Circle+ realtime set-clock month-end scenario.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • dirixmjm
  • ArnoutD
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the main objective of the pull request—solving a monthly overflow issue (Issue #399)—and is concise and specific.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-399

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link

codecov bot commented Jan 27, 2026

Codecov Report

❌ Patch coverage is 82.14286% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.07%. Comparing base (32d8653) to head (54de6e2).

Files with missing lines Patch % Lines
tests/test_usb.py 80.00% 5 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link

@coderabbitai coderabbitai bot left a 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.

Copy link

Copilot AI left a 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 via dt_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.

Comment on lines 882 to 885
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(
Copy link

Copilot AI Jan 27, 2026

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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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.

Copy link

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:

  1. Freezes time to Jan 31 - the last day of the month where adding days could cause overflow
  2. Mocks the clock response to return day_of_week=5 (Saturday) when the frozen date is Friday, requiring a +1 day calculation
  3. Verifies no exception is raised and the method returns True
  4. The old code would fail with ValueError: day is out of range for month when trying replace(day=32)
  5. 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.

@bouwew bouwew marked this pull request as ready for review January 27, 2026 19:40
@bouwew bouwew requested a review from a team as a code owner January 27, 2026 19:40
Copy link

@coderabbitai coderabbitai bot left a 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.

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
E Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Copy link
Contributor

@dirixmjm dirixmjm left a 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.

@bouwew
Copy link
Contributor Author

bouwew commented Jan 28, 2026

@dirixmjm Thanks, but I'm not there yet, trying to recreate the ValueError in the original code with the added testcase.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants