Skip to content

CT002: relay mode bucket aggregation & adaptive consumer eviction#465

Merged
tomquist merged 4 commits into
developfrom
claude/fervent-heisenberg-0aet1p
Jun 12, 2026
Merged

CT002: relay mode bucket aggregation & adaptive consumer eviction#465
tomquist merged 4 commits into
developfrom
claude/fervent-heisenberg-0aet1p

Conversation

@tomquist

@tomquist tomquist commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes relay-mode cross-talk bucket aggregation to match real CT hardware behavior and implements adaptive consumer eviction based on observed poll cadence (issues #457, #460, #462).

Key Changes

Relay Mode Bucket Aggregation (Issue #457)

  • Relay mode now forwards each battery's reported power in per-phase buckets, not reported+grid (the expected next net)
  • Active control preserves the existing behavior: A/B/C buckets aggregate net instructed power to shield against PV passthrough artifacts (issue Venus E stops the charging process when Venus D is feeding excess power into the grid. #376)
  • x/ABC consumers are never actively instructed, so their buckets always carry reported power in both modes

Bucket Routing for Inspection & Combined Mode (Issue #460)

  • Inspection reporters (phase "0") now route to the x_* bucket instead of inflating phase A
  • Combined-mode reporters (phase "D") now route to the ABC_* bucket and ABC_chrg_nb count instead of phase A
  • Added _bucket_for_phase() helper to centralize phase→bucket mapping in both Python and C++

Adaptive Consumer Eviction (Issue #462)

  • Default behavior (when consumer_ttl=None): consumers expire after missing ~2 of their own observed poll cycles, matching real CT hardware
  • Fallback (30s) applies while cadence is unknown (only one poll seen)
  • Floor (5s) protects fast pollers from transient network jitter
  • Fixed TTL (explicit consumer_ttl value) preserves the old behavior for networks with long polling gaps
  • Stale consumers drop from aggregation immediately (per response cycle) without waiting for cleanup task

Implementation Details

  • Phase normalization now stores canonical "0" for unassigned/inspection states instead of forcing "A"
  • _collect_reports_by_phase() checks consumer expiration before aggregation, mirroring real CT behavior
  • Both Python and C++ implementations updated in parallel (parity requirement)
  • Test coverage added for all three issues across unit and e2e scenarios

Files Modified

  • src/astrameter/ct002/ct002.py — relay aggregation logic, adaptive TTL, bucket routing
  • esphome/components/ct002/ct002.cpp — C++ mirror of above
  • esphome/components/ct002/ct002.h — bucket enum, TTL constants, PhaseReports struct
  • tests/test_ct002_protocol.py — unit tests for new behavior
  • tests/components/ct002/test_shared_e2e.py — e2e tests for parity
  • Config/docs updates for new defaults

https://claude.ai/code/session_01L1fNX5zVsyvfUtVw9yK2Hi

Summary by CodeRabbit

  • New Features

    • Adaptive default battery eviction: silent batteries drop after missing ~2 of their own poll cycles; CONSUMER_TTL can enforce a fixed window.
    • Runtime controls to toggle active-control and adjust consumer TTL.
  • Bug Fixes

    • Relay mode forwards each battery’s reported power into cross-talk buckets.
    • Inspection reporters map to an x bucket; combined-mode reporters map to an ABC bucket; counts and aggregates corrected.
  • Documentation

    • Updated docs, README, examples and UI help to clarify TTL and bucketing behavior.
  • Tests

    • New/expanded tests for bucketing, relay semantics, and adaptive eviction.

…ets, adaptive eviction

Three emulator fixes (mirrored in the ESPHome ct002 component per the
parity rule), all in the cross-talk aggregation / response path:

- #457: relay mode (ACTIVE_CONTROL=False) now aggregates each battery's
  *reported* power into the per-phase *_chrg/_dchrg buckets instead of
  reported+grid (the battery's next expected net). Active control keeps
  the net-instructed-power semantics from #376.

- #460: add the x (unassigned/inspection) and ABC (combined, phase "D")
  buckets. Inspection ('0') reporters now populate x_chrg/x_dchrg and no
  longer inflate phase A's count (which skewed the relay share split
  during any peer's inspection window); phase-D reporters populate
  ABC_chrg/ABC_dchrg and ABC_chrg_nb. The consumer's phase is stored as
  reported (normalized to "0" for inspection markers) instead of being
  forced to "A".

- #462: consumer eviction now defaults to an adaptive TTL (~2 missed
  cycles of the battery's observed poll cadence, floored at 5s, 30s
  while the cadence is unknown), like the real CT, instead of a fixed
  120s. Aggregation also skips expired consumers per response so relay
  counts shrink at poll granularity. An explicit CONSUMER_TTL (Python) /
  consumer_ttl (ESPHome) restores a fixed window. Eviction and report
  timestamps now use the injected clock, unifying the timebase with the
  dedup logic and making eviction deterministically testable.

Tests: new relay/x/ABC/eviction unit tests, shared Python-vs-ESPHome e2e
scenarios (with new active_control/consumer_ttl test-hook cfg keys), and
"0"/"D" phases in the wire-identical fuzzer.

https://claude.ai/code/session_01L1fNX5zVsyvfUtVw9yK2Hi
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 254c79d3-3322-4fd3-9e0f-4785de65a6d7

📥 Commits

Reviewing files that changed from the base of the PR and between 9c2a28f and 538791a.

📒 Files selected for processing (1)
  • CHANGELOG.md
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md

Walkthrough

Preserve reported phases, add explicit x/A/B/C/ABC aggregation buckets, forward per-phase reported power in relay mode, and change consumer eviction default to adaptive (~2 missed polls) with optional fixed CONSUMER_TTL override.

Changes

CT002 relay aggregation with adaptive TTL and phase bucketing

Layer / File(s) Summary
Phase bucket model and adaptive TTL constants
esphome/components/ct002/ct002.h, src/astrameter/ct002/ct002.py
Adds PhaseBucket enum (x/A/B/C/ABC) and adaptive TTL constants used to compute per-consumer eviction windows.
Consumer expiration logic and adaptive TTL computation
esphome/components/ct002/ct002.h, esphome/components/ct002/ct002.cpp, src/astrameter/ct002/ct002.py
Implements consumer_ttl_for_/consumer_expired_ (C++) and _consumer_ttl_seconds/_consumer_expired (Python) and uses them for cleanup and dedupe purges.
Phase parsing and bucket mapping
esphome/components/ct002/ct002.cpp, src/astrameter/ct002/ct002.py
Normalizes incoming phases to A/B/C/D or canonical 0, preserves reported_phase, and maps D→ABC and non A/B/C/D→x.
Relay-mode aggregation and bucket collection
esphome/components/ct002/ct002.cpp, src/astrameter/ct002/ct002.py
Refactors collect_reports_by_phase to skip expired consumers, map phases to buckets, and select aggregation source: instructed power for active-control A/B/C, reported power for relay/x/ABC.
Response field construction with bucket indices
esphome/components/ct002/ct002.cpp, src/astrameter/ct002/ct002.py
Updates response building to emit explicit x and ABC bucket fields and counts and to adjust per-consumer count semantics for active vs relay modes.
Schema changes and configuration defaults
esphome/components/ct002/ct002.h, esphome/components/ct002/__init__.py, src/astrameter/ct002/ct002.py, src/astrameter/main.py
Makes consumer_ttl unset-by-default (adaptive); ESPHome schema removes default and emits set_consumer_ttl_seconds() only when configured.
Documentation and configuration examples
CHANGELOG.md, README.md, config.ini.example, esphome.example.yaml, docs/ct002-ct003-protocol.md, web/ts/schema.ts
Documents adaptive default (~2 missed polls), bucket routing (x / ABC / A/B/C), relay-mode reported-power semantics, and fixed-window override via CONSUMER_TTL.
Test backend control methods and setup
tests/components/ct002/test_shared_e2e.py
Adds set_active_control() and set_consumer_ttl() helpers, aligns CT002 init, and expands fuzzer phases to include 0 and D.
Protocol and behavior test suites
tests/test_ct002_protocol.py, tests/components/ct002/test_shared_e2e.py
Adds tests for relay buckets forwarding reported power, inspection/x and combined/ABC routing, active-control edge cases, deterministic adaptive TTL behaviors, fixed TTL override, and immediate stale aggregation shrink.

Sequence Diagram(s)

sequenceDiagram
    participant Battery as Battery (Consumer)
    participant Emulator as CT002 Emulator
    participant Aggregator as Collector
    participant Response as Response Builder
    Battery->>Emulator: report(power, phase)
    Emulator->>Emulator: normalize phase -> A/B/C/D or 0 (x)
    Emulator->>Emulator: compute per-consumer TTL (adaptive or configured)
    Emulator->>Aggregator: update consumer store (skip expired)
    Aggregator->>Aggregator: map phase -> bucket (x/A/B/C/ABC)
    Aggregator->>Aggregator: select value (instructed vs reported)
    Aggregator->>Response: aggregate by bucket
    Response->>Response: emit x/A/B/C/ABC fields
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.66% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the two main changes: relay mode bucket aggregation and adaptive consumer eviction, directly matching the PR's core objectives.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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/fervent-heisenberg-0aet1p

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 June 12, 2026 05:32
@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-06-12 18:44 UTC

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/astrameter/ct002/ct002.py`:
- Around line 362-368: The reporting path still coerces non-ABC phases to "A"
even though you now store canonical "0"/"D" in Consumer.phase; update
reporting_consumer_rows() (and any helper that builds the reporting row type) to
pass through the normalized_phase value (preserve "0" and "D") instead of
remapping to "A", and ensure format_cd4_slave_csv() and the row serialization
accept and emit these canonical phases unchanged so the Python reporting rows
match the ESPHome mirror; inspect any row type/serializer used by
reporting_consumer_rows(), reporting row constructors, and
format_cd4_slave_csv() and remove the forced non-ABC -> "A" conversion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a8569fce-a9bb-495b-a91f-78022b90181f

📥 Commits

Reviewing files that changed from the base of the PR and between d60c840 and a2e91c9.

📒 Files selected for processing (14)
  • CHANGELOG.md
  • README.md
  • config.ini.example
  • docs/ct002-ct003-protocol.md
  • esphome.example.yaml
  • esphome/components/ct002/__init__.py
  • esphome/components/ct002/ct002.cpp
  • esphome/components/ct002/ct002.h
  • esphome/components/ct002/test_hooks.cpp
  • src/astrameter/ct002/ct002.py
  • src/astrameter/main.py
  • tests/components/ct002/test_shared_e2e.py
  • tests/test_ct002_protocol.py
  • web/ts/schema.ts

Comment thread src/astrameter/ct002/ct002.py
claude added 3 commits June 12, 2026 05:46
Consumer.phase can now hold "0"/"D", but reporting_consumer_rows() still
coerced every non-a/b/c phase to "a", so the Python cd=4 slave list
claimed phase A for inspecting/combined batteries while the ESPHome
mirror returns the stored phase verbatim. Extend ReportingPhase with
"d"/"0" and pass the canonical char through, matching the real CT's
slv_p=<phase char> semantics.

https://claude.ai/code/session_01L1fNX5zVsyvfUtVw9yK2Hi
@tomquist tomquist merged commit 5d1a801 into develop Jun 12, 2026
27 checks passed
@tomquist tomquist deleted the claude/fervent-heisenberg-0aet1p branch June 12, 2026 18:43
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