Skip to content

feat(discovery): inverter records carry entities, capabilities and Predbat-named ratings (observe-only, piece 1) - #5228

Merged
springfall2008 merged 19 commits into
mainfrom
feat/discovery-record-vocabulary
Sep 26, 2026
Merged

springfall2008 merged 19 commits into
mainfrom
feat/discovery-record-vocabulary

Conversation

@springfall2008

Copy link
Copy Markdown
Owner

Piece 1 of moving automatic inverter configuration into the discovery coordinator. Observe-only: every inverter reporter's discovery record now carries enough to rebuild its INVERTER_DEF row and to reproduce what its automatic_config() binds, but nothing configures from it yet. No automatic_config(), inverter.py or write path changes; each reporter's automatic_config() was checked byte-identical before commit.

Design: docs/superpowers/specs/2026-09-24-discovery-inverter-record-vocabulary-design.md (decisions D1-D15). Plan: docs/superpowers/plans/2026-09-24-discovery-inverter-record-vocabulary.md.

What changes

  • Record vocabulary (coordinator.py, schema_version 1 -> 2)
    • entities: every setting automatic configuration binds for the device, keyed by Predbat setting name. Each descriptor needs access (rw Predbat writes / r Predbat reads) and exactly one of entity_id or a fixed value; invert replaces the *_power_invert settings.
    • capabilities: a dict of the seven INVERTER_DEF behaviour keys (support_charge_freeze, support_discharge_freeze, support_feedin_first, can_span_midnight, charge_discharge_with_rate, charge_control_immediate, target_soc_used_for_discharge) -> bool. Replaces the old token list.
    • ratings under Predbat setting names: inverter_limit, export_limit (configured grid export cap), import_limit, battery_rate_max, soc_max, battery_min_soc. Only figures the device reports - a value Predbat derives or the user overrides is an entity, not a rating (D14).
    • inverter_definition(record, write_and_poll_sleep, base=None): a pure function that rebuilds an INVERTER_DEF-shaped dict from a record. The has_* presence flags, time formats, SoC units, charge-control mode and load-entity count are derived from the entity map rather than stated twice. Never mutates INVERTER_DEF.
  • Seven reporters converted: Sunsynk, Deye, AlphaESS, Fox, Solis, GE Cloud, GivTCP. Records describe their own device (D10), ignore user opt-outs like automatic_ignore_pv (D11), and PV-only devices carry read-only pv_power/pv_today (D12).
  • tests/discovery_contract.py: shared checks every reporter's tests run - completeness (the record alone, with no row as a base, rebuilds its INVERTER_DEF row) and agreement in both directions (the record binds exactly what the unchanged automatic_config() binds, apart from named per-device/opt-out extras).
  • One INVERTER_DEF change: GEC and GEE soc_units "kWh" -> "%" (GE Cloud binds soc_percent; inv_soc_units is never read). GE-family clock_time_format is left alone because those rows also serve the GivTCP add-on and GE Cloud templates; the GivTCP/GE Cloud records describe the ISO format their sensors really publish, with a named exception in the completeness test (D13).
  • ComponentBase.WRITE_AND_POLL_SLEEP (2; GivTCP and GE Cloud 10) - read only by tests until piece 3.
  • Docs: docs/discovery-catalogue.md and the catalogue design spec updated for schema 2; a debug-journal entry on a worktree trap (running the harness in a fresh worktree without manifest.yaml self-updates over tracked files).

Visible effect: the discovery section of debug dumps and the /discovery page show the new shapes. GivTCP's entity map now lists only settings automatic configuration binds (so fewer entities than before).

Testing

  • ./run_all --quick: all tests pass (4 slow skipped, ~73s).
  • Final whole-branch review by a fresh reviewer: ready to merge, no Critical/Important findings; --quick --shuffle over six seeds, all green.
  • Per reporter: completeness, agreement both ways, two-device and mixed-fleet tests; Fox/Solis also pin that not-yet-read devices carry nothing.

Known follow-ups (not blocking; items 1-2 should land before piece 3)

  1. Deye/AlphaESS drop an energy-counter binding on a poll that omits it; Sunsynk keeps bindings once seen - pick one policy.
  2. inverter_definition() prefers soc_kw over soc_percent; inverter.py prefers soc_percent (no reporter binds both today).
  3. Fox export_limit/import_limit ratings accept only numeric setting values; if live settings/get returns strings the rating is silently absent - needs a live capture.
  4. Test-strength nits: redaction test uses value: "0" not "True" and doesn't check invert; capture_automatic_config() doesn't restore a pre-existing instance set_arg; AlphaESS/GE Cloud completeness loops don't assert a record was checked.

Next: piece 2 (SolaX, Sigenergy, Enphase, Teslemetry, Gateway reporters), then piece 3 (the coordinator configures from records; the spec lists what piece 3 must handle).

🤖 Generated with Claude Code

CI and others added 19 commits September 24, 2026 18:50
…and ratings

Spec for piece 1 of moving automatic configuration into the coordinator:
entities becomes the full per-device settings map (rw/r), capabilities an
INVERTER_DEF-shaped bool dict, ratings keyed by Predbat setting name, and
protocol detail derived from the entity map. Sections 2 and 3 are proposals
awaiting review.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…uestions

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ection 3 keeps automatic_config untouched and settles Solis reserve

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s derive from inverter_mode; dummied settings stay out of entities

Checking inverter.py while writing the plan: num_load_entities is read at
inverter.py:1523 (it adds load_power_1.. into the load), so it is derived
from the entity map; has_rest_api is the unread one. has_ge_inverter_mode /
has_ge_eco_toggle are True on the GE/GEC rows, so they derive from the
inverter_mode entity's domain rather than defaulting to False.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ates over it

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…the piece-3 findings

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ry (piece 1)

Ten tasks: coordinator vocabulary and inverter_definition(), shared contract
checks, the seven reporters, and retiring the transitional capability list.
Every code block was verified on throwaway prototype branches: the combined
branch passes the quick suite. Spec gains the soc_max reading of D14 and the
no-HA-filter convention for inverter entities.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d inverter_definition()

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…r reporters

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…edbat-named ratings

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tity map and device-reported ratings

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…edbat-named ratings

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…redbat-named ratings

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d Predbat-named ratings

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nd Predbat-named ratings; GEC/GEE soc_units %

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d entities and Predbat-named ratings

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ma 2

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…leep is 10 on the GE types; charge_rate_percent means power control

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@springfall2008
springfall2008 merged commit ebc7712 into main Sep 26, 2026
2 checks passed
@springfall2008
springfall2008 deleted the feat/discovery-record-vocabulary branch September 26, 2026 08:21
@mitulgovind

Copy link
Copy Markdown

Question about how the later pieces will handle inverters that have no discovery reporter.

I'm running a LuxPower LXP 3600ACS through the LuxPower HA integration, configured manually in apps.yaml (Predbat v9.3.1, HA add-on). Discovery picks up my Octopus meters and both Solcast sites with no conflicts. inverters: is empty because LuxPower has no discovery reporter, and resulting_config still shows num_inverters: 1 from apps.yaml:

inverters: []
observations:
  conflicts: []
  resulting_config:
    num_inverters: 1
    inverter_type: '#5f01158e'

Everything works fine today since this is observe-only. My question is for when discovery starts driving configuration: will a manual apps.yaml inverter with no discovery reporter always take precedence, and could an empty inverters: [] ever be read as "no inverter found" and override it?

Happy to share the full (anonymised) discovery output if it helps.

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