Skip to content

Add configurable wait-for-fresh-push behavior for event-driven powermeters#330

Merged
tomquist merged 4 commits intodevelopfrom
claude/fix-powermeter-issues-Nq13v
Apr 19, 2026
Merged

Add configurable wait-for-fresh-push behavior for event-driven powermeters#330
tomquist merged 4 commits intodevelopfrom
claude/fix-powermeter-issues-Nq13v

Conversation

@tomquist
Copy link
Copy Markdown
Owner

@tomquist tomquist commented Apr 19, 2026

Summary

This PR introduces a configurable WAIT_FOR_NEXT_MESSAGE setting that controls whether astrameter waits (up to 2 seconds) for fresh data from event-driven powermeters before responding to battery requests. This prevents stale-data timeouts from breaking CT002 responses when upstream meters update slower than the freshness window.

Key Changes

  • New read_ct_powermeter() function in main.py: Extracted and refactored the powermeter reading logic from update_readings() into a testable, reusable function that:

    • Matches powermeters to client addresses via ClientFilter
    • Optionally awaits fresh push messages with a 2-second timeout
    • Gracefully handles TimeoutError by serving the last-known cached value
    • Pads single/dual-phase readings to three phases with zeros
  • Configuration support: Added WAIT_FOR_NEXT_MESSAGE setting with:

  • Powermeter tuple expansion: Updated all powermeter tuple signatures from (Powermeter, ClientFilter) to (Powermeter, ClientFilter, bool) to carry the wait flag throughout the codebase

  • Simplified MQTT wait logic in mqtt.py: Removed the complex multi-phase consistency loop from wait_for_next_message() — now it returns on any fresh message event (consistency checks belong in wait_for_message() / get_powermeter_watts())

  • Comprehensive test coverage: Added 8 new tests covering:

    • Client filter matching behavior
    • Phase padding logic
    • Wait enable/disable scenarios
    • Timeout handling with cached fallback
    • Configuration parsing (global, per-section, overrides)

Notable Implementation Details

  • The 2-second timeout is hardcoded in read_ct_powermeter() and is not user-configurable (intentional design choice for simplicity)
  • TimeoutError from slow push meters is silently swallowed with debug logging to prevent battery control loops from seeing stale-meter errors
  • The refactored update_readings() callback now simply delegates to read_ct_powermeter(), improving code clarity and testability

https://claude.ai/code/session_01H5W8HEXpcYorYrZCxJcBvS

Summary by CodeRabbit

  • New Features

    • Added WAIT_FOR_NEXT_MESSAGE configuration option to control powermeter update waiting behavior; configurable both globally and per-powermeter with a 2-second timeout (defaults to true).
  • Improvements

    • Powermeter read timeouts now gracefully fall back to cached values instead of propagating errors, reducing latency when devices update slowly.
  • Documentation

    • Configuration guide updated with new WAIT_FOR_NEXT_MESSAGE setting and examples.

claude added 2 commits April 19, 2026 19:27
PR #322 wired wait_for_next_message into update_readings so push-based
powermeters serve fresh data on every CT002 request. Two regressions
followed:

1. Slow event-driven meters (e.g. Home Assistant wrapping a P1 that
   ticks every 10s, or any source with high THROTTLE_INTERVAL) raise
   TimeoutError on every request, which surfaces as "CT002 before_send
   failed" warnings and skips the cached value that would have been
   perfectly usable (issue #327).

2. MQTT multi-topic kept clearing the event whenever the waker happened
   to be a phase that had already published, so any topic still at None
   would force the full 5s timeout even though a fresh message had
   arrived.

Treat wait_for_next_message as best-effort freshness:

- update_readings caps the wait at 2s and swallows TimeoutError (cached
  value is served, which matches behaviour pre-#322).
- Add WAIT_FOR_NEXT_MESSAGE (global [GENERAL] + per-section) so users
  can opt out of the wait entirely for sources that always update
  slower than the cap. Defaults to true.
- MQTT wait_for_next_message returns on any next message instead of
  re-arming the event when not all topics have ever published.

Refactor the closure inside run_device into a module-level
read_ct_powermeter helper so the swallow-timeout behaviour is
unit-testable.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 19, 2026

Warning

Rate limit exceeded

@tomquist has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 48 minutes and 27 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 48 minutes and 27 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e4c28682-9076-4711-97dd-3d636cb6c3b2

📥 Commits

Reviewing files that changed from the base of the PR and between 6999fcf and a89afd4.

📒 Files selected for processing (5)
  • config.ini.example
  • ha_addon/config.yaml
  • ha_addon/run.sh
  • ha_addon/translations/en.yaml
  • src/astrameter/shelly/shelly.py

Walkthrough

The PR introduces a new global configuration option WAIT_FOR_NEXT_MESSAGE (default true) enabling conditional waiting (up to ~2 seconds) for event-driven powermeter updates before responding with CT data. Per-powermeter overrides allow disabling the wait to optimize for latency-sensitive scenarios.

Changes

Cohort / File(s) Summary
Documentation & Configuration
README.md, config.ini.example, src/astrameter/web_config.py
Added new WAIT_FOR_NEXT_MESSAGE configuration option (global and per-powermeter override) with descriptions. Extended web config metadata to expose the new field in JSON schema.
Config Loading
src/astrameter/config/config_loader.py, src/astrameter/config/config_loader_test.py
Modified read_all_powermeter_configs() to return 3-tuples (Powermeter, ClientFilter, bool) instead of 2-tuples, parsing global [GENERAL].WAIT_FOR_NEXT_MESSAGE and per-section overrides with cascading defaults. Added test coverage for flag behavior.
Main Application Logic
src/astrameter/main.py, src/astrameter/main_test.py
Added new read_ct_powermeter() helper that selectively waits for fresh powermeter data based on config flag (2s timeout), suppresses TimeoutError, and pads readings to three phases. Updated run_device() signature and all powermeter tuple handling to accommodate 3-tuple structure.
Powermeter MQTT Implementation
src/astrameter/powermeter/mqtt.py, src/astrameter/powermeter/mqtt_test.py
Simplified wait_for_next_message() to perform a single timed wait on the event instead of repeatedly checking for fully populated values. Updated test expectations to reflect new behavior.
Shelly Device Integration
src/astrameter/shelly/shelly.py, src/astrameter/shelly/shelly_udp_test.py
Updated Shelly.__init__() to unpack 3-tuples from powermeter configuration; no functional logic changes to request handling.

Sequence Diagram(s)

sequenceDiagram
    participant Device as Device/Client
    participant Main as Main App
    participant Config as Config Loader
    participant Powermeter as Powermeter (MQTT)
    
    Device->>Main: CT reading request
    Main->>Config: read powermeter config
    Config-->>Main: (powermeter, filter, wait_flag)
    
    Main->>Powermeter: select by ClientFilter
    alt wait_flag == true
        Main->>Powermeter: wait_for_next_message(timeout=2s)
        Powermeter->>Powermeter: wait for MQTT event
        alt fresh data available
            Powermeter-->>Main: event received
        else timeout
            Powermeter-->>Main: TimeoutError (suppressed)
        end
    end
    
    Main->>Powermeter: get_powermeter_watts()
    Powermeter-->>Main: watt values
    Main->>Main: pad to 3 phases
    Main-->>Device: CT readings
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding a configurable option to control whether the system waits for fresh data from event-driven powermeters, which is the central feature introduced across documentation, configuration, and implementation.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/fix-powermeter-issues-Nq13v

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@tomquist tomquist marked this pull request as ready for review April 19, 2026 19:46
Copy link
Copy Markdown

@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.

Caution

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

⚠️ Outside diff range comments (2)
config.ini.example (1)

206-217: ⚠️ Potential issue | 🟡 Minor

Remove the duplicate Home Assistant throttle example.

This section now shows both #THROTTLE_INTERVAL = 1 and #THROTTLE_INTERVAL = 2, which makes the recommended Home Assistant value ambiguous.

📝 Proposed cleanup
 ## Per-powermeter throttling override (optional)
-#THROTTLE_INTERVAL = 1
+## HomeAssistant typically needs 2-3 seconds due to network latency
+#THROTTLE_INTERVAL = 2
 ## Skip the up-to-2s wait for a fresh push from this powermeter and always
 ## serve the last-known value. Useful when the underlying sensor (e.g. P1
 ## smart meter) updates slower than 2s, so the wait would always time out
 ## and the cached value would be served anyway. Defaults to the global
 ## [GENERAL] WAIT_FOR_NEXT_MESSAGE setting (true).
 `#WAIT_FOR_NEXT_MESSAGE` = false
-
-## Per-powermeter throttling override (optional)
-## HomeAssistant typically needs 2-3 seconds due to network latency
-#THROTTLE_INTERVAL = 2
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config.ini.example` around lines 206 - 217, Remove the duplicate Home
Assistant throttle example by deleting the first per-powermeter block that shows
"#THROTTLE_INTERVAL = 1" (the block immediately above the WAIT_FOR_NEXT_MESSAGE
example) so only the single Home Assistant example with "#THROTTLE_INTERVAL = 2"
remains; ensure the remaining comment still explains this is a per-powermeter
throttling override and optional, and that it references the global [GENERAL]
WAIT_FOR_NEXT_MESSAGE behavior if present.
src/astrameter/shelly/shelly.py (1)

204-212: ⚠️ Potential issue | 🟠 Major

Honor WAIT_FOR_NEXT_MESSAGE in the Shelly request path.

Line 204 discards the new flag, so DEVICE_TYPE=shelly* still serves immediately while the CT path applies the 2s best-effort wait. Capture the flag and mirror the CT behavior before Line 212.

🐛 Proposed fix
-                for pm, client_filter, _ in self._powermeters:
+                wait_for_next = False
+                for pm, client_filter, wait_flag in self._powermeters:
                     if client_filter.matches(addr[0]):
                         powermeter = pm
+                        wait_for_next = wait_flag
                         break
                 if powermeter is None:
                     logger.warning(f"No powermeter found for client {addr[0]}")
                     return
 
+                if wait_for_next:
+                    try:
+                        await powermeter.wait_for_next_message(timeout=2)
+                    except TimeoutError:
+                        logger.debug(
+                            "Powermeter %s produced no fresh message within 2s; "
+                            "serving last known value",
+                            type(powermeter).__name__,
+                        )
+
                 powers = await powermeter.get_powermeter_watts()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/astrameter/shelly/shelly.py` around lines 204 - 212, The code currently
ignores the WAIT_FOR_NEXT_MESSAGE flag when selecting a powermeter and
immediately calls powermeter.get_powermeter_watts(); capture the flag from the
incoming request (same place CT reads it), store it in a local variable (e.g.,
wait_for_next_message), and if true mirror the CT path by awaiting the same
best-effort wait (2s or the helper used by CT) before calling
powermeter.get_powermeter_watts(); use the existing symbols powermeter, addr,
and self._powermeters to locate where to insert the check and await so behavior
matches the CT implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@config.ini.example`:
- Around line 206-217: Remove the duplicate Home Assistant throttle example by
deleting the first per-powermeter block that shows "#THROTTLE_INTERVAL = 1" (the
block immediately above the WAIT_FOR_NEXT_MESSAGE example) so only the single
Home Assistant example with "#THROTTLE_INTERVAL = 2" remains; ensure the
remaining comment still explains this is a per-powermeter throttling override
and optional, and that it references the global [GENERAL] WAIT_FOR_NEXT_MESSAGE
behavior if present.

In `@src/astrameter/shelly/shelly.py`:
- Around line 204-212: The code currently ignores the WAIT_FOR_NEXT_MESSAGE flag
when selecting a powermeter and immediately calls
powermeter.get_powermeter_watts(); capture the flag from the incoming request
(same place CT reads it), store it in a local variable (e.g.,
wait_for_next_message), and if true mirror the CT path by awaiting the same
best-effort wait (2s or the helper used by CT) before calling
powermeter.get_powermeter_watts(); use the existing symbols powermeter, addr,
and self._powermeters to locate where to insert the check and await so behavior
matches the CT implementation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a35c6e64-d630-480a-9f7a-14c3ea2b21ed

📥 Commits

Reviewing files that changed from the base of the PR and between 30cc695 and 6999fcf.

📒 Files selected for processing (11)
  • README.md
  • config.ini.example
  • src/astrameter/config/config_loader.py
  • src/astrameter/config/config_loader_test.py
  • src/astrameter/main.py
  • src/astrameter/main_test.py
  • src/astrameter/powermeter/mqtt.py
  • src/astrameter/powermeter/mqtt_test.py
  • src/astrameter/shelly/shelly.py
  • src/astrameter/shelly/shelly_udp_test.py
  • src/astrameter/web_config.py

claude added 2 commits April 19, 2026 19:52
Adds a "Wait For Fresh Home Assistant Reading" toggle (default true) to
the HA app so users with slow-updating source sensors (e.g. P1 smart
meters) can opt out of the up-to-2s freshness wait and serve the
last-known value immediately.
The Shelly UDP handler was ignoring the per-powermeter wait flag, so a
Shelly-emulated device would never wait for a fresh push from an
event-driven powermeter (behaviour matched pre-#322). Mirror the CT path:
read the flag when matching addr to powermeter, then best-effort await
``wait_for_next_message(timeout=2)`` with ``TimeoutError`` swallowed.

Also consolidate the duplicate ``[HOMEASSISTANT]`` per-powermeter throttle
example in config.ini.example: keep the more informative
``THROTTLE_INTERVAL = 2`` block (with the "2-3 seconds due to network
latency" note) and drop the stray ``= 1`` duplicate that predated the
``WAIT_FOR_NEXT_MESSAGE`` example.
@tomquist tomquist merged commit dc20442 into develop Apr 19, 2026
13 checks passed
@tomquist tomquist deleted the claude/fix-powermeter-issues-Nq13v branch April 19, 2026 20:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants