Skip to content

Conversation

@bouwew
Copy link
Contributor

@bouwew bouwew commented Aug 12, 2025

Don't store energy-log data when it's outside the time-interval of interest (MAX_LOG_HOURS).

Summary by CodeRabbit

  • New Features

    • Energy logs now enforce a time-based recency threshold and store only recent, non-empty records.
    • Startup collection adapts to the current hour and is capped to reduce requests and speed initial log gathering.
  • Reliability

    • Collection unified to a single success outcome; ongoing collection halts on failure, remaining tasks cancel early, and cache updates occur only when new records were stored.
  • Documentation

    • Changelog updated with an "Ongoing" note referencing the energy-collection improvement.

@coderabbitai
Copy link

coderabbitai bot commented Aug 12, 2025

Walkthrough

Reworks PlugwiseCircle energy-log collection: adds MAX_LOG_HOURS, changes energy_log_update to return a single bool indicating if any recent record was stored, adds per-record recency checks, simplifies initial collection/rollover flow, tightens cache writes and task cancellation.

Changes

Cohort / File(s) Summary of Changes
Energy API & constants
plugwise_usb/nodes/circle.py
Added MAX_LOG_HOURS = DAY_IN_HOURS; removed _last_collected_energy_timestamp; changed `energy_log_update(self, address: int
Per-record processing & control flow
plugwise_usb/nodes/circle.py
Added _check_timestamp_is_recent(address, slot, timestamp) and per-slot recency validation; energy_log_update now returns True only if ≥1 recent non-empty record stored; writes empty records for missing/stale slots; initial collection reworked with factor-based address count using ceil(now.hour / factor) + 1, capped by MAX_LOG_HOURS; removed prior timestamp-gap stop logic; cache writes guarded by any-record-stored and save_cache.
Async task handling
plugwise_usb/nodes/circle.py
get_missing_energy_logs updated to await tasks, cancel remaining tasks when a task returns False, and drain cancellations with gather(..., return_exceptions=True).
Changelog
CHANGELOG.md
Added top-level "Ongoing" subsection referencing PR #311 about improving energy collection.

Sequence Diagram(s)

sequenceDiagram
  actor Scheduler
  participant Circle as PlugwiseCircle
  participant Node
  participant Cache

  Scheduler->>Circle: energy_update()
  Circle->>Node: get_current_log_address()
  alt rollover suspected
    Circle->>Node: get_previous_log_address()
  end
  loop addresses to collect
    Circle->>Node: request EnergyLogs(address)
    Node-->>Circle: EnergyLogs(records)
    Circle->>Circle: _check_timestamp_is_recent(address, slot, ts)
    alt recent & non-empty
      Circle->>Cache: store record(s) (if save_cache=True)
      Note right of Circle: any_record_stored = True
    else
      Circle->>Circle: store empty record / skip stale
    end
  end
  Circle-->>Scheduler: return True if any_record_stored else False
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • dirixmjm

Poem

I hop through logs from dawn till night,
Toss stale timestamps out of sight.
One little boolean says what's stored,
Rollovers checked, no data ignored.
Fresh watts burrow warm and bright. 🐇⚡️

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch improve-energy-collection

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai or @coderabbitai title anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@codecov
Copy link

codecov bot commented Aug 12, 2025

Codecov Report

❌ Patch coverage is 41.46341% with 24 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.61%. Comparing base (8042899) to head (f6c4d36).
⚠️ Report is 29 commits behind head on main.

Files with missing lines Patch % Lines
plugwise_usb/nodes/circle.py 41.46% 24 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #311      +/-   ##
==========================================
+ Coverage   80.54%   80.61%   +0.06%     
==========================================
  Files          36       36              
  Lines        7540     7536       -4     
==========================================
+ Hits         6073     6075       +2     
+ Misses       1467     1461       -6     

☔ 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

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

77-77: Consider documenting the constant purpose

While MAX_LOG_HOURS is clearly derived from DAY_IN_HOURS, it would be helpful to add a comment explaining that this represents the maximum age threshold for energy log records (24 hours).

+# Maximum age (in hours) for energy log records to be considered recent
 MAX_LOG_HOURS = DAY_IN_HOURS

467-467: Improve timestamp calculation clarity

The calculation int(floor(datetime.now(tz=UTC).hour / 4) + 1) could be more self-documenting by using parentheses to make the order of operations explicit.

-        total_addresses = int(floor(datetime.now(tz=UTC).hour / 4) + 1)
+        total_addresses = int(floor(datetime.now(tz=UTC).hour / 4)) + 1

590-590: Consider extracting the seconds conversion constant

The magic number 3600 could be replaced with a constant or use a more explicit calculation for better readability.

+from ..constants import HOUR_IN_SECONDS  # Add to imports if not exists
 
-        if age_seconds > MAX_LOG_HOURS * 3600:
+        if age_seconds > MAX_LOG_HOURS * HOUR_IN_SECONDS:

If HOUR_IN_SECONDS doesn't exist in constants, you could define it there or use:

-        if age_seconds > MAX_LOG_HOURS * 3600:
+        if age_seconds > MAX_LOG_HOURS * 60 * 60:  # hours to seconds
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 757787c and e24a92f.

📒 Files selected for processing (1)
  • plugwise_usb/nodes/circle.py (10 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
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.
📚 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 (4)
plugwise_usb/messages/responses.py (1)
  • log_address (770-772)
plugwise_usb/messages/requests.py (18)
  • CircleEnergyLogsRequest (1131-1165)
  • response (124-128)
  • send (366-375)
  • send (388-397)
  • send (439-441)
  • send (477-486)
  • send (513-522)
  • send (543-554)
  • send (567-576)
  • send (589-598)
  • send (622-631)
  • send (668-677)
  • send (720-729)
  • send (776-785)
  • send (809-818)
  • send (849-858)
  • send (881-890)
  • send (913-922)
plugwise_usb/nodes/helpers/counter.py (4)
  • add_empty_log (70-72)
  • update (147-176)
  • update (286-328)
  • timestamp (179-187)
plugwise_usb/nodes/helpers/pulses.py (1)
  • add_empty_log (389-421)
🔇 Additional comments (4)
plugwise_usb/nodes/circle.py (4)

471-477: LGTM! Improved energy log collection logic

The refactored logic correctly stops initial collection when all slots are empty or outdated, preventing unnecessary storage of stale data.


513-579: Well-designed energy log update refactoring

The refactored energy_log_update method effectively:

  1. Returns a single boolean to indicate success (at least one recent record stored)
  2. Implements time-based filtering via _check_timestamp_is_recent
  3. Properly handles empty slots when timestamps are outdated
  4. Updates cache only when records are actually stored

This change successfully prevents storing stale energy log data as intended.


544-551: Good defensive programming with recency check

The addition of the recency check before storing energy log records is a crucial improvement that aligns with the PR objective to filter out data outside the configured time interval.


581-600: LGTM! Clear and effective timestamp recency validation

The _check_timestamp_is_recent helper method:

  • Correctly calculates age in seconds with UTC timezone handling
  • Provides clear warning messages with all relevant context
  • Returns appropriate boolean result for filtering decisions

The implementation effectively supports the PR's goal of preventing storage of outdated energy log data.

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

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

503-513: Fix cancellation flow to avoid awaiting canceled tasks (break early and cancel only pending tasks).

Awaiting tasks after canceling them will raise CancelledError. Also, canceling already-finished tasks is unnecessary. Break out once the first failure is observed and cancel only the remaining, not-yet-awaited tasks.

Apply this diff:

-        tasks = [
-            create_task(self.energy_log_update(address))
-            for address in missing_addresses
-        ]
-        for task in tasks:
-            await task
-            # When an energy log collection task returns False, do not execute the remaining tasks
-            if not task.result():
-                for t in tasks:
-                    t.cancel()
+        tasks = [
+            create_task(self.energy_log_update(address))
+            for address in missing_addresses
+        ]
+        for idx, task in enumerate(tasks):
+            result = await task
+            # When an energy log collection task returns False, stop and cancel the remaining tasks
+            if not result:
+                for t in tasks[idx + 1 :]:
+                    t.cancel()
+                break
🧹 Nitpick comments (3)
plugwise_usb/nodes/circle.py (3)

457-465: Docstring is outdated relative to new collection strategy.

The docstring still states “from the last 10 log addresses” while the implementation collects a dynamic number of addresses. Update the docstring to avoid confusion.

Suggested replacement for the method docstring (apply outside this range):

async def _get_initial_energy_logs(self) -> None:
    """Collect initial energy logs for recent hours up to MAX_LOG_HOURS (or hours elapsed today)."""

548-553: Fix comment typo and phrasing.

Small nit to improve readability.

Apply this diff:

-                # Don't store an old log-record; store am empty record instead
+                # Don't store an old log record; store an empty record instead

585-605: Consider lowering log level for outdated records to avoid log noise.

Outdated logs can be normal when backfilling or after rollovers. Emitting a warning per slot may spam logs. Suggest info or debug.

Apply this diff:

-            _LOGGER.warning(
+            _LOGGER.info(
                 "EnergyLog from Node %s | address %s | slot %s | timestamp %s is outdated, ignoring...",
                 self._mac_in_str,
                 address,
                 slot,
                 timestamp,
             )
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e24a92f and 8d04842.

📒 Files selected for processing (1)
  • plugwise_usb/nodes/circle.py (9 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
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.
📚 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 (8)
plugwise_usb/nodes/helpers/pulses.py (2)
  • calc_log_address (20-36)
  • add_empty_log (389-421)
plugwise_usb/messages/responses.py (1)
  • log_address (770-772)
plugwise_usb/messages/requests.py (18)
  • CircleEnergyLogsRequest (1131-1165)
  • response (124-128)
  • send (366-375)
  • send (388-397)
  • send (439-441)
  • send (477-486)
  • send (513-522)
  • send (543-554)
  • send (567-576)
  • send (589-598)
  • send (622-631)
  • send (668-677)
  • send (720-729)
  • send (776-785)
  • send (809-818)
  • send (849-858)
  • send (881-890)
  • send (913-922)
plugwise_usb/nodes/helpers/counter.py (1)
  • add_empty_log (70-72)
plugwise_usb/api.py (1)
  • save_cache (368-371)
plugwise_usb/network/cache.py (1)
  • save_cache (27-34)
plugwise_usb/nodes/node.py (1)
  • save_cache (705-719)
plugwise_usb/nodes/helpers/cache.py (1)
  • save_cache (40-46)
⏰ 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: Run pytest using Python 3.13
🔇 Additional comments (5)
plugwise_usb/nodes/circle.py (5)

371-374: Double-check intent: gate on “requested” vs. “successful” for the initial last-log fetch.

Setting _last_energy_log_requested to the boolean result means a failed attempt will keep re-requesting on every energy_update call. If the goal is to “request once at startup” regardless of success, set the flag before awaiting.

Optional change if you want to avoid repeated requests:

-        if not self._last_energy_log_requested:
-            self._last_energy_log_requested = await self.energy_log_update(
-                self._current_log_address
-            )
+        if not self._last_energy_log_requested:
+            self._last_energy_log_requested = True
+            await self.energy_log_update(self._current_log_address)

384-393: LGTM: boolean handling across rollover and single-missing flows is consistent.

The refactor cleanly propagates success/failure from energy_log_update and logs address-specific failures. This aligns the control flow with the new return contract.

Also applies to: 397-405, 418-427


517-535: LGTM: simplified return contract and early exit behavior.

Clear True/False semantics, guarded request, and consistent availability updates improve readability and robustness.


523-527: LGTM: request logging and cache-save gating.

  • Clear, address-specific request logging.
  • Save-to-cache only when new records were stored prevents unnecessary writes.
  • Returning the aggregated boolean matches the stated contract.

Also applies to: 536-536, 577-584


28-28: LGTM: explicit MAX_LOG_HOURS constant and import.

Defining MAX_LOG_HOURS and basing it on DAY_IN_HOURS makes the recency policy explicit and easily adjustable.

Also applies to: 77-78

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

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

76-76: Declare MAX_LOG_HOURS as a Final constant

This constant appears to be intended as a module-level constant. Make that explicit to aid type-checkers and readability.

-MAX_LOG_HOURS = DAY_IN_HOURS
+MAX_LOG_HOURS: Final = DAY_IN_HOURS

516-573: Energy log update: remove dead state and tighten comments

  • The local variable last_energy_timestamp_collected is declared but never used.
  • Comment typo: “store am empty record” -> “store an empty record”.
-        last_energy_timestamp_collected = False
         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
             )
             if (
                 log_timestamp is None
                 or log_pulses is None
-                # Don't store an old log record; store am empty record instead
+                # Don't store an old log record; store an empty record instead
                 or not self._check_timestamp_is_recent(address, _slot, log_timestamp)
             ):
                 self._energy_counters.add_empty_log(response.log_address, _slot)
                 continue

574-594: Clarify recency docstring; consider reducing log level for outdated records

  • Docstring wording is ambiguous: “newer than MAX_LOG_HOURS” -> “within MAX_LOG_HOURS hours”.
  • Optional: Logging every outdated slot as warning can be noisy on first runs. Consider info or debug unless this is truly exceptional.
-    """Check if the timestamp of the received log-record is recent.
-
-    A timestamp newer than MAX_LOG_HOURS is considered recent.
-    """
+    """Return True if the record timestamp is within MAX_LOG_HOURS hours from now."""
@@
-        if age_seconds > MAX_LOG_HOURS * 3600:
-            _LOGGER.warning(
+        if age_seconds > MAX_LOG_HOURS * 3600:
+            _LOGGER.info(
                 "EnergyLog from Node %s | address %s | slot %s | timestamp %s is outdated, ignoring...",
                 self._mac_in_str,
                 address,
                 slot,
                 timestamp,
             )
             return False
         return True

369-371: Variable naming mismatch: _last_energy_log_requested now stores success state

You now set _last_energy_log_requested to the boolean result of energy_log_update. That makes it a “last update succeeded” flag rather than “requested” status. If intentional, consider renaming to avoid confusion.

Suggested rename (follow-up, outside this diff):

  • _last_energy_log_requested -> _last_energy_log_update_succeeded

523-536: Log messages: consistent phrasing

Minor nit: “EnergyLogs from node …” and “Requesting EnergyLogs …” mix singular/plural and capitalization. For consistency and easier grep, consider standardizing to “Energy logs” (lowercase) across messages.

Example:

  • "Requesting energy logs from node %s, address=%s"
  • "Energy logs from node %s, address=%s"
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8d04842 and 40e0179.

📒 Files selected for processing (1)
  • plugwise_usb/nodes/circle.py (8 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
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.
📚 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 (7)
plugwise_usb/nodes/helpers/pulses.py (2)
  • calc_log_address (20-36)
  • add_empty_log (389-421)
plugwise_usb/messages/responses.py (1)
  • log_address (770-772)
plugwise_usb/messages/requests.py (18)
  • CircleEnergyLogsRequest (1131-1165)
  • response (124-128)
  • send (366-375)
  • send (388-397)
  • send (439-441)
  • send (477-486)
  • send (513-522)
  • send (543-554)
  • send (567-576)
  • send (589-598)
  • send (622-631)
  • send (668-677)
  • send (720-729)
  • send (776-785)
  • send (809-818)
  • send (849-858)
  • send (881-890)
  • send (913-922)
plugwise_usb/nodes/helpers/counter.py (4)
  • add_empty_log (70-72)
  • update (147-176)
  • update (286-328)
  • timestamp (179-187)
plugwise_usb/network/cache.py (1)
  • save_cache (27-34)
plugwise_usb/nodes/node.py (1)
  • save_cache (705-719)
plugwise_usb/nodes/helpers/cache.py (1)
  • save_cache (40-46)
🔇 Additional comments (2)
plugwise_usb/nodes/circle.py (2)

383-404: Rollover handling LGTM

The two-step rollover fetch (current then previous address) aligns with address-slot semantics and the recency filter. Returning None on failure keeps the flow consistent.


416-425: Single missing address flow LGTM

Using the boolean return to decide whether to proceed and update power is consistent with the new contract.

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

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

546-546: Remove unused variable to fix Ruff F841

last_energy_timestamp_collected is assigned but never used.

Apply this diff:

-        last_energy_timestamp_collected = False
🧹 Nitpick comments (2)
plugwise_usb/nodes/circle.py (2)

552-559: Nit: comment typo and clarity

“store am empty record” → “store an empty record”. Keeps logs and comments professional.

Apply this diff:

-                # Don't store an old log record; store am empty record instead
+                # Don't store an old log record; store an empty record instead

579-586: Nit: docstring wording for recency

“A timestamp newer than MAX_LOG_HOURS is considered recent” is ambiguous. Suggest explicitly stating “not older than MAX_LOG_HOURS”.

Apply this diff:

-        """Check if the timestamp of the received log-record is recent.
-
-        A timestamp newer than MAX_LOG_HOURS is considered recent.
-        """
+        """Check whether the log-record timestamp is recent.
+
+        A timestamp not older than MAX_LOG_HOURS (in hours) is considered recent.
+        """
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 40e0179 and e6c9050.

📒 Files selected for processing (1)
  • plugwise_usb/nodes/circle.py (8 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
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.
📚 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 (6)
plugwise_usb/nodes/helpers/pulses.py (2)
  • calc_log_address (20-36)
  • add_empty_log (389-421)
plugwise_usb/messages/responses.py (1)
  • log_address (770-772)
plugwise_usb/messages/requests.py (18)
  • CircleEnergyLogsRequest (1131-1165)
  • response (124-128)
  • send (366-375)
  • send (388-397)
  • send (439-441)
  • send (477-486)
  • send (513-522)
  • send (543-554)
  • send (567-576)
  • send (589-598)
  • send (622-631)
  • send (668-677)
  • send (720-729)
  • send (776-785)
  • send (809-818)
  • send (849-858)
  • send (881-890)
  • send (913-922)
plugwise_usb/network/cache.py (1)
  • save_cache (27-34)
plugwise_usb/nodes/node.py (1)
  • save_cache (705-719)
plugwise_usb/nodes/helpers/cache.py (1)
  • save_cache (40-46)
🪛 GitHub Actions: Latest commit
plugwise_usb/nodes/circle.py

[error] 513-513: Ruff: PLC0415 'import' should be at the top-level of a file. (Command: 'ruff check plugwise_usb/ tests/').


[error] 546-546: Ruff: F841 local variable 'last_energy_timestamp_collected' assigned to but never used. (Command: 'ruff check plugwise_usb/ tests/').

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

27-27: LGTM: Importing DAY_IN_HOURS for bounded recency window

Import is used to derive MAX_LOG_HOURS and matches the PR goal to time-bound energy logs.


76-76: LGTM: MAX_LOG_HOURS aligns recency filtering with a full day

Defining MAX_LOG_HOURS = DAY_IN_HOURS is clear and centralizes the policy.


369-371: Re-attempt behavior: confirm repeated last-log requests are intended

Setting _last_energy_log_requested to the boolean result of energy_log_update will re-request the last logs on every energy_update() call until a “recent” record is stored. This could be noisy if data remains outdated for a while.

Would you prefer tracking “attempted once” separately to avoid repeated requests, or is the current retry-until-recent behavior desired? I can propose a small refactor if you want to avoid retries when no recent data is available.


383-391: LGTM: Rollover handling for current address

The retry on _current_log_address with proper logging and early return on failure looks consistent with the new boolean contract.


395-404: LGTM: Previous-address retry after rollover

Using calc_log_address(self._current_log_address, 1, -4) to step one address back is correct for hourly-address stepping (4 slots).


417-425: LGTM: Single missing address path

Directly awaiting energy_log_update and proceeding to power_update on success matches the new API and short-circuits appropriately.


571-576: LGTM: Save cache only when at least one recent record was stored

This reduces unnecessary cache writes and aligns with the recency filter intent.

@dirixmjm dirixmjm self-requested a review August 13, 2025 10:08
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.

Nice improvements!

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

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

531-552: Guard against None timestamp before updating availability

CircleEnergyLogsResponse may not always carry a timestamp. Mirror the defensive pattern used in power_update.

Apply this diff:

-        await self._available_update_state(True, response.timestamp)
+        if response.timestamp is not None:
+            await self._available_update_state(True, response.timestamp)
+        else:
+            await self._available_update_state(True)
🧹 Nitpick comments (5)
plugwise_usb/nodes/circle.py (5)

77-79: Minor consistency: annotate MAX_LOG_HOURS as Final

For consistency with MAX_ADDRESSES_COLLECTED, annotate MAX_LOG_HOURS with Final.

Apply this diff:

-MAX_LOG_HOURS = DAY_IN_HOURS
+MAX_LOG_HOURS: Final = DAY_IN_HOURS

371-374: Name mismatch: _last_energy_log_requested tracks success, not just “requested”

After the change, this flag indicates “successful recent log fetch” (True/False), not merely that a request was sent. Consider renaming to avoid confusion in future maintenance (e.g., _initial_energy_log_fetched).


517-526: Log canceled addresses when halting batch; keep gather drain

You correctly cancel and drain remaining tasks. Consider logging which addresses are being canceled for better traceability during debugging.

Apply this diff:

         for idx, task in enumerate(tasks):
             result = await task
             # When an energy log collection task returns False, stop and cancel the remaining tasks
             if not result:
                 to_cancel = tasks[idx + 1 :]
+                _LOGGER.debug(
+                    "async_energy_update | %s | Batch update halting; canceling remaining addresses: %s",
+                    self._mac_in_str,
+                    missing_addresses[idx + 1 :],
+                )
                 for t in to_cancel:
                     t.cancel()
                 # Drain cancellations to avoid "Task exception was never retrieved"
                 await gather(*to_cancel, return_exceptions=True)
                 break

561-569: Nit: fix typo and clarify comment

  • “store am empty record” → “store an empty record”.

Apply this diff:

-                # Don't store an old log record; store am empty record instead
+                # Don't store an old log record; store an empty record instead

588-607: Clarify docstring wording and keep semantics unchanged

“A timestamp newer than MAX_LOG_HOURS” is ambiguous. Suggest “not older than MAX_LOG_HOURS hours” for clarity.

Apply this diff:

     def _check_timestamp_is_recent(
         self, address: int, slot: int, timestamp: datetime
     ) -> bool:
-        """Check if the timestamp of the received log-record is recent.
-
-        A timestamp newer than MAX_LOG_HOURS is considered recent.
-        """
+        """Check if the log-record timestamp is recent.
+        
+        A timestamp not older than MAX_LOG_HOURS hours is considered recent.
+        """
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b10698c and 677f678.

📒 Files selected for processing (1)
  • plugwise_usb/nodes/circle.py (8 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
🔇 Additional comments (6)
plugwise_usb/nodes/circle.py (6)

5-5: LGTM: Top-level imports and constants usage

  • Importing gather at top-level resolves PLC0415 and is correct for the cancellation-drain code.
  • ceil import is used appropriately for address rounding.
  • DAY_IN_HOURS import for deriving MAX_LOG_HOURS aligns with the PR objective.

Also applies to: 11-11, 28-28


385-393: Rollover: consistent boolean handling on failure

This path correctly treats a False return from energy_log_update as a failure and aborts the update. Matches the new boolean contract.


397-405: Rollover retry: previous address handling is correct

Retrying prev_log_address on rollover is correct and uses calc_log_address appropriately.


419-427: Single missing address path is correct

Correctly updates the single missing address and proceeds to power_update only on success.


479-487: Initial collection early-stop is sensible

Stopping when an address yields only empty/outdated slots avoids unnecessary requests and matches the PR objective.


580-587: Cache save conditioned on actual stored records is good

Only saving to cache when a recent non-empty record was stored reduces unnecessary I/O.

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

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

551-552: Guard against None timestamp when updating availability

CircleEnergyLogsResponse may not always carry a timestamp. Other paths (e.g., power_update) guard for None. Apply a defensive check here too.

-        await self._available_update_state(True, response.timestamp)
+        if response.timestamp is not None:
+            await self._available_update_state(True, response.timestamp)
+        else:
+            await self._available_update_state(True)
🧹 Nitpick comments (2)
plugwise_usb/nodes/circle.py (2)

561-569: Nit: fix comment typo and tighten wording

Small clarity tweak; no behavior change.

-            if (
-                log_timestamp is None
-                or log_pulses is None
-                # Don't store an old log record; store am empty record instead
-                or not self._check_timestamp_is_recent(address, _slot, log_timestamp)
-            ):
+            if (
+                log_timestamp is None
+                or log_pulses is None
+                # Don't store an old log record; store an empty record instead
+                or not self._check_timestamp_is_recent(address, _slot, log_timestamp)
+            ):

588-607: Clarify recency docstring; confirm boundary semantics

The docstring “newer than MAX_LOG_HOURS is considered recent” is ambiguous. Prefer explicitly stating “within the last MAX_LOG_HOURS hours.” Also, if you intend to exclude records exactly at the boundary (age == MAX_LOG_HOURS hours), change ‘>’ to ‘>=’.

Docstring improvement:

-        """Check if the timestamp of the received log-record is recent.
-
-        A timestamp newer than MAX_LOG_HOURS is considered recent.
-        """
+        """Check if a log record timestamp is within the last MAX_LOG_HOURS hours."""

If boundary should be exclusive (i.e., 24h-old should be considered outdated), change the comparison:

-        if age_seconds > MAX_LOG_HOURS * 3600:
+        if age_seconds >= MAX_LOG_HOURS * 3600:

Please confirm whether the inclusive or exclusive boundary is desired for MAX_LOG_HOURS.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b10698c and 677f678.

📒 Files selected for processing (1)
  • plugwise_usb/nodes/circle.py (8 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
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.
📚 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 (6)
plugwise_usb/nodes/helpers/pulses.py (2)
  • calc_log_address (20-36)
  • add_empty_log (389-421)
plugwise_usb/messages/responses.py (2)
  • log_address (770-772)
  • log_data (775-794)
plugwise_usb/messages/requests.py (18)
  • CircleEnergyLogsRequest (1131-1165)
  • response (124-128)
  • send (366-375)
  • send (388-397)
  • send (439-441)
  • send (477-486)
  • send (513-522)
  • send (543-554)
  • send (567-576)
  • send (589-598)
  • send (622-631)
  • send (668-677)
  • send (720-729)
  • send (776-785)
  • send (809-818)
  • send (849-858)
  • send (881-890)
  • send (913-922)
plugwise_usb/api.py (1)
  • save_cache (368-371)
plugwise_usb/network/cache.py (1)
  • save_cache (27-34)
plugwise_usb/nodes/helpers/cache.py (1)
  • save_cache (40-46)
🔇 Additional comments (3)
plugwise_usb/nodes/circle.py (3)

5-5: Top-level asyncio.gather import fix looks good

Importing gather at module scope resolves PLC0415 and keeps cancellation draining clean.


371-373: Storing the boolean result for the “last log requested” flag is correct

This aligns the initial fetch logic with the new boolean contract of energy_log_update.


517-526: Good: cancel-and-drain pattern avoids “Task exception was never retrieved”

Creating all tasks at once, short-circuiting on first False, and draining cancellation via gather(return_exceptions=True) is robust.

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 (2)
plugwise_usb/nodes/circle.py (2)

549-551: Guard against None timestamp before updating availability

This mirrors the defensive pattern used in power_update and avoids passing a None timestamp.

Apply:

-        await self._available_update_state(True, response.timestamp)
+        if response.timestamp is not None:
+            await self._available_update_state(True, response.timestamp)
+        else:
+            await self._available_update_state(True)

456-475: Fix “consumption-only” vs “production+consumption” factor when production is disabled via NO_PRODUCTION_INTERVAL

Currently, production disabled via NO_PRODUCTION_INTERVAL is treated as “production enabled,” yielding factor=2 instead of 4. This halves the intended hours-per-address and over-collects addresses.

Use an explicit check that treats both None and NO_PRODUCTION_INTERVAL as “consumption-only.”

Apply:

-        # When only consumption is measured, 1 address contains data from 4 hours
-        # When both consumption and production are measured, 1 address contains data from 2 hours
-        factor = 4 if self.energy_production_interval is None else 2
+        # When only consumption is measured, 1 address contains data from 4 hours
+        # When both consumption and production are measured, 1 address contains data from 2 hours
+        cons_only = self.energy_production_interval in (None, NO_PRODUCTION_INTERVAL)
+        factor = 4 if cons_only else 2
         max_addresses_to_collect = MAX_LOG_HOURS // factor
         total_addresses = min(
             max_addresses_to_collect, ceil(datetime.now(tz=UTC).hour / factor) + 1
         )
🧹 Nitpick comments (2)
plugwise_usb/nodes/circle.py (2)

418-426: Single missing address fast-path is OK; consider logging on False

Optional: add a small debug log when result is False to aid troubleshooting before falling back to the background task.

Apply:

         if len(missing_addresses) == 1:
             result = await self.energy_log_update(missing_addresses[0])
             if result:
                 await self.power_update()
                 _LOGGER.debug(
                     "async_energy_update for %s | single energy log is missing | %s",
                     self._mac_in_str,
                     missing_addresses,
                 )
                 return self._energy_counters.energy_statistics
+            else:
+                _LOGGER.debug(
+                    "async_energy_update for %s | single-address update returned False, deferring to task",
+                    self._mac_in_str,
+                )

587-604: Optional: avoid repeated datetime.now calls during per-slot checks

Minor micro-optimization: compute “now” once per response to reduce time calls inside the hot loop.

Apply within energy_log_update before the for-loop:

-        for _slot in range(4, 0, -1):
+        now_utc = datetime.now(tz=UTC)
+        for _slot in range(4, 0, -1):

and change _check_timestamp_is_recent to accept an optional now_utc parameter:

-    def _check_timestamp_is_recent(
-        self, address: int, slot: int, timestamp: datetime
-    ) -> bool:
+    def _check_timestamp_is_recent(
+        self, address: int, slot: int, timestamp: datetime, now_utc: datetime | None = None
+    ) -> bool:
         """Check if a log record timestamp is within the last MAX_LOG_HOURS hours."""
-        age_seconds = (
-            datetime.now(tz=UTC) - timestamp.replace(tzinfo=UTC)
-        ).total_seconds()
+        reference = now_utc or datetime.now(tz=UTC)
+        age_seconds = (reference - timestamp.replace(tzinfo=UTC)).total_seconds()

and call it as:

-                or not self._check_timestamp_is_recent(address, _slot, log_timestamp)
+                or not self._check_timestamp_is_recent(address, _slot, log_timestamp, now_utc)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8378501 and 702af00.

📒 Files selected for processing (1)
  • plugwise_usb/nodes/circle.py (8 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
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.
📚 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 (5)
plugwise_usb/nodes/helpers/pulses.py (2)
  • calc_log_address (20-36)
  • add_empty_log (389-421)
plugwise_usb/nodes/node.py (2)
  • energy_consumption_interval (179-185)
  • energy_production_interval (189-195)
plugwise_usb/messages/responses.py (2)
  • log_address (770-772)
  • log_data (775-794)
plugwise_usb/messages/requests.py (18)
  • CircleEnergyLogsRequest (1131-1165)
  • response (124-128)
  • send (366-375)
  • send (388-397)
  • send (439-441)
  • send (477-486)
  • send (513-522)
  • send (543-554)
  • send (567-576)
  • send (589-598)
  • send (622-631)
  • send (668-677)
  • send (720-729)
  • send (776-785)
  • send (809-818)
  • send (849-858)
  • send (881-890)
  • send (913-922)
plugwise_usb/nodes/helpers/counter.py (4)
  • timestamp (179-187)
  • add_empty_log (70-72)
  • update (147-176)
  • update (286-328)
⏰ 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 (11)
plugwise_usb/nodes/circle.py (11)

5-5: Top-level asyncio import for gather resolves PLC0415 and is correct

Importing gather at module scope is the right fix and matches its usage below.


11-11: Use of ceil for address grouping aligns with hour-to-address mapping

Using ceil(...) for computing the number of addresses to cover the current day is appropriate.


77-78: MAX_LOG_HOURS derived from DAY_IN_HOURS is clear and matches PR goal

Keeps recency filtering bounded to one day by default, as intended.


370-372: Initial last-log request now follows boolean contract

Storing the boolean result from energy_log_update into _last_energy_log_requested is consistent and keeps retry semantics intact.


384-391: Rollover path: boolean handling and diagnostics look good

Checking the boolean result and logging the address on failure provides useful signal without exceptions.


397-405: Prev-address retry on rollover is correct

calc_log_address(..., -4) for the previous address is appropriate; logging on failure is helpful.


479-486: Early stop on empty/outdated address is sensible

Halting initial collection when a whole address is stale prevents unnecessary older fetches and matches the MAX_LOG_HOURS constraint.


516-525: Cancel-and-drain pattern avoids “Task exception was never retrieved”

Good: cancel remaining tasks and drain them with gather(..., return_exceptions=True).


560-567: Empty or outdated slot handling is correct

Marking empty/outdated slots explicitly keeps PulseCollection ranges coherent for missing-address computation.


569-576: import_only=True is appropriate during batch import

Defers intermediate statistics recalculations until after the loop, then calls update() once.


579-585: Write cache only when a new record was stored

Prevents unnecessary I/O when the response was entirely empty/outdated.

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)

471-474: Production-interval check misses NO_PRODUCTION_INTERVAL

When production logging is disabled via NO_PRODUCTION_INTERVAL (not None), the current check treats it as “production present” and uses factor=2, leading to over-collection. Consider both None and NO_PRODUCTION_INTERVAL as “consumption-only.”

-        cons_only = self.energy_production_interval is None
-        factor = 4 if cons_only else 2
+        cons_only = self.energy_production_interval in (None, NO_PRODUCTION_INTERVAL)
+        factor = 4 if cons_only else 2
🧹 Nitpick comments (3)
plugwise_usb/nodes/circle.py (3)

456-457: Docstring wording doesn’t match behavior (today vs recent hours)

The logic can include records from the previous day as long as they are within MAX_LOG_HOURS. To avoid confusion, update the docstring to “recent hours” rather than “hours elapsed today”.

-        """Collect initial energy logs for the hours elapsed today up to MAX_LOG_HOURS."""
+        """Collect initial energy logs for recent hours up to MAX_LOG_HOURS."""

531-549: Avoid redundant cache writes from batch updates

energy_log_update saves the cache when any record is stored, and get_missing_energy_logs subsequently saves the entire collection again. This can cause unnecessary I/O churn during batch updates. Consider allowing the caller to suppress the per-call save and rely on the aggregated save at the end of the batch.

-    async def energy_log_update(self, address: int | None) -> bool:
+    async def energy_log_update(self, address: int | None, *, save_cache: bool = True) -> bool:
@@
-        if any_record_stored and self._cache_enabled:
+        if any_record_stored and self._cache_enabled and save_cache:
             _LOGGER.debug(
                 "Saving energy record update to cache for %s", self._mac_in_str
             )
             await self.save_cache()

And call with save_cache=False from batch/initial collection paths:

-            result = await self.energy_log_update(log_address)
+            result = await self.energy_log_update(log_address, save_cache=False)
-        tasks = [
-            create_task(self.energy_log_update(address))
+        tasks = [
+            create_task(self.energy_log_update(address, save_cache=False))
             for address in missing_addresses
         ]

Also applies to: 580-587


588-595: Minor: future timestamps due to drift — treat as recent explicitly

If node clocks are slightly ahead, age_seconds can be negative. Explicitly clamping to 0 avoids edge-case surprises and keeps intent clear.

-        age_seconds = (
-            datetime.now(tz=UTC) - timestamp.replace(tzinfo=UTC)
-        ).total_seconds()
+        age_seconds = max(
+            0.0,
+            (datetime.now(tz=UTC) - timestamp.replace(tzinfo=UTC)).total_seconds(),
+        )
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 702af00 and 991743e.

📒 Files selected for processing (1)
  • plugwise_usb/nodes/circle.py (8 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
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.
📚 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
📚 Learning: 2025-08-13T13:20:25.164Z
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#311
File: plugwise_usb/nodes/circle.py:550-551
Timestamp: 2025-08-13T13:20:25.164Z
Learning: In the plugwise_usb library, the PlugwiseMessage class always provides a timestamp in responses, so defensive null checks for response.timestamp are not necessary when calling _available_update_state().

Applied to files:

  • plugwise_usb/nodes/circle.py
🔇 Additional comments (8)
plugwise_usb/nodes/circle.py (8)

5-5: Top-level asyncio.gather and math.ceil import — good change

Resolves PLC0415 and enables proper batching and rounding logic later.

Also applies to: 11-11


28-28: MAX_LOG_HOURS tied to DAY_IN_HOURS — aligned with PR objective

Clear, single source of truth for the recency window. Good.

Also applies to: 77-78


370-372: Correctly adopting the new boolean contract from energy_log_update

This aligns the initial “last log” request with the simplified True/False return.


384-391: Rollover handling now logs addresses and retries previous address — solid

The explicit logging and retry on previous address improves debuggability and robustness during rollover.

Also applies to: 396-404


417-426: Single-missing-address fast path looks good

Short-circuiting when the single update succeeds reduces overhead and matches the new return semantics.


474-476: Confirm intended scope for initial collection (today-only vs last N hours)

The formula ceil(datetime.now(tz=UTC).hour / factor) + 1 can pull an extra previous-day address during the early hours (e.g., at 01:xx with consumption-only). If your intent is “recent hours within MAX_LOG_HOURS,” this is fine and the docstring should reflect that (see previous comment). If you actually want “today-only,” consider ceil((datetime.now(tz=UTC).hour + 1) / factor) instead.


479-487: Early break on first non-recent/empty address — good stop condition

Prevents scanning further back once the current address has no usable data (handles rollover gracefully).


517-526: Properly awaiting canceled tasks

Canceling and draining with gather(return_exceptions=True) avoids “Task exception was never retrieved” and resource leaks. Nice.

@bouwew bouwew force-pushed the improve-energy-collection branch from cbd8902 to f6c4d36 Compare August 13, 2025 16:20
@sonarqubecloud
Copy link

@bouwew bouwew merged commit d3a7331 into main Aug 13, 2025
15 of 17 checks passed
@bouwew bouwew deleted the improve-energy-collection branch August 13, 2025 16:27
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)

456-477: Consider NO_PRODUCTION_INTERVAL when determining consumption-only factor

When production logging is disabled, energy_production_interval may be None or equal to NO_PRODUCTION_INTERVAL. The current check only handles None, which will misclassify “consumption-only” as “production-enabled” when the interval equals NO_PRODUCTION_INTERVAL, halving the initial address count.

Apply:

-        cons_only = self.energy_production_interval is None
+        cons_only = self.energy_production_interval in (None, NO_PRODUCTION_INTERVAL)
         factor = 4 if cons_only else 2

This matches the documented mapping:

  • consumption-only → 1 address covers 4 hours
  • consumption+production → 1 address covers 2 hours
🧹 Nitpick comments (3)
plugwise_usb/nodes/circle.py (3)

370-372: Avoid repeated last-log re-requests: set the flag independent of storage success

Currently, _last_energy_log_requested is set to True only when energy_log_update returns True (i.e., when at least one recent, non-empty record was stored). If the last address contains no recent data, this will keep re-requesting the same last address on every energy_update call, adding network churn with no benefit.

Set the flag after the first attempt to prevent repeated calls; cache/save behavior is already controlled by save_cache and the subsequent flow.

Proposed diff:

-            self._last_energy_log_requested = await self.energy_log_update(
-                self._current_log_address, save_cache=False
-            )
+            await self.energy_log_update(self._current_log_address, save_cache=False)
+            self._last_energy_log_requested = True

If you intentionally want to keep retrying until a recent record is present, consider renaming the attribute to reflect that behavior (e.g., _last_energy_log_present) to avoid confusion.


531-587: Minor: use response.log_address in recency check for accuracy in logs

If the device ever responds with a different address than requested (rare, but the code already trusts response.log_address for storage), use response.log_address in _check_timestamp_is_recent for consistent logging context.

Proposed diff:

-                or not self._check_timestamp_is_recent(address, _slot, log_timestamp)
+                or not self._check_timestamp_is_recent(
+                    response.log_address, _slot, log_timestamp
+                )

Also, the docstring says “Return True only when at least one recent, non-empty record was stored.” The code sets any_record_stored = True for every recent, non-empty record processed, regardless of whether it changed the cache (dedup). That’s fine if “stored” refers to in-memory counters; if you intend “stored in cache,” consider OR-ing the boolean returned by _energy_log_record_update_state instead.


588-605: Reduce log level for expected “outdated” records to avoid noise

Outdated records are expected when enforcing a sliding window and during rollovers. Warning-level logs can be noisy. Recommend downgrading to debug (or info if you want visibility by default).

Proposed diff:

-            _LOGGER.warning(
+            _LOGGER.debug(
                 "EnergyLog from Node %s | address %s | slot %s | timestamp %s is outdated, ignoring...",
                 self._mac_in_str,
                 address,
                 slot,
                 timestamp,
             )

If desired, I can add unit tests targeting:

  • Boundary behavior at exactly MAX_LOG_HOURS age (should be accepted).
  • Slightly over the limit (should be rejected).
  • Clock skew scenarios (negative age clamped to zero).
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 991743e and f6c4d36.

📒 Files selected for processing (1)
  • plugwise_usb/nodes/circle.py (8 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
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.
📚 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
📚 Learning: 2025-08-13T13:20:25.164Z
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#311
File: plugwise_usb/nodes/circle.py:550-551
Timestamp: 2025-08-13T13:20:25.164Z
Learning: In the plugwise_usb library, the PlugwiseMessage class always provides a timestamp in responses, so defensive null checks for response.timestamp are not necessary when calling _available_update_state().

Applied to files:

  • plugwise_usb/nodes/circle.py
🧬 Code Graph Analysis (1)
plugwise_usb/nodes/circle.py (7)
plugwise_usb/api.py (1)
  • save_cache (368-371)
plugwise_usb/network/cache.py (1)
  • save_cache (27-34)
plugwise_usb/nodes/node.py (3)
  • save_cache (705-719)
  • energy_consumption_interval (179-185)
  • energy_production_interval (189-195)
plugwise_usb/nodes/helpers/cache.py (1)
  • save_cache (40-46)
plugwise_usb/nodes/helpers/pulses.py (2)
  • calc_log_address (20-36)
  • add_empty_log (389-421)
plugwise_usb/messages/responses.py (2)
  • log_address (770-772)
  • log_data (775-794)
plugwise_usb/messages/requests.py (18)
  • CircleEnergyLogsRequest (1131-1165)
  • response (124-128)
  • send (366-375)
  • send (388-397)
  • send (439-441)
  • send (477-486)
  • send (513-522)
  • send (543-554)
  • send (567-576)
  • send (589-598)
  • send (622-631)
  • send (668-677)
  • send (720-729)
  • send (776-785)
  • send (809-818)
  • send (849-858)
  • send (881-890)
  • send (913-922)
🔇 Additional comments (4)
plugwise_usb/nodes/circle.py (4)

5-5: MAX_LOG_HOURS introduction and import adjustments look good

  • Importing gather and ceil is appropriate for the new task-draining and address math.
  • Using DAY_IN_HOURS to define MAX_LOG_HOURS aligns the code to a clear, single source of truth for “last 24h”.

Also applies to: 11-11, 28-28, 77-78


384-391: Rollover handling path is consistent and clear

  • Correctly retries current and previous addresses and handles failures by exiting early.
  • Logging provides sufficient context for diagnosing rollover issues.

Also applies to: 396-405


479-486: Early stop on empty/outdated initial address is sensible

Breaking the initial collection when the address yields only empty/outdated slots avoids unnecessary requests and aligns with the recency constraint.


514-526: Good: cancel and drain remaining tasks on first False result

  • Properly cancels remaining tasks and awaits them with gather(..., return_exceptions=True), preventing “Task exception was never retrieved” issues.
  • Top-level import of gather resolves PLC0415.

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.

3 participants