Skip to content

Conversation

@dirixmjm
Copy link
Contributor

@dirixmjm dirixmjm commented Sep 14, 2025

Summary by CodeRabbit

  • New Features

    • Automatic periodic background clock synchronization for Plugwise Circle devices (every 3600s), started on init and stopped cleanly on unload.
  • Bug Fixes

    • More reliable time-drift detection using absolute drift comparison and clearer drift logging.
    • Ensures background sync task is properly cancelled and awaited to avoid hangs or leftover activity.
  • Tests

    • Added a comprehensive test transport and rich test data; tests refactored to use centralized test-data module.
  • Documentation

    • Changelog updated with the scheduled clock synchronization entry.

@coderabbitai
Copy link

coderabbitai bot commented Sep 14, 2025

Walkthrough

Adds a background scheduler to PlugwiseCircle that periodically calls clock_synchronize() every 3600s (with jitter), starts/stores/cancels the task during init/unload, converts MAX_LOG_HOURS to a Final, changes clock-drift checks in Circle and Circle Plus to use absolute drift, and adds a comprehensive DummyTransport test harness and test data.

Changes

Cohort / File(s) Summary
Circle clock scheduler
plugwise_usb/nodes/circle.py
Added CLOCK_SYNC_PERIOD: Final = 3600; annotated MAX_LOG_HOURS as Final; imported CancelledError, Task, create_task, gather, sleep, and random; added `_clock_synchronize_task: Task[None]
Clock drift check (Circle & Circle Plus)
plugwise_usb/nodes/circle.py, plugwise_usb/nodes/circle_plus.py
Replaced sign-sensitive drift comparison with absolute-value check using abs(clock_offset.total_seconds()) < MAX_TIME_DRIFT; updated logging to report absolute drift in seconds.
Test harness & data
tests/stick_test_data.py, tests/test_usb.py
Added crc_fun, construct_message, LOG_TIMESTAMPS, RESPONSE_MESSAGES, PARTLY_RESPONSE_MESSAGES, SECOND_RESPONSE_MESSAGES, and a feature-rich DummyTransport mock to tests/stick_test_data.py; refactored tests/test_usb.py to use the shared test-data module (pw_userdata) and replaced local helpers/DummyTransport usages with the centralized ones.
Cache formatting only
plugwise_usb/network/cache.py
Purely formatting/wrapping changes in save_cache and restore_cache; no behavioral or API changes.
Changelog
CHANGELOG.md
Added Ongoing entry for PR 341: "Schedule clock synchronization every 3600 seconds." Removed an existing Ongoing entry for PR 342.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor App as App/Framework
    participant PC as PlugwiseCircle
    participant BG as _clock_synchronize_task
    participant Dev as Circle Device

    App->>PC: __init__()
    alt no task or task.done()
        PC->>BG: create_task(_clock_synchronize_scheduler())
    end

    rect rgb(245,250,255)
    note over BG: Background loop — every 3600s (+/- jitter)
    loop every 3600s
        BG->>PC: call clock_synchronize()
        PC->>Dev: perform clock sync
        Dev-->>PC: ack/result
        BG->>BG: sleep(CLOCK_SYNC_PERIOD + jitter)
    end
    end

    App->>PC: unload()
    alt task active
        PC->>BG: cancel()
        BG-->>PC: raises CancelledError / stops
        PC->>PC: await gather(task, return_exceptions=True)
        PC->>PC: clear _clock_synchronize_task
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • bouwew

Poem

I hop in quiet hourly beats,
I nudge the ticks and tune the greets,
A jittered hop, a careful fix,
I keep the clocks in tidy ticks. 🐇⏰

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "schedule clock synchronization every 3600 seconds" succinctly and accurately describes the primary change in the diff — adding a background scheduler and CLOCK_SYNC_PERIOD = 3600 to perform periodic clock synchronization — and is specific and clear enough for a reviewer scanning the PR list.
Docstring Coverage ✅ Passed Docstring coverage is 95.65% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch mdi_clock

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


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 Sep 14, 2025

Codecov Report

❌ Patch coverage is 80.95238% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.33%. Comparing base (26d5994) to head (296b0a0).

Files with missing lines Patch % Lines
plugwise_usb/nodes/circle.py 80.95% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #341      +/-   ##
==========================================
+ Coverage   81.25%   81.33%   +0.08%     
==========================================
  Files          36       36              
  Lines        8110     8129      +19     
==========================================
+ Hits         6590     6612      +22     
+ Misses       1520     1517       -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: 3

🧹 Nitpick comments (1)
plugwise_usb/nodes/circle.py (1)

1003-1006: Name the task for easier debugging (optional).

Keeps logs and task dumps readable without changing behavior.

         if self._clock_synchronize_task is None or self._clock_synchronize_task.done():
             self._clock_synchronize_task = create_task(
                 self._clock_synchronize_scheduler()
             )
+            if hasattr(self._clock_synchronize_task, "set_name"):
+                self._clock_synchronize_task.set_name(
+                    f"plugwise-clock-sync:{self._mac_in_str}"
+                )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 40185ed and 1446502.

📒 Files selected for processing (1)
  • plugwise_usb/nodes/circle.py (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
plugwise_usb/nodes/circle.py (1)
plugwise_usb/nodes/circle_plus.py (1)
  • clock_synchronize (68-108)
🔇 Additional comments (1)
plugwise_usb/nodes/circle.py (1)

144-145: LGTM: dedicated field for the scheduler task.

Clear ownership and type are good. No issues.

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: 0

♻️ Duplicate comments (1)
plugwise_usb/nodes/circle.py (1)

857-865: Keep the scheduler alive on errors; preserve cancellation semantics.

Unhandled exceptions in clock_synchronize() will terminate the loop; also re‑raise CancelledError after logging to keep true cancellation semantics.

Apply:

 async def _clock_synchronize_scheduler(self) -> None:
     """Background task: periodically synchronize the clock until cancelled."""
     try:
         while True:
             await sleep(60)
-            await self.clock_synchronize()
+            try:
+                await self.clock_synchronize()
+            except Exception:
+                _LOGGER.exception(
+                    "Clock synchronization failed for %s", self._mac_in_str
+                )
     except CancelledError:
-        _LOGGER.debug("Clock sync scheduler cancelled for %s", self.name)
+        _LOGGER.debug("Clock sync scheduler cancelled for %s", self._mac_in_str)
+        raise

Optional: add small jitter (±5s) to the sleep to avoid aligned bursts when many nodes start together.

🧹 Nitpick comments (2)
plugwise_usb/nodes/circle.py (2)

1006-1009: Optionally name the task for easier debugging.

Helps tracing in logs and debuggers.

-            self._clock_synchronize_task = create_task(
-                self._clock_synchronize_scheduler()
-            )
+            t = create_task(self._clock_synchronize_scheduler())
+            if hasattr(t, "set_name"):
+                t.set_name(f"clock-sync:{self._mac_in_str}")
+            self._clock_synchronize_task = t

1100-1104: Good: cancel, await, and clear the clock-sync task.

Prevents “Task exception was never retrieved” and leaks.

For consistency, consider draining and clearing _retrieve_energy_logs_task above as well:

if self._retrieve_energy_logs_task and not self._retrieve_energy_logs_task.done():
    self._retrieve_energy_logs_task.cancel()
    await gather(self._retrieve_energy_logs_task, return_exceptions=True)
    self._retrieve_energy_logs_task = None
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1446502 and e527f81.

📒 Files selected for processing (1)
  • plugwise_usb/nodes/circle.py (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
plugwise_usb/nodes/circle.py (2)
plugwise_usb/nodes/circle_plus.py (1)
  • clock_synchronize (68-108)
plugwise_usb/nodes/node.py (1)
  • name (218-222)
🔇 Additional comments (2)
plugwise_usb/nodes/circle.py (2)

5-5: Good: asyncio imports updated.

Including CancelledError, Task, create_task, gather, sleep is correct for the new scheduler and shutdown path.


144-145: Good: explicit task handle for the scheduler.

Typed as Task[None] | None and initialized in init, preventing attribute‑errors later.

@dirixmjm dirixmjm changed the title schedule clock synchronization every 60 seconds schedule clock synchronization every 3600 seconds Sep 15, 2025
@dirixmjm dirixmjm marked this pull request as ready for review September 15, 2025 07:53
@dirixmjm dirixmjm requested a review from a team as a code owner September 15, 2025 07:53
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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
plugwise_usb/nodes/circle.py (1)

885-889: Bug: timedelta.seconds ignores sign — negative drifts never sync.
Using timedelta.seconds drops the sign; the second clause is always true. Use abs(total_seconds()).

-        if (clock_offset.seconds < MAX_TIME_DRIFT) or (
-            clock_offset.seconds > -(MAX_TIME_DRIFT)
-        ):
+        if abs(clock_offset.total_seconds()) < MAX_TIME_DRIFT:
             return True
🧹 Nitpick comments (3)
plugwise_usb/nodes/circle.py (3)

859-871: Resilient background loop — add tiny jitter and unify log context.
Loop/cancellation are solid. To avoid thundering-herd effects, add ±5s jitter; also prefer consistent id in logs.

-                await sleep(CLOCK_SYNC_PERIOD)
+                await sleep(CLOCK_SYNC_PERIOD)
                 try:
-                    await self.clock_synchronize()
+                    await self.clock_synchronize()
                 except Exception:
-                    _LOGGER.exception("Clock synchronisation failed for %s", self.name)
+                    _LOGGER.exception("Clock synchronization failed for %s", self._mac_in_str)
         except CancelledError:
-            _LOGGER.debug("Clock sync scheduler cancelled for %s", self.name)
+            _LOGGER.debug("Clock sync scheduler cancelled for %s", self._mac_in_str)
             raise

Optional jitter (outside this hunk):

# add at top-level imports
from random import uniform

# replace the sleep line:
await sleep(CLOCK_SYNC_PERIOD + uniform(-5, 5))

1012-1015: Start the scheduler only after successful initialize.
Currently the task starts before later checks (e.g., calibration) that can fail, leaving a background task running when initialize() returns False.

Apply within this hunk:

-        if self._clock_synchronize_task is None or self._clock_synchronize_task.done():
-            self._clock_synchronize_task = create_task(
-                self._clock_synchronize_scheduler()
-            )

Then add just before return True (after await super().initialize()):

if self._clock_synchronize_task is None or self._clock_synchronize_task.done():
    self._clock_synchronize_task = create_task(self._clock_synchronize_scheduler())

1106-1110: Graceful cancellation and draining — good.
Cancel + gather + clear avoids “Task exception was never retrieved.”

Mirror this pattern for _retrieve_energy_logs_task above to drain its cancellation as well:

if self._retrieve_energy_logs_task is not None and not self._retrieve_energy_logs_task.done():
    self._retrieve_energy_logs_task.cancel()
    await gather(self._retrieve_energy_logs_task, return_exceptions=True)
    self._retrieve_energy_logs_task = None
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 85d6559 and a4553ec.

📒 Files selected for processing (1)
  • plugwise_usb/nodes/circle.py (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
plugwise_usb/nodes/circle.py (1)
plugwise_usb/nodes/circle_plus.py (1)
  • clock_synchronize (68-108)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Process test coverage
🔇 Additional comments (3)
plugwise_usb/nodes/circle.py (3)

5-5: Asyncio imports updated — good.
Importing CancelledError and friends is correct and matches the shutdown handling.


146-148: Scheduler task field initialization — OK.
Clear None-or-Task typing; fits later lifecycle calls.


77-80: Align CLOCK_SYNC_PERIOD with PR objective (60s vs 3600s).

PR title/objective says "every 60 seconds" but the code sets CLOCK_SYNC_PERIOD = 3600 — confirm intended cadence and change if 60s is correct.

File: plugwise_usb/nodes/circle.py (lines 77–80)

-CLOCK_SYNC_PERIOD: Final = 3600
+CLOCK_SYNC_PERIOD: Final = 60  # seconds

@dirixmjm dirixmjm marked this pull request as draft September 15, 2025 08:03
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: 0

♻️ Duplicate comments (1)
plugwise_usb/nodes/circle.py (1)

859-871: Fix AttributeError: use _mac_in_str in logs inside the scheduler.

self.mac_in_str doesn’t exist; this would raise at runtime and kill the loop.

Apply:

-                    _LOGGER.exception("Clock synchronisation failed for %s", self.mac_in_str)
+                    _LOGGER.exception("Clock synchronisation failed for %s", self._mac_in_str)
@@
-            _LOGGER.debug("Clock sync scheduler cancelled for %s", self.mac_in_str)
+            _LOGGER.debug("Clock sync scheduler cancelled for %s", self._mac_in_str)

Optional: add small jitter (±5s) to sleep(CLOCK_SYNC_PERIOD) to avoid synchronized bursts when many nodes start together.

🧹 Nitpick comments (1)
plugwise_usb/nodes/circle.py (1)

889-897: Log the correct drift magnitude and fix a typo in the error message.

  • Use abs(total_seconds()) for drift logging (current .seconds can misreport negatives).
  • Typo: “en” → “when”.

Apply:

-        _LOGGER.info(
-            "Reset clock of node %s because time has drifted %s sec",
-            self._mac_in_str,
-            str(clock_offset.seconds),
-        )
+        _LOGGER.info(
+            "Reset clock of node %s because time has drifted %s sec",
+            self._mac_in_str,
+            str(int(abs(clock_offset.total_seconds()))),
+        )
@@
-            raise NodeError(
-                "Unable to synchronize clock en when protocol version is unknown"
-            )
+            raise NodeError(
+                "Unable to synchronize clock when protocol version is unknown"
+            )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 296b0a0 and eba5cb6.

📒 Files selected for processing (2)
  • plugwise_usb/nodes/circle.py (7 hunks)
  • plugwise_usb/nodes/circle_plus.py (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-19T06:38:04.702Z
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#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.

Applied to files:

  • plugwise_usb/nodes/circle_plus.py
  • plugwise_usb/nodes/circle.py
🧬 Code graph analysis (1)
plugwise_usb/nodes/circle.py (1)
plugwise_usb/nodes/circle_plus.py (1)
  • clock_synchronize (68-106)
🔇 Additional comments (7)
plugwise_usb/nodes/circle_plus.py (1)

89-96: Absolute drift check is good; tweak logging for correctness and remove stray parenthesis.

clock_offset.seconds is misleading for negative offsets; prefer abs(int(clock_offset.total_seconds())). Also the log message ends with an extra “)”.
[ suggest_nitpick_refactor ]
Apply:

-        _LOGGER.info(
-            "Reset realtime clock of node %s because time has drifted %s seconds while max drift is set to %s seconds)",
-            self._node_info.mac,
-            str(clock_offset.seconds),
-            str(MAX_TIME_DRIFT),
-        )
+        _LOGGER.info(
+            "Reset realtime clock of node %s because time has drifted %s seconds while max drift is set to %s seconds",
+            self._node_info.mac,
+            str(int(abs(clock_offset.total_seconds()))),
+            str(MAX_TIME_DRIFT),
+        )
plugwise_usb/nodes/circle.py (6)

5-5: Importing CancelledError for clean shutdown — LGTM.


77-80: New Finals (MAX_LOG_HOURS, CLOCK_SYNC_PERIOD) — LGTM.


146-148: Scheduler task field initialization — LGTM.


886-887: Absolute drift comparison — LGTM.


1010-1013: Starting the background scheduler after initial sync — LGTM.


1104-1108: Task cancellation is properly drained — LGTM.

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: 0

🧹 Nitpick comments (4)
plugwise_usb/nodes/circle.py (4)

859-871: Add small jitter and unify spelling in log message.

Prevents synchronized bursts across many nodes and keeps spelling consistent ("synchronization"). Behavior unchanged otherwise.

Apply:

@@
-            while True:
-                await sleep(CLOCK_SYNC_PERIOD)
+            while True:
+                # add small jitter to avoid thundering herd when many nodes start together
+                await sleep(CLOCK_SYNC_PERIOD + (random.uniform(-5, 5)))
@@
-                except Exception:
-                    _LOGGER.exception("Clock synchronisation failed for %s", self._mac_in_str)
+                except Exception:
+                    _LOGGER.exception("Clock synchronization failed for %s", self._mac_in_str)

Also add import:

 import logging
+import random
 from math import ceil

886-892: Nit: align info log with CirclePlus and include max drift.

Keeps logs consistent and more informative.

-        _LOGGER.info(
-            "Reset clock of node %s because time has drifted %s sec",
-            self._mac_in_str,
-            str(int(abs(clock_offset.total_seconds()))),
-        )
+        _LOGGER.info(
+            "Reset realtime clock of node %s because time drifted %s seconds (max %s seconds)",
+            self._mac_in_str,
+            str(int(abs(clock_offset.total_seconds()))),
+            str(MAX_TIME_DRIFT),
+        )

893-897: Typo in error message.

Remove stray “en”.

-            raise NodeError(
-                "Unable to synchronize clock en when protocol version is unknown"
-            )
+            raise NodeError(
+                "Unable to synchronize clock when protocol version is unknown"
+            )

1010-1013: Name the task for easier debugging (optional).

A task name helps tracing in logs and debuggers.

-            self._clock_synchronize_task = create_task(
-                self._clock_synchronize_scheduler()
-            )
+            self._clock_synchronize_task = create_task(
+                self._clock_synchronize_scheduler(),
+                name=f"clock-sync-{self._mac_in_str}",
+            )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eba5cb6 and 5fec0d3.

📒 Files selected for processing (2)
  • plugwise_usb/nodes/circle.py (7 hunks)
  • plugwise_usb/nodes/circle_plus.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • plugwise_usb/nodes/circle_plus.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-19T06:38:04.702Z
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#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.

Applied to files:

  • plugwise_usb/nodes/circle.py
🧬 Code graph analysis (1)
plugwise_usb/nodes/circle.py (1)
plugwise_usb/nodes/circle_plus.py (1)
  • clock_synchronize (68-106)
🔇 Additional comments (5)
plugwise_usb/nodes/circle.py (5)

5-5: Good: proper asyncio imports for task lifecycle.

Includes CancelledError, sleep, gather; consistent with usage below.


77-78: Const correctness looks good.

Keeping MAX_LOG_HOURS as Final preserves semantics.


79-79: Clock sync period defined clearly.

Defaulting to 3600s matches the PR objective.


146-147: Task handle field added and typed.

Clean addition; avoids stray tasks across reloads.


1104-1108: Good: cancel, await, and clear the scheduler task.

Drains cancellation and avoids “Task exception was never retrieved”.

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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5fec0d3 and 22cbfee.

📒 Files selected for processing (2)
  • plugwise_usb/network/cache.py (2 hunks)
  • plugwise_usb/nodes/circle.py (7 hunks)
✅ Files skipped from review due to trivial changes (1)
  • plugwise_usb/network/cache.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-19T06:38:04.702Z
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#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.

Applied to files:

  • plugwise_usb/nodes/circle.py
🧬 Code graph analysis (1)
plugwise_usb/nodes/circle.py (1)
plugwise_usb/nodes/circle_plus.py (1)
  • clock_synchronize (68-106)
🪛 GitHub Actions: Latest commit
plugwise_usb/nodes/circle.py

[error] 863-863: F821 Undefined name random.

🔇 Additional comments (6)
plugwise_usb/nodes/circle.py (6)

5-5: Good: imported CancelledError for graceful task shutdown.
This aligns with the cancellation handling in the scheduler and unload.


77-80: Constants look good and typed as Final.
MAX_LOG_HOURS aliasing and CLOCK_SYNC_PERIOD=3600 are clear.


146-148: Task holder initialization is correct and typed.
This prevents multiple concurrent schedulers.


888-895: Correct: use absolute drift for comparison and clearer log.
This avoids sign sensitivity on early/late clock differences.


1013-1016: Scheduler startup placement is fine.
Guarding with “is None or done()” avoids duplicates.


1107-1111: Good: cancel, await, and clear the scheduler task.
Prevents “Task exception was never retrieved.”

@sonarqubecloud
Copy link

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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
plugwise_usb/nodes/circle.py (1)

624-647: Fix slots_empty semantics in energy_log_update (outdated records wrongly mark address as “updated”).

cache_updated is set even when a non-empty record is outdated and skipped, which makes slots_empty False while no recent data was stored. This breaks the contract (“True when all four slots are empty or outdated”) and prevents early stop in _get_initial_energy_logs, causing unnecessary backfill requests.

Apply:

@@
-        for _slot in range(4, 0, -1):
+        for _slot in range(4, 0, -1):
             log_timestamp, log_pulses = response.log_data[_slot]
             _LOGGER.debug(
                 "In slot=%s: pulses=%s, timestamp=%s", _slot, log_pulses, log_timestamp
             )
-            address = response.log_address
+            address = response.log_address
             if log_timestamp is None or log_pulses is None:
                 self._energy_counters.add_empty_log(address, _slot)
             else:
                 log_timestamp = log_timestamp.replace(tzinfo=UTC)
-                if self._check_timestamp_is_recent(address, _slot, log_timestamp):
-                    self._energy_counters.add_pulse_log(
-                        address,
-                        _slot,
-                        log_timestamp,
-                        log_pulses,
-                        import_only=True,
-                    )
-                cache_updated = True
+                if self._check_timestamp_is_recent(address, _slot, log_timestamp):
+                    self._energy_counters.add_pulse_log(
+                        address,
+                        _slot,
+                        log_timestamp,
+                        log_pulses,
+                        import_only=True,
+                    )
+                    cache_updated = True
🧹 Nitpick comments (5)
plugwise_usb/nodes/circle.py (3)

624-631: Tiny readability nit: don’t shadow the address parameter.

Rename the inner address variable to avoid confusion.

-            address = response.log_address
-            if log_timestamp is None or log_pulses is None:
-                self._energy_counters.add_empty_log(address, _slot)
+            log_address = response.log_address
+            if log_timestamp is None or log_pulses is None:
+                self._energy_counters.add_empty_log(log_address, _slot)
@@
-                    self._energy_counters.add_pulse_log(
-                        address,
+                    self._energy_counters.add_pulse_log(
+                        log_address,

1098-1104: Drain and clear _retrieve_energy_logs_task on unload.

You cancel but don’t await; this can leak “Task exception was never retrieved”.

-        if (
-            self._retrieve_energy_logs_task is not None
-            and not self._retrieve_energy_logs_task.done()
-        ):
-            self._retrieve_energy_logs_task.cancel()
+        if self._retrieve_energy_logs_task:
+            self._retrieve_energy_logs_task.cancel()
+            await gather(self._retrieve_energy_logs_task, return_exceptions=True)
+            self._retrieve_energy_logs_task = None

80-81: Optional: single source of truth for MAX_LOG_HOURS.

Consider importing the limit from the pulses/helper module (or constants) to avoid divergent behavior between modules and tests that monkeypatch the pulses value.

tests/stick_test_data.py (1)

62-73: Simplify seq-id formatting.

Use zfill for clarity and micro-efficiency.

-        temp_str = str(hex(temp_int)).lstrip("0x").upper()
-        while len(temp_str) < 4:
-            temp_str = "0" + temp_str
-        return temp_str.encode()
+        return f"{temp_int:04X}".encode()
tests/test_usb.py (1)

795-802: Match the patched method’s signature.

get_missing_energy_logs is async and takes only self; align the fake to avoid confusion and future regressions.

-        async def fake_get_missing_energy_logs(address: int) -> None:
+        async def fake_get_missing_energy_logs(self) -> None:
             """Mock missing energy logs."""
             # no-op
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 22cbfee and c340353.

📒 Files selected for processing (3)
  • plugwise_usb/nodes/circle.py (7 hunks)
  • tests/stick_test_data.py (2 hunks)
  • tests/test_usb.py (15 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-19T06:38:04.702Z
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#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.

Applied to files:

  • plugwise_usb/nodes/circle.py
🧬 Code graph analysis (3)
tests/test_usb.py (1)
tests/stick_test_data.py (2)
  • DummyTransport (39-142)
  • construct_message (28-36)
plugwise_usb/nodes/circle.py (2)
plugwise_usb/nodes/circle_plus.py (1)
  • clock_synchronize (68-106)
plugwise_usb/exceptions.py (1)
  • NodeError (24-25)
tests/stick_test_data.py (3)
plugwise_usb/messages/requests.py (3)
  • seq_id (131-133)
  • seq_id (136-145)
  • response (124-128)
plugwise_usb/messages/__init__.py (2)
  • seq_id (38-40)
  • seq_id (43-45)
plugwise_usb/connection/receiver.py (1)
  • close (157-161)
🔇 Additional comments (10)
plugwise_usb/nodes/circle.py (5)

5-11: Imports for task cancellation and jitter look good.

Adding CancelledError and random unblocks the scheduler and fixes the F821.


78-81: Constants introduced are sensible defaults.

Final-annotating MAX_LOG_HOURS and adding CLOCK_SYNC_PERIOD=3600 fit the PR goal.


147-149: Good: explicit task slot for the scheduler.

The dedicated _clock_synchronize_task field simplifies lifecycle management.


860-873: Scheduler is resilient and cancellation-safe.

Looping with jitter, exception logging, and explicit CancelledError handling are correct.


1014-1017: Starting the scheduler after initial sync is correct.

Creates the task only once or when a prior one completed.

tests/stick_test_data.py (2)

28-36: Message constructor looks correct (CRC and framing).


109-123: Transport response helpers simulate fragmentation well.

The delayed and immediate paths cover typical I/O behaviors.

Also applies to: 124-139

tests/test_usb.py (3)

54-55: Type annotation update to DummyTransport is correct.


73-79: Return type updated appropriately.


1854-1859: Good migration to shared test data utilities.

Centralizing construct_message and DummyTransport reduces duplication and brittleness across tests.

Also applies to: 1879-1880, 1891-1893, 1920-1921, 1945-1947, 1967-1968, 1992-1993, 2056-2061, 2101-2102, 2112-2113, 2165-2166

@dirixmjm dirixmjm closed this Sep 24, 2025
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.

2 participants