Skip to content

Conversation

@bouwew
Copy link
Contributor

@bouwew bouwew commented Aug 30, 2025

This PR implements my alternative idea to store the contents of self.logs into the energy-collection cache entry: a copy of the (limited to 24h) energy-log data in memory is stored on disk instead of a separate collection that keeps on growing because no pruning of older data was implemented.

Also, an inconsistency (re-)introduced in energy_log_update() was solved.

Finally, various suggestions from the Rabbit were implemented.

Summary by CodeRabbit

  • Refactor
    • Streamlined energy-log caching to prefer in-memory updates with centralized, conditional disk saves; improved handling during log rollovers and initial sync to reduce I/O and improve stability.
  • Bug Fixes
    • Fixed a minor log message typo.
  • Documentation
    • Added CHANGELOG entry documenting EnergyLogs caching improvements.
  • Chores
    • Bumped package version to 0.44.14.

@coderabbitai
Copy link

coderabbitai bot commented Aug 30, 2025

Walkthrough

Refactors energy log handling in plugwise_usb/nodes/circle.py: energy_log_update now returns (result, slots_empty), centralizes cache persistence via save_cache(), updates startup/rollover/initial/missing-log flows and unload to use new semantics, bumps project version, and updates CHANGELOG.

Changes

Cohort / File(s) Summary
Energy log update & caching
plugwise_usb/nodes/circle.py
Change PlugwiseCircle.energy_log_update signature to return (bool result, bool slots_empty); update startup, rollover, initial-collection, and missing-log flows to handle tuple returns; in-memory cache updates centralized and disk writes moved to save_cache(); remove direct disk writes from _energy_log_records_save_to_cache; minor log message fix (relay_init wording).
Project metadata
pyproject.toml
Bump version from 0.44.13 to 0.44.14.
Documentation
CHANGELOG.md
Add 0.44.14 entry documenting improved EnergyLogs caching (PR 329).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor App as Controller
  participant Circle as PlugwiseCircle
  participant Mem as In-memory Cache
  participant Disk as Disk (save_cache)

  Note over Circle,Mem: Startup / single update
  App->>Circle: energy_log_update(addr, save_cache=false)
  activate Circle
  Circle->>Mem: update in-memory slots
  Circle-->>App: (result, slots_empty)
  deactivate Circle

  rect rgba(230,245,255,0.5)
    Note over App,Circle: Rollover handling
    App->>Circle: energy_log_update(cur_addr, save_cache=false)
    Circle-->>App: (cur_ok, cur_empty)
    alt cur_ok
      App->>Circle: energy_log_update(prev_addr, save_cache=false)
      Circle-->>App: (prev_ok, prev_empty)
      alt cur_empty == false or prev_empty == false
        App->>Circle: save_cache()
        Circle->>Disk: persist cache
        Disk-->>Circle: ok
      else
        Note over App: no disk write
      end
    else
      Note over App: halt on failure
    end
  end

  rect rgba(235,255,235,0.5)
    Note over App,Circle: Missing logs (parallel)
    par for each addr
      App->>Circle: energy_log_update(addr, save_cache=false)
      Circle-->>App: (ok, slots_empty)
    and ...
    end
    alt any slots not empty
      App->>Circle: save_cache()
      Circle->>Disk: persist cache
      Disk-->>Circle: ok
    else
      Note over App: skip persistence
    end
  end

  rect rgba(255,240,230,0.5)
    Note over App,Circle: Unload
    App->>Circle: save_cache()
    Circle->>Disk: persist cache
    Disk-->>Circle: ok
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • ArnoutD
  • dirixmjm

Poem

A little rabbit with bytes to spare,
I hop through logs with careful care.
Tuples in paw and cache held tight,
I save to disk only when it's right.
Hooray for clean hops in the night! 🐇✨


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 16517a3 and 62753d0.

📒 Files selected for processing (2)
  • CHANGELOG.md (1 hunks)
  • pyproject.toml (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • pyproject.toml
⏰ 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: Check commit
✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch energy_log_cache

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 or @coderabbit 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 30, 2025

Codecov Report

❌ Patch coverage is 38.09524% with 26 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.70%. Comparing base (b7176ce) to head (62753d0).
⚠️ Report is 24 commits behind head on main.

Files with missing lines Patch % Lines
plugwise_usb/nodes/circle.py 38.09% 26 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #329      +/-   ##
==========================================
+ Coverage   80.56%   80.70%   +0.13%     
==========================================
  Files          36       36              
  Lines        7564     7560       -4     
==========================================
+ Hits         6094     6101       +7     
+ Misses       1470     1459      -11     

☔ 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: 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)

597-626: Fix cache_updated aggregation so cache flush isn’t skipped.

Inside energy_log_update(), cache_updated is overwritten per slot; if an earlier slot updated but a later didn’t, save_cache won’t trigger, risking loss on restart.

Apply:

-        cache_updated = False
-        for _slot in range(4, 0, -1):
+        cache_updated = 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 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

-            cache_updated = await self._energy_log_record_update_state(
+            updated = await self._energy_log_record_update_state(
                 response.log_address,
                 _slot,
                 log_timestamp.replace(tzinfo=UTC),
                 log_pulses,
                 import_only=True,
             )
+            cache_updated = cache_updated or updated
🧹 Nitpick comments (4)
plugwise_usb/nodes/circle.py (4)

534-535: Optional: force synchronous flush at batch ends.

Initial and “missing logs” bulk imports end with await self.save_cache(), which schedules a write (trigger_only=True). If you want stronger durability immediately after batch imports, flush synchronously.

-        await self.save_cache()
+        await self.save_cache(trigger_only=False)

Also applies to: 569-570


744-745: Avoid re-serializing the full energy log cache per record.

_energy_log_record_update_state() serializes the entire logs set for every new record, multiplying work during bulk imports.

Batch once per address:

-                _LOGGER.debug(
-                    "Adding logrecord (%s, %s) to cache of %s",
-                    str(address),
-                    str(slot),
-                    self._mac_in_str,
-                )
-                await self._energy_log_records_save_to_cache()
+                _LOGGER.debug(
+                    "Adding logrecord (%s, %s) to cache of %s",
+                    str(address),
+                    str(slot),
+                    self._mac_in_str,
+                )
                 return True
@@
-        _LOGGER.debug(
+        _LOGGER.debug(
             "Cache is empty, adding new logrecord (%s, %s) for %s",
             str(address),
             str(slot),
             self._mac_in_str,
         )
-        await self._energy_log_records_save_to_cache()
         return True

And, in energy_log_update(), save the cache string once per address when anything changed:

-        self._energy_counters.update()
-        if cache_updated and save_cache:
+        self._energy_counters.update()
+        if cache_updated:
+            await self._energy_log_records_save_to_cache()
+        if cache_updated and save_cache:
             _LOGGER.debug(
                 "Saving energy record update to cache for %s", self._mac_in_str
             )
             await self.save_cache()

Also applies to: 758-759


574-574: Docstring doesn’t match intended semantics.

Per prior learning (PR #314), True indicates successful processing/storage in memory, not necessarily “at least one recent, non-empty record stored.”

Using that learning: update the docstring for clarity.

-        """Request energy logs and return True only when at least one recent, non-empty record was stored; otherwise return False."""
+        """Request energy logs; return True if processing succeeded (records stored in memory), regardless of whether new entries were added. Return False on transport or address errors."""

1182-1184: Typo in log message.

“cachefor” → “cache for”.

-                "Saving relay_init state update to cachefor %s", self._mac_in_str
+                "Saving relay_init state update to cache for %s", self._mac_in_str
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b7176ce and d26521a.

📒 Files selected for processing (2)
  • plugwise_usb/nodes/circle.py (5 hunks)
  • pyproject.toml (1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#314
File: plugwise_usb/nodes/circle.py:695-705
Timestamp: 2025-08-13T17:29:32.348Z
Learning: In plugwise_usb/nodes/circle.py, the method self._energy_counters.get_pulse_logs() already returns data in reverse-sorted order (newest-first), so no additional sorting is required when serializing energy log records to cache.
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#314
File: plugwise_usb/nodes/circle.py:570-573
Timestamp: 2025-08-14T12:26:12.780Z
Learning: In plugwise_usb/nodes/circle.py, the energy_log_update method's return value of True indicates successful processing and storing of energy log records in memory (via _energy_counters), regardless of whether the records were new, duplicates, or empty. The docstring "return True only when at least one recent, non-empty record was stored" refers to successful processing/storage in the energy counters system, not necessarily new cache entries.
📚 Learning: 2025-08-13T17:29:32.348Z
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#314
File: plugwise_usb/nodes/circle.py:695-705
Timestamp: 2025-08-13T17:29:32.348Z
Learning: In plugwise_usb/nodes/circle.py, the method self._energy_counters.get_pulse_logs() already returns data in reverse-sorted order (newest-first), so no additional sorting is required when serializing energy log records to cache.

Applied to files:

  • plugwise_usb/nodes/circle.py
📚 Learning: 2025-08-14T12:26:12.780Z
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#314
File: plugwise_usb/nodes/circle.py:570-573
Timestamp: 2025-08-14T12:26:12.780Z
Learning: In plugwise_usb/nodes/circle.py, the energy_log_update method's return value of True indicates successful processing and storing of energy log records in memory (via _energy_counters), regardless of whether the records were new, duplicates, or empty. The docstring "return True only when at least one recent, non-empty record was stored" refers to successful processing/storage in the energy counters system, not necessarily new cache entries.

Applied to files:

  • plugwise_usb/nodes/circle.py
📚 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 (2)
plugwise_usb/nodes/node.py (1)
  • save_cache (706-720)
plugwise_usb/nodes/helpers/cache.py (1)
  • save_cache (40-46)
🔇 Additional comments (3)
pyproject.toml (1)

7-7: Version bump looks fine.

Pre-release suffix aligns with draft status; no issues spotted.

plugwise_usb/nodes/circle.py (2)

444-445: Good: explicit save after rollover.

Guarantees persistence after dual-address rollover fetches; aligns cache with memory.


1105-1109: Remove redundant disk flush on unload. The base PlugwiseBaseNode.unload() already calls save_cache(trigger_only=False, full_write=True) to synchronously write the cache to disk, so adding another await self.save_cache(trigger_only=False) is unnecessary.

Likely an incorrect or invalid review comment.

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

600-616: Bug: cache_updated is overwritten per slot; use OR to accumulate

As written, a later False can clobber an earlier True, skipping persistence when at least one slot updated. Accumulate instead.

Apply:

-        cache_updated = False
+        cache_updated = False
         for _slot in range(4, 0, -1):
             log_timestamp, log_pulses = response.log_data[_slot]
@@
-            cache_updated = await self._energy_log_record_update_state(
+            slot_updated = await self._energy_log_record_update_state(
                 response.log_address,
                 _slot,
                 log_timestamp.replace(tzinfo=UTC),
                 log_pulses,
                 import_only=True,
             )
+            cache_updated |= slot_updated

623-629: Batch-flush in-memory cache once per address (avoid per-slot rebuild + races)

Call _energy_log_records_save_to_cache() once after processing all 4 slots, then save to disk if requested. This reduces O(N) rebuilds per slot and avoids interleaved writers from concurrent tasks.

Apply:

-        self._energy_counters.update()
-        if cache_updated and save_cache:
+        self._energy_counters.update()
+        if cache_updated and self._cache_enabled:
+            await self._energy_log_records_save_to_cache()
+        if cache_updated and save_cache:
             _LOGGER.debug(
                 "Saving energy record update to cache for %s", self._mac_in_str
             )
             await self.save_cache()

735-748: Avoid per-slot in-memory cache rebuilds inside _energy_log_record_update_state

Each new/changed slot triggers a full-string rebuild via _energy_log_records_save_to_cache(), which is O(N) and can interleave across concurrent tasks. Let the caller batch-flush once.

Apply:

         if (cached_logs := self._get_cache(CACHE_ENERGY_COLLECTION)) is not None:
             entries = cached_logs.split("|") if cached_logs else []
             if log_cache_record not in entries:
                 _LOGGER.debug(
                     "Adding logrecord (%s, %s) to cache of %s",
                     str(address),
                     str(slot),
                     self._mac_in_str,
                 )
-                await self._energy_log_records_save_to_cache()
                 return True

755-762: Same here: batch-flush instead of rebuilding per-slot

Remove the immediate rebuild; let the caller handle it once.

Apply:

         _LOGGER.debug(
             "Cache is empty, adding new logrecord (%s, %s) for %s",
             str(address),
             str(slot),
             self._mac_in_str,
         )
-        await self._energy_log_records_save_to_cache()
         return True
🧹 Nitpick comments (5)
plugwise_usb/nodes/circle.py (5)

700-719: Docstring/log message are misleading: this function no longer writes to file

The helper now only updates the in-memory cache string. Fix wording to prevent confusion.

Apply:

-    async def _energy_log_records_save_to_cache(self) -> None:
-        """Save currently collected energy logs to cached file."""
+    async def _energy_log_records_save_to_cache(self) -> None:
+        """Update the in-memory energy log cache string (no file I/O)."""
@@
-        _LOGGER.debug("Saving energy logrecords to cache for %s", self._mac_in_str)
+        _LOGGER.debug("Updating in-memory energy log records for %s", self._mac_in_str)

535-537: Initial collection: optionally flush in-memory before saving to disk

If you adopt the batch-flush pattern above, add an in-memory flush here prior to save_cache().

Apply:

-        if self._cache_enabled:
-            await self.save_cache()
+        if self._cache_enabled:
+            await self._energy_log_records_save_to_cache()
+            await self.save_cache()

571-573: Missing-logs task: flush in-memory before disk save (align with new pattern)

Same rationale as initial collection.

Apply:

-        if self._cache_enabled:
-            await self.save_cache()
+        if self._cache_enabled:
+            await self._energy_log_records_save_to_cache()
+            await self.save_cache()

574-581: Docstring of energy_log_update no longer matches semantics

Implementation returns True for any successfully processed response (including empty/outdated slots). Update wording to match behavior and the prior learning.

Apply:

-    ) -> bool:
-        """Request energy logs and return True only when at least one recent, non-empty record was stored; otherwise return False."""
+    ) -> bool:
+        """Request energy logs and return True when logs were processed into memory; False only on request/parameter failure."""

717-717: Minor: fix log wording

“logrecords” → “log records”.

Apply:

-        _LOGGER.debug("Saving energy logrecords to cache for %s", self._mac_in_str)
+        _LOGGER.debug("Saving energy log records to cache for %s", self._mac_in_str)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2d7317a and 2d25c76.

📒 Files selected for processing (1)
  • plugwise_usb/nodes/circle.py (7 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#314
File: plugwise_usb/nodes/circle.py:695-705
Timestamp: 2025-08-13T17:29:32.348Z
Learning: In plugwise_usb/nodes/circle.py, the method self._energy_counters.get_pulse_logs() already returns data in reverse-sorted order (newest-first), so no additional sorting is required when serializing energy log records to cache.
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#314
File: plugwise_usb/nodes/circle.py:570-573
Timestamp: 2025-08-14T12:26:12.780Z
Learning: In plugwise_usb/nodes/circle.py, the energy_log_update method's return value of True indicates successful processing and storing of energy log records in memory (via _energy_counters), regardless of whether the records were new, duplicates, or empty. The docstring "return True only when at least one recent, non-empty record was stored" refers to successful processing/storage in the energy counters system, not necessarily new cache entries.
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-08-13T17:29:32.348Z
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#314
File: plugwise_usb/nodes/circle.py:695-705
Timestamp: 2025-08-13T17:29:32.348Z
Learning: In plugwise_usb/nodes/circle.py, the method self._energy_counters.get_pulse_logs() already returns data in reverse-sorted order (newest-first), so no additional sorting is required when serializing energy log records to cache.

Applied to files:

  • plugwise_usb/nodes/circle.py
📚 Learning: 2025-08-14T12:26:12.780Z
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#314
File: plugwise_usb/nodes/circle.py:570-573
Timestamp: 2025-08-14T12:26:12.780Z
Learning: In plugwise_usb/nodes/circle.py, the energy_log_update method's return value of True indicates successful processing and storing of energy log records in memory (via _energy_counters), regardless of whether the records were new, duplicates, or empty. The docstring "return True only when at least one recent, non-empty record was stored" refers to successful processing/storage in the energy counters system, not necessarily new cache entries.

Applied to files:

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

419-446: Rollover path: deferring disk writes is correct

Switching both energy_log_update calls to save_cache=False and issuing a single save_cache() afterward keeps disk I/O bounded. LGTM.


1108-1111: Incorrect: super().unload already persists cache to disk
PlugwiseBaseNode.unload checks _cache_enabled, then calls

await self.save_cache(trigger_only=False, full_write=True)

which synchronously writes the Node’s cache to disk . Therefore no additional save is needed here.

Likely an incorrect or invalid review comment.

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

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)

903-906: Clock drift check uses timedelta.seconds (bug) — use total_seconds()

timedelta.seconds is modulo 24h and non-negative; the current condition is effectively always True. Compare abs(clock_offset.total_seconds()) against MAX_TIME_DRIFT.

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

711-714: Docstring nit: this writes to the in-memory cache, not the file

_save_to_cache() updates the node’s cached state; disk persistence happens via save_cache().

-    """Save currently collected energy logs to cached file."""
+    """Serialize in-memory energy logs into the node's in-memory cache.
+
+    Note: call save_cache() elsewhere to persist to disk.
+    """

526-533: Message/behavior mismatch: “stopping initial collection” log won’t trigger

energy_log_update() now returns False only on transport/address errors, not when all slots are empty/outdated. The “stopping initial collection” message won’t occur. Either:

  • detect “all slots were empty/outdated” explicitly and break, or
  • reword the message to reflect the current behavior.

Example (detect empties via a returned flag):

-            result = await self.energy_log_update(log_address, save_cache=False)
-            if not result:
+            processed, had_recent = await self.energy_log_update(log_address, save_cache=False)
+            if not had_recent:
                 _LOGGER.debug(
                     "All slots at log address %s are empty or outdated – stopping initial collection",
                     log_address,
                 )
                 break

(If changing the signature is undesirable, consider a lightweight helper on EnergyCounters to detect “had any recent for address/slot”.)

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ecd8889 and 47ea2cc.

📒 Files selected for processing (2)
  • plugwise_usb/nodes/circle.py (7 hunks)
  • pyproject.toml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • pyproject.toml
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#314
File: plugwise_usb/nodes/circle.py:695-705
Timestamp: 2025-08-13T17:29:32.348Z
Learning: In plugwise_usb/nodes/circle.py, the method self._energy_counters.get_pulse_logs() already returns data in reverse-sorted order (newest-first), so no additional sorting is required when serializing energy log records to cache.
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#314
File: plugwise_usb/nodes/circle.py:570-573
Timestamp: 2025-08-14T12:26:12.780Z
Learning: In plugwise_usb/nodes/circle.py, the energy_log_update method's return value of True indicates successful processing and storing of energy log records in memory (via _energy_counters), regardless of whether the records were new, duplicates, or empty. The docstring "return True only when at least one recent, non-empty record was stored" refers to successful processing/storage in the energy counters system, not necessarily new cache entries.
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-08-14T12:26:12.780Z
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#314
File: plugwise_usb/nodes/circle.py:570-573
Timestamp: 2025-08-14T12:26:12.780Z
Learning: In plugwise_usb/nodes/circle.py, the energy_log_update method's return value of True indicates successful processing and storing of energy log records in memory (via _energy_counters), regardless of whether the records were new, duplicates, or empty. The docstring "return True only when at least one recent, non-empty record was stored" refers to successful processing/storage in the energy counters system, not necessarily new cache entries.

Applied to files:

  • plugwise_usb/nodes/circle.py
📚 Learning: 2025-08-13T17:29:32.348Z
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#314
File: plugwise_usb/nodes/circle.py:695-705
Timestamp: 2025-08-13T17:29:32.348Z
Learning: In plugwise_usb/nodes/circle.py, the method self._energy_counters.get_pulse_logs() already returns data in reverse-sorted order (newest-first), so no additional sorting is required when serializing energy log records to cache.

Applied to files:

  • plugwise_usb/nodes/circle.py
🧬 Code graph analysis (1)
plugwise_usb/nodes/circle.py (2)
plugwise_usb/nodes/node.py (1)
  • save_cache (706-720)
plugwise_usb/nodes/helpers/cache.py (1)
  • save_cache (40-46)
🔇 Additional comments (4)
plugwise_usb/nodes/circle.py (4)

421-423: Good: defer cache writes during rollover fetches

Passing save_cache=False here is the right move to avoid extra disk churn.


435-437: Rollover safety looks correct

Retrying the previous address with calc_log_address(..., 1, -4) is consistent with the 4-slot page.


718-725: Newest-first serialization aligns with EnergyCounters order

Using get_pulse_logs() order directly matches the “newest-first” guarantee (per learnings), so no extra sorting is needed.


1193-1194: Minor log text fix: LGTM

The relay_init cache message reads well now.

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

446-448: Gate and flush before persisting; avoid unconditional disk I/O

Unconditionally calling save_cache() can write when nothing changed and also skips flushing the in-memory serialization first. Per maintainer guidance, persist only when cache content actually changed, and flush the in-memory cache string first.

Apply:

-            if self._cache_enabled:
-                await self.save_cache()
+            if self._cache_enabled:
+                prev = self._get_cache(CACHE_ENERGY_COLLECTION) or ""
+                await self._energy_log_records_save_to_cache()
+                if self._get_cache(CACHE_ENERGY_COLLECTION) != prev:
+                    await self.save_cache()

537-539: Same here: only save if cache actually changed; flush first

Mirror the rollover fix for initial collection completion.

-        if self._cache_enabled:
-            await self.save_cache()
+        if self._cache_enabled:
+            prev = self._get_cache(CACHE_ENERGY_COLLECTION) or ""
+            await self._energy_log_records_save_to_cache()
+            if self._get_cache(CACHE_ENERGY_COLLECTION) != prev:
+                await self.save_cache()

573-575: And for missing-logs task completion

Prevent no-op writes; serialize first and compare.

-        if self._cache_enabled:
-            await self.save_cache()
+        if self._cache_enabled:
+            prev = self._get_cache(CACHE_ENERGY_COLLECTION) or ""
+            await self._energy_log_records_save_to_cache()
+            if self._get_cache(CACHE_ENERGY_COLLECTION) != prev:
+                await self.save_cache()

579-583: Handle empty/outdated data explicitly — update call-sites using if not result:
The changed return semantics (True even for empty/duplicate logs, False only on transport/address errors) means these if not result: checks no longer stop or retry on “no or outdated” data as intended. Update at least the following locations to distinguish real errors from empty/outdated slots (e.g., inspect returned records length or introduce a new status):

  • plugwise_usb/nodes/circle.py:405 (initial startup)
  • plugwise_usb/nodes/circle.py:423 (first collection attempt)
  • plugwise_usb/nodes/circle.py:438 (rollover retry)
  • plugwise_usb/nodes/circle.py:525 (initial loop stop)
🧹 Nitpick comments (4)
plugwise_usb/nodes/circle.py (4)

631-637: Nit: improve debug message wording

“Saving and storing energy cache” is redundant. Consider “Persisting energy log updates for %s”.

-                _LOGGER.debug(
-                    "Saving and storing energy cache for %s", self._mac_in_str
-                )
+                _LOGGER.debug("Persisting energy log updates for %s", self._mac_in_str)

726-726: Nit: consider logging record count

Optionally include the number of serialized records to aid diagnostics (e.g., “Updating in-memory energy log records (N entries) …”).


1192-1192: Nit: unify log phrasing

Keep wording consistent with other “Saving … to cache” messages (e.g., “Saving relay-init state update to cache for %s”).


446-448: Optional: factor persistence into a small helper to avoid repetition

If you prefer DRY, introduce a helper and call it at the three sites.

Add:

async def _persist_energy_cache_if_changed(self) -> None:
    if not self._cache_enabled:
        return
    prev = self._get_cache(CACHE_ENERGY_COLLECTION) or ""
    await self._energy_log_records_save_to_cache()
    if self._get_cache(CACHE_ENERGY_COLLECTION) != prev:
        await self.save_cache()

Then replace each site with:

await self._persist_energy_cache_if_changed()

Also applies to: 537-539, 573-575

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c1b221b and ae4d939.

📒 Files selected for processing (1)
  • plugwise_usb/nodes/circle.py (9 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#329
File: plugwise_usb/nodes/circle.py:0-0
Timestamp: 2025-08-30T10:46:42.627Z
Learning: In plugwise_usb/nodes/circle.py, the save_cache() method should only be called when both cache_updated AND save_cache parameters are True. If no cache updates occurred (cache_updated = False), there's no meaningful data to persist to disk, even if the caller requested saving (save_cache = True). This prevents unnecessary disk I/O operations.
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#314
File: plugwise_usb/nodes/circle.py:695-705
Timestamp: 2025-08-13T17:29:32.348Z
Learning: In plugwise_usb/nodes/circle.py, the method self._energy_counters.get_pulse_logs() already returns data in reverse-sorted order (newest-first), so no additional sorting is required when serializing energy log records to cache.
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#314
File: plugwise_usb/nodes/circle.py:570-573
Timestamp: 2025-08-14T12:26:12.780Z
Learning: In plugwise_usb/nodes/circle.py, the energy_log_update method's return value of True indicates successful processing and storing of energy log records in memory (via _energy_counters), regardless of whether the records were new, duplicates, or empty. The docstring "return True only when at least one recent, non-empty record was stored" refers to successful processing/storage in the energy counters system, not necessarily new cache entries.
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-08-30T10:46:42.627Z
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#329
File: plugwise_usb/nodes/circle.py:0-0
Timestamp: 2025-08-30T10:46:42.627Z
Learning: In plugwise_usb/nodes/circle.py, the save_cache() method should only be called when both cache_updated AND save_cache parameters are True. If no cache updates occurred (cache_updated = False), there's no meaningful data to persist to disk, even if the caller requested saving (save_cache = True). This prevents unnecessary disk I/O operations.

Applied to files:

  • plugwise_usb/nodes/circle.py
📚 Learning: 2025-08-14T12:26:12.780Z
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#314
File: plugwise_usb/nodes/circle.py:570-573
Timestamp: 2025-08-14T12:26:12.780Z
Learning: In plugwise_usb/nodes/circle.py, the energy_log_update method's return value of True indicates successful processing and storing of energy log records in memory (via _energy_counters), regardless of whether the records were new, duplicates, or empty. The docstring "return True only when at least one recent, non-empty record was stored" refers to successful processing/storage in the energy counters system, not necessarily new cache entries.

Applied to files:

  • plugwise_usb/nodes/circle.py
📚 Learning: 2025-08-13T17:29:32.348Z
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#314
File: plugwise_usb/nodes/circle.py:695-705
Timestamp: 2025-08-13T17:29:32.348Z
Learning: In plugwise_usb/nodes/circle.py, the method self._energy_counters.get_pulse_logs() already returns data in reverse-sorted order (newest-first), so no additional sorting is required when serializing energy log records to cache.

Applied to files:

  • plugwise_usb/nodes/circle.py
🧬 Code graph analysis (1)
plugwise_usb/nodes/circle.py (2)
plugwise_usb/nodes/node.py (1)
  • save_cache (706-720)
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 (3)
plugwise_usb/nodes/circle.py (3)

419-437: Rollover path defers disk writes correctly

Using save_cache=False on both rollover fetches centralizes persistence to a single point. This aligns with the PR’s objective to control write locations.


621-629: Import-only path and update flag look good

Processing only recent, non-empty records with import_only=True and accumulating cache_updated is consistent with centralized persistence.


710-710: Good: clarify no file I/O here

Explicitly documenting this as in-memory-only helps reasoning about where disk writes happen.

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

521-539: Inverted condition stops initial collection on data-present instead of empty/outdated

Per your docstring, slots_empty=False means at least one recent slot updated. The current check breaks when data exists; it should break when all slots are empty/outdated.

-            if result and not slots_empty:
-                # (address with outdated data in slots is stored as with None data!)
-                # Stop initial log collection when an address contains no (None) or outdated data
+            if result and slots_empty:
+                # Stop initial log collection when an address contains no (None) or only outdated data
                 _LOGGER.debug(
                     "All slots at log address %s are empty or outdated – stopping initial collection",
                     log_address,
                 )
                 break

Optionally, avoid needless disk I/O by persisting only if any address had updates:

         log_address = self._current_log_address
+        any_updates = False
         while total_addresses > 0:
             result, slots_empty = await self.energy_log_update(
                 log_address, save_cache=False
             )
-            if result and not slots_empty:
+            if result and slots_empty:
                 ...
                 break
+            any_updates |= (not slots_empty)
             log_address, _ = calc_log_address(log_address, 1, -4)
             total_addresses -= 1
 
-        if self._cache_enabled:
-            await self.save_cache()
+        if self._cache_enabled and any_updates:
+            await self.save_cache()

563-576: Return-value regression: tasks await a tuple; cancellation never triggers and save is unconditional

energy_log_update now returns (result, slots_empty). Using “result = await task” makes result a non-empty tuple, so “if not result” is always False. Also gate save_cache on actual updates.

-        tasks = [
+        tasks = [
             create_task(self.energy_log_update(address, save_cache=False))
             for address in missing_addresses
         ]
-        for idx, task in enumerate(tasks):
-            result = await task
+        any_updates = False
+        for idx, task in enumerate(tasks):
+            result, slots_empty = await task
             # When an energy log collection task returns False, stop and cancel the remaining tasks
             if not result:
                 to_cancel = tasks[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
+            any_updates |= (not slots_empty)
 
-        if self._cache_enabled:
-            await self.save_cache()
+        if self._cache_enabled and any_updates:
+            await self.save_cache()
🧹 Nitpick comments (5)
plugwise_usb/nodes/circle.py (5)

404-409: Fix typo in comment

"acutally" -> "actually".

-        # address is acutally reported by the node even when all slots at that address are empty.
+        # address is actually reported by the node even when all slots at that address are empty.

420-446: Avoid unnecessary cache writes after rollover; gate save on actual updates

Only persist when at least one of the two updates stored recent data (i.e., slots_empty is False). This aligns with your “only save when there were updates” approach.

-            # Try collecting energy-stats from _current_log_address
-            result, _ = await self.energy_log_update(
+            # Try collecting energy-stats from _current_log_address
+            result, slots_empty_cur = await self.energy_log_update(
                 self._current_log_address, save_cache=False
             )
             if not result:
                 ...
             # Retry with previous log address as Circle node pointer to self._current_log_address
             # could be rolled over while the last log is at previous address
             prev_log_address, _ = calc_log_address(self._current_log_address, 1, -4)
-            result, _ = await self.energy_log_update(prev_log_address, save_cache=False)
+            result, slots_empty_prev = await self.energy_log_update(prev_log_address, save_cache=False)
             if not result:
                 ...
-            if self._cache_enabled:
-                await self.save_cache()
+            if self._cache_enabled and (not slots_empty_cur or not slots_empty_prev):
+                await self.save_cache()

578-586: Clarify and fix typos in the new return-value docstring

Minor grammar/typo fixes; explicitly mention “empty or outdated”.

-        Return first bool as True if processing succeeded: records stored in memory, also with empty slots.
-        Return fist bool as False on transport or address errors.
-        Return second bool as False when all slots are empty otherwise as True
+        Return first bool as True if processing succeeded (records stored in memory, possibly all-empty).
+        Return first bool as False on transport or address errors.
+        Return second bool as False when all slots are empty or outdated; otherwise True.

612-633: Simplify cache_updated logic; remove sticky slot_updated state

slot_updated persists across iterations; directly set cache_updated=True when a recent slot is stored.

-        slot_updated = 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:
                 self._energy_counters.add_empty_log(response.log_address, _slot)
             elif self._check_timestamp_is_recent(
                 response.log_address, _slot, log_timestamp.replace(tzinfo=UTC)
             ):
                 self._energy_counters.add_pulse_log(
                     response.log_address,
                     _slot,
                     log_timestamp.replace(tzinfo=UTC),
                     log_pulses,
                     import_only=True,
                 )
-                slot_updated = True
-
-            cache_updated |= slot_updated
+                cache_updated = True

635-645: Tighten debug message wording

“Saving and storing” is redundant.

-                _LOGGER.debug(
-                    "Saving and storing energy cache for %s", self._mac_in_str
-                )
+                _LOGGER.debug("Saving energy cache for %s", self._mac_in_str)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ae4d939 and 70d4992.

📒 Files selected for processing (1)
  • plugwise_usb/nodes/circle.py (11 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#314
File: plugwise_usb/nodes/circle.py:570-573
Timestamp: 2025-08-14T12:26:12.780Z
Learning: In plugwise_usb/nodes/circle.py, the energy_log_update method's return value of True indicates successful processing and storing of energy log records in memory (via _energy_counters), regardless of whether the records were new, duplicates, or empty. The docstring "return True only when at least one recent, non-empty record was stored" refers to successful processing/storage in the energy counters system, not necessarily new cache entries.
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#314
File: plugwise_usb/nodes/circle.py:695-705
Timestamp: 2025-08-13T17:29:32.348Z
Learning: In plugwise_usb/nodes/circle.py, the method self._energy_counters.get_pulse_logs() already returns data in reverse-sorted order (newest-first), so no additional sorting is required when serializing energy log records to cache.
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#329
File: plugwise_usb/nodes/circle.py:0-0
Timestamp: 2025-08-30T10:46:42.627Z
Learning: In plugwise_usb/nodes/circle.py, the save_cache() method should only be called when both cache_updated AND save_cache parameters are True. If no cache updates occurred (cache_updated = False), there's no meaningful data to persist to disk, even if the caller requested saving (save_cache = True). This prevents unnecessary disk I/O operations.
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-08-14T12:26:12.780Z
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#314
File: plugwise_usb/nodes/circle.py:570-573
Timestamp: 2025-08-14T12:26:12.780Z
Learning: In plugwise_usb/nodes/circle.py, the energy_log_update method's return value of True indicates successful processing and storing of energy log records in memory (via _energy_counters), regardless of whether the records were new, duplicates, or empty. The docstring "return True only when at least one recent, non-empty record was stored" refers to successful processing/storage in the energy counters system, not necessarily new cache entries.

Applied to files:

  • plugwise_usb/nodes/circle.py
📚 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-13T17:29:32.348Z
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#314
File: plugwise_usb/nodes/circle.py:695-705
Timestamp: 2025-08-13T17:29:32.348Z
Learning: In plugwise_usb/nodes/circle.py, the method self._energy_counters.get_pulse_logs() already returns data in reverse-sorted order (newest-first), so no additional sorting is required when serializing energy log records to cache.

Applied to files:

  • plugwise_usb/nodes/circle.py
📚 Learning: 2025-08-30T10:46:42.627Z
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#329
File: plugwise_usb/nodes/circle.py:0-0
Timestamp: 2025-08-30T10:46:42.627Z
Learning: In plugwise_usb/nodes/circle.py, the save_cache() method should only be called when both cache_updated AND save_cache parameters are True. If no cache updates occurred (cache_updated = False), there's no meaningful data to persist to disk, even if the caller requested saving (save_cache = True). This prevents unnecessary disk I/O operations.

Applied to files:

  • plugwise_usb/nodes/circle.py
🧬 Code graph analysis (1)
plugwise_usb/nodes/circle.py (4)
plugwise_usb/nodes/node.py (1)
  • save_cache (706-720)
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/nodes/helpers/counter.py (5)
  • timestamp (179-187)
  • add_empty_log (70-72)
  • add_pulse_log (74-89)
  • update (147-176)
  • update (286-328)
🔇 Additional comments (3)
plugwise_usb/nodes/circle.py (3)

458-461: LGTM: single missing address path persists via energy_log_update

Calling with save_cache=True delegates persistence correctly inside energy_log_update.


715-733: LGTM: in-memory-only serialization helper

Good separation of concerns; no file I/O here and relies on get_pulse_logs() ordering.


1156-1156: LGTM: message clarity nit fixed

“cache for” wording looks good.

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)

582-645: Fix docstring: second bool name/semantics inverted

Code returns slots_empty=True when all slots are empty/outdated and False when at least one recent record was stored. The docstring currently states the opposite. Correct it to prevent misuse at call sites.

Apply:

-    ) -> tuple[bool, bool]:
-        """Request energy logs from node and store them.
-
-        Return first bool as True if processing succeeded (records stored in memory, possibly all-empty).
-        Return first bool as False on transport or address errors.
-        Return second bool as False when all slots are empty or outdated; otherwise True.
-        """
+    ) -> tuple[bool, bool]:
+        """Request energy logs from node and store them.
+
+        First bool: True when processing succeeded (records stored in memory, possibly all-empty);
+        False only on transport or address errors.
+        Second bool: slots_empty — True when all four slots at the address are empty or outdated;
+        False when at least one recent, non-empty record was stored.
+        """

Nit: you can also normalize the timestamp once to avoid repeated replace calls:

-            elif self._check_timestamp_is_recent(
-                response.log_address, _slot, log_timestamp.replace(tzinfo=UTC)
-            ):
+            elif self._check_timestamp_is_recent(
+                response.log_address, _slot, log_timestamp.replace(tzinfo=UTC)
+            ):
                 self._energy_counters.add_pulse_log(
                     response.log_address,
                     _slot,
-                    log_timestamp.replace(tzinfo=UTC),
+                    log_timestamp.replace(tzinfo=UTC),
                     log_pulses,
                     import_only=True,
                 )
🧹 Nitpick comments (2)
plugwise_usb/nodes/circle.py (2)

521-541: Initial collection: consider aborting on transport error

Current loop only breaks on “all slots empty/outdated”. If request fails (result is False), continuing likely won’t help and can spam the stick. Suggest breaking on error too.

Apply:

-            if result and slots_empty:
+            if result and slots_empty:
                 # Stop initial log collection when an address contains no (None) or outdated data
                 # Outdated data can indicate a EnergyLog address rollover: from address 6014 to 0
                 _LOGGER.debug(
                     "All slots at log address %s are empty or outdated – stopping initial collection",
                     log_address,
                 )
                 break
+            if not result:
+                _LOGGER.debug(
+                    "Initial collection: request failed at log address %s – stopping",
+                    log_address,
+                )
+                break

564-580: Parallel fetches: optionally cap concurrency

Launching a task per missing address can create bursts. If devices report many gaps, consider limiting concurrency (e.g., 4–8) with a semaphore or batched gathers to protect the link and reduce retries.

I can provide a minimal semaphore-based refactor if you want it in this PR.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 70d4992 and 73d5236.

📒 Files selected for processing (1)
  • plugwise_usb/nodes/circle.py (13 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#314
File: plugwise_usb/nodes/circle.py:570-573
Timestamp: 2025-08-14T12:26:12.780Z
Learning: In plugwise_usb/nodes/circle.py, the energy_log_update method's return value of True indicates successful processing and storing of energy log records in memory (via _energy_counters), regardless of whether the records were new, duplicates, or empty. The docstring "return True only when at least one recent, non-empty record was stored" refers to successful processing/storage in the energy counters system, not necessarily new cache entries.
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#314
File: plugwise_usb/nodes/circle.py:695-705
Timestamp: 2025-08-13T17:29:32.348Z
Learning: In plugwise_usb/nodes/circle.py, the method self._energy_counters.get_pulse_logs() already returns data in reverse-sorted order (newest-first), so no additional sorting is required when serializing energy log records to cache.
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.
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#329
File: plugwise_usb/nodes/circle.py:0-0
Timestamp: 2025-08-30T10:46:42.627Z
Learning: In plugwise_usb/nodes/circle.py, the save_cache() method should only be called when both cache_updated AND save_cache parameters are True. If no cache updates occurred (cache_updated = False), there's no meaningful data to persist to disk, even if the caller requested saving (save_cache = True). This prevents unnecessary disk I/O operations.
📚 Learning: 2025-08-14T12:26:12.780Z
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#314
File: plugwise_usb/nodes/circle.py:570-573
Timestamp: 2025-08-14T12:26:12.780Z
Learning: In plugwise_usb/nodes/circle.py, the energy_log_update method's return value of True indicates successful processing and storing of energy log records in memory (via _energy_counters), regardless of whether the records were new, duplicates, or empty. The docstring "return True only when at least one recent, non-empty record was stored" refers to successful processing/storage in the energy counters system, not necessarily new cache entries.

Applied to files:

  • plugwise_usb/nodes/circle.py
📚 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-13T17:29:32.348Z
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#314
File: plugwise_usb/nodes/circle.py:695-705
Timestamp: 2025-08-13T17:29:32.348Z
Learning: In plugwise_usb/nodes/circle.py, the method self._energy_counters.get_pulse_logs() already returns data in reverse-sorted order (newest-first), so no additional sorting is required when serializing energy log records to cache.

Applied to files:

  • plugwise_usb/nodes/circle.py
📚 Learning: 2025-08-30T10:46:42.627Z
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#329
File: plugwise_usb/nodes/circle.py:0-0
Timestamp: 2025-08-30T10:46:42.627Z
Learning: In plugwise_usb/nodes/circle.py, the save_cache() method should only be called when both cache_updated AND save_cache parameters are True. If no cache updates occurred (cache_updated = False), there's no meaningful data to persist to disk, even if the caller requested saving (save_cache = True). This prevents unnecessary disk I/O operations.

Applied to files:

  • plugwise_usb/nodes/circle.py
🧬 Code graph analysis (1)
plugwise_usb/nodes/circle.py (5)
plugwise_usb/nodes/node.py (1)
  • save_cache (706-720)
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 (1)
  • log_address (770-772)
plugwise_usb/nodes/helpers/counter.py (4)
  • add_empty_log (70-72)
  • add_pulse_log (74-89)
  • update (147-176)
  • update (286-328)
🔇 Additional comments (7)
plugwise_usb/nodes/circle.py (7)

404-409: Startup: one-shot “last address” probe looks good

Fetching once with save_cache=False is aligned with the new tuple return and avoids needless I/O. No further action.


420-446: Rollover handling is correct and I/O-minimal

  • Probing both current and previous addresses prevents missing the tail record at rollover.
  • Saving only when at least one address wasn’t empty matches the cache policy. Since energy_log_update() flushes the in-memory cache string on updates, an extra flush here isn’t needed.

459-461: Single-missing-address fast path

Calling energy_log_update(..., save_cache=True) directly here is the right tradeoff to persist immediately.


714-733: Cache serialization: newest-first and in-memory only looks right

  • Uses get_pulse_logs() ordering (newest-first), matching prior guidance.
  • No disk I/O here, as intended.

1078-1081: Unload: confirm persistence path

You only refresh the in-memory cache string on unload; no save_cache() is called here. If PlugwiseBaseNode.unload() does not persist the node cache afterwards, the last in-memory snapshot could be lost. Please confirm super().unload() writes the cache; if not, consider saving here.

Would you like me to inspect PlugwiseBaseNode.unload() and wire a safe, non-redundant final save if needed?


1155-1158: Minor log string fix

“cache for” wording corrected; looks good.


582-589: Call-site audit complete: energy_log_update unpacked correctly
All invocations unpack both returned values; no single-value assignments detected.

@bouwew bouwew marked this pull request as ready for review August 31, 2025 09:47
@bouwew bouwew requested a review from a team as a code owner August 31, 2025 09:47
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 (1)
plugwise_usb/nodes/circle.py (1)

624-637: Consider explicit logging for empty slots

While the logic correctly adds empty logs for slots with None timestamp or pulses, it might be helpful to add debug logging when empty slots are detected for troubleshooting.

             if log_timestamp is None or log_pulses is None:
                 self._energy_counters.add_empty_log(address, _slot)
+                _LOGGER.debug(
+                    "Empty slot detected at address=%s, slot=%s", address, _slot
+                )
             else:
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 437df11 and 16517a3.

📒 Files selected for processing (3)
  • CHANGELOG.md (1 hunks)
  • plugwise_usb/nodes/circle.py (13 hunks)
  • pyproject.toml (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • pyproject.toml
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#314
File: plugwise_usb/nodes/circle.py:570-573
Timestamp: 2025-08-14T12:26:12.780Z
Learning: In plugwise_usb/nodes/circle.py, the energy_log_update method's return value of True indicates successful processing and storing of energy log records in memory (via _energy_counters), regardless of whether the records were new, duplicates, or empty. The docstring "return True only when at least one recent, non-empty record was stored" refers to successful processing/storage in the energy counters system, not necessarily new cache entries.
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#329
File: plugwise_usb/nodes/circle.py:0-0
Timestamp: 2025-08-30T10:46:42.627Z
Learning: In plugwise_usb/nodes/circle.py, the save_cache() method should only be called when both cache_updated AND save_cache parameters are True. If no cache updates occurred (cache_updated = False), there's no meaningful data to persist to disk, even if the caller requested saving (save_cache = True). This prevents unnecessary disk I/O operations.
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#314
File: plugwise_usb/nodes/circle.py:695-705
Timestamp: 2025-08-13T17:29:32.348Z
Learning: In plugwise_usb/nodes/circle.py, the method self._energy_counters.get_pulse_logs() already returns data in reverse-sorted order (newest-first), so no additional sorting is required when serializing energy log records to cache.
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-08-14T12:26:12.780Z
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#314
File: plugwise_usb/nodes/circle.py:570-573
Timestamp: 2025-08-14T12:26:12.780Z
Learning: In plugwise_usb/nodes/circle.py, the energy_log_update method's return value of True indicates successful processing and storing of energy log records in memory (via _energy_counters), regardless of whether the records were new, duplicates, or empty. The docstring "return True only when at least one recent, non-empty record was stored" refers to successful processing/storage in the energy counters system, not necessarily new cache entries.

Applied to files:

  • plugwise_usb/nodes/circle.py
📚 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-13T17:29:32.348Z
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#314
File: plugwise_usb/nodes/circle.py:695-705
Timestamp: 2025-08-13T17:29:32.348Z
Learning: In plugwise_usb/nodes/circle.py, the method self._energy_counters.get_pulse_logs() already returns data in reverse-sorted order (newest-first), so no additional sorting is required when serializing energy log records to cache.

Applied to files:

  • plugwise_usb/nodes/circle.py
📚 Learning: 2025-08-30T10:46:42.627Z
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#329
File: plugwise_usb/nodes/circle.py:0-0
Timestamp: 2025-08-30T10:46:42.627Z
Learning: In plugwise_usb/nodes/circle.py, the save_cache() method should only be called when both cache_updated AND save_cache parameters are True. If no cache updates occurred (cache_updated = False), there's no meaningful data to persist to disk, even if the caller requested saving (save_cache = True). This prevents unnecessary disk I/O operations.

Applied to files:

  • plugwise_usb/nodes/circle.py
🧬 Code graph analysis (1)
plugwise_usb/nodes/circle.py (3)
plugwise_usb/nodes/node.py (1)
  • save_cache (706-720)
plugwise_usb/nodes/helpers/pulses.py (2)
  • calc_log_address (20-36)
  • add_empty_log (389-421)
plugwise_usb/nodes/helpers/counter.py (4)
  • add_empty_log (70-72)
  • add_pulse_log (74-89)
  • 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: Run pytest using Python 3.13
🔇 Additional comments (10)
plugwise_usb/nodes/circle.py (10)

404-409: LGTM! Energy log update now returns tuple with empty-slot indication

The change to capture both the result and slots_empty state aligns well with the new return semantics of energy_log_update.


446-447: Cache save condition correctly implements the requirement

Good implementation! The cache is saved only when there's actual non-empty data from either the current or previous log address, which prevents unnecessary disk I/O for all-empty records.

Based on the retrieved learnings, this correctly follows the pattern where save_cache() should only be called when there's meaningful data to persist.


461-463: Minor: Inconsistent destructuring pattern

For consistency with other calls, consider using underscore for the unused slots_empty value.

-                result, _ = await self.energy_log_update(
+                result, _ = await self.energy_log_update(

Actually, the code is already using underscore correctly. No change needed.


523-542: Well-structured initial energy log collection

The logic correctly:

  1. Stops collection when all slots are empty (preventing unnecessary fetches)
  2. Tracks whether any non-empty data was found
  3. Only saves to cache when there's actual data

This aligns perfectly with the PR objectives of improving cache efficiency.


566-581: Excellent handling of concurrent task results

The implementation correctly:

  1. Unpacks the tuple results from each task
  2. Cancels remaining tasks on failure (good for efficiency)
  3. Properly drains cancellations to avoid "Task exception was never retrieved" warnings
  4. Tracks and saves cache only when there's non-empty data

583-648: Method signature and logic update aligns with PR objectives

The updated energy_log_update method now:

  1. Returns a tuple (result, slots_empty) providing clearer semantics
  2. Properly tracks empty vs non-empty slots
  3. Only updates cache when there's actual data to save

The docstring clearly explains the return value semantics, which helps API consumers understand the difference between transport/address errors and successful processing of empty data.


718-736: Documentation update correctly reflects new behavior

The updated comment clearly indicates this method now only updates the in-memory cache string without performing file I/O, which aligns with the centralized disk write approach via save_cache().


1159-1159: Typo fix in log message

Good catch on fixing the typo from "cachefor" to "cache for".


1082-1084: Unload behavior correctly uses save_cache()

The change from calling _energy_log_records_save_to_cache() to just ensuring cache is enabled before unload aligns with the centralized persistence model. The parent class's unload() method will handle the actual save_cache() call.


420-444: energy_update correctly returns None for empty log fetches and all callers handle None as expected

@bouwew bouwew requested a review from dirixmjm August 31, 2025 10:08
@sonarqubecloud
Copy link

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.

Great improvements, let's keep those caches manageable.

@bouwew bouwew merged commit a63392c into main Aug 31, 2025
16 of 17 checks passed
@bouwew bouwew deleted the energy_log_cache branch August 31, 2025 15:34
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