Skip to content

Discovery catalogue: AlphaESS, Solis, Deye and Sunsynk reporters - #5206

Merged
springfall2008 merged 12 commits into
mainfrom
feat/discovery-phase1-plan2
Sep 24, 2026
Merged

springfall2008 merged 12 commits into
mainfrom
feat/discovery-phase1-plan2

Conversation

@springfall2008

@springfall2008 springfall2008 commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

What this does

Adds AlphaESS, Solis, Deye and Sunsynk as reporters to the discovery catalogue, completing Phase 1 of the reporter rollout. With Fox (#5184) and GivTCP/GE Cloud already reporting, the catalogue now describes hardware from every cloud integration in Phase 1.

#5184 has merged, and this PR now targets main. The branch has main merged in, and the quick suite passes on the result.

Observe-only

None of the four integrations behaves differently. Across the four modules the production change is 350 added lines and no removed ones: an import, a build_discovery() method and one self.refresh_discovery() call in run() per file. Each reporter reads only data its component already holds, so there are no new API calls. refresh_discovery() catches and logs its own failures. No automatic_config(), publish or control code is touched.

Where the call sits

In every component the report is filed after the data is published and before the first-cycle early return and automatic_config(), as Fox does in #5184. A first cycle whose telemetry poll fails still reports, and so does an install that auto-configuration skips. Wiring tests drive each component's real run() down the deferring path, so moving the call below the early return fails a test.

What each reporter says

inverter_type Functions Ratings Notes
AlphaESS AlphaESSCloud on every system (battery-less systems are dropped at discovery) battery, plus solar when the PV rating is non-zero inverter, PV, battery kWh ev_charger flag only when live telemetry has seen one
Solis SolisCloud only where automatic_config() drives it: the detail doesn't report "no battery" and the SoH parses solar on every inverter, battery on battery inverters, none until the detail has been read inverter, battery Ah (bank total), pack count, kWh only when solis_nominal_voltage is configured tou_v2 flag; inverterName never reported
Deye DeyeCloud on every inverter solar and battery inverter, battery kWh, battery Ah station_id only when the account has exactly one station
Sunsynk SunsynkCloud on every inverter solar and battery inverter, battery kWh, battery Ah export_limit capability

Each inverter_type rule copies the one in its component's own automatic_config(). The rule is copied rather than shared, so the control path isn't touched.

Deliberately left out: values that change from cycle to cycle (AlphaESS emsStatus, usCapacity, surplusCobat; the Solis SoH value itself), because every change would re-file the report. Also left out are free-text site names and anything not held without a new API call.

Deye and Sunsynk cannot tell a PV-only inverter from a hybrid. Their APIs give no signal and their automatic_config() treats every inverter as a battery inverter, so their records follow that configuration. The docs say so, and describe how a misconfigured PV-only unit shows: battery among its functions but no battery ratings.

Review

Each reporter was reviewed on its own, then the branch as a whole. The whole-branch review led to seven fixes, all in the last commit:

  • Solis battery_capacity_ah is now the bank total. Register 172 is Ah per pack. Deye and Sunsynk report the whole bank under the same key, so Solis now multiplies by the pack count, as publish_entities() already does.
  • Derived battery_kwh is rounded to 2 dp in the Solis and Sunsynk reporters (Deye already rounded).
  • A Solis inverter whose detail hasn't been read yet reports no functions. It previously showed solar only, which reads as PV-only.
  • Two new wiring tests: one checks that Solis still files a report when auto-configuration is off, and one checks that a report is re-filed on a later cycle.
  • Docstring and docs wording corrections.

First review

Three fixes from the Copilot review, all in a50dc04:

  • Sunsynk keeps its last battery ratings through a partial poll. fetch_device_data() rebuilds device_values every poll and drops a battery field the endpoint didn't return. publish_data() then leaves the battery_capacity sensor, and so soc_max, at its last value. The reporter now does the same, rather than filing a thinner report and then re-filing.
  • nominal_pack_voltage() warns once per chargeVolt that fits no LiFePO4 stack. It was reached two or three times a cycle and logged the same Warn each time.
  • The plan's Solis example is corrected to the bank-total battery_capacity_ah. A new Amendments section records how the executed code differs from the plan.

Serials stay readable

A hardware serial is not secret, so this PR also changes the catalogue's redaction (2967c89). Solis, Deye and Sunsynk serials are all digits, and the guard against a misfiled MPAN used to hide them. It hid the serial, the device_id built from it and the duplicate_serial observation naming it, and logged a Warn per inverter on every catalogue read.

  • A serial a record declares (hardware_ids.serial or an entry in serials) now stays readable wherever it appears as a whole token. Only the serial itself is let through: a digit run beside it, or a longer number it merely sits inside, is still checked. Other hardware_ids keys keep the all-digit guard.
  • A device_id built from its record's own serial is no longer replaced wholesale just because the record carries account_ids. Deye's deye:{serial} stays readable while its station ID is still pseudonymised. Octopus and Solcast device_ids, which are built from account identifiers, are unchanged.
  • Two new coordinator tests use real serial shapes. Mutation checks showed each rule is load-bearing, and the docs' redaction section now states the rule.

Testing

  • The quick suite passes and pre-commit is clean.
  • Fixtures are each integration's existing captured API samples (ESS_LIST_SAMPLE, the Solis inverter details, Deye's live data list, Sunsynk's confirmed-live constants).
  • Each reporter has tests for the records it describes, for devices it excludes, for an empty fleet, and a round trip through validate_report() that checks nothing is dropped.
  • The Deye test fails if the reporter calls derive_battery_capacity(), which logs and writes state.
  • Placement and evidence rules were each shown to be load-bearing with a temporary mutation that made a test fail.

Known issues, not fixed here

  • Capability names need a pass. export_limit means "has the setting" on Fox but "an export limit can be derived" on Sunsynk. Whether functions records evidence or configuration also differs between reporters. This was planned for once several reporters had contributed tokens.

Next

The remaining reporters (Teslemetry, Sigenergy, SolaX, Enphase, Gateway, then myenergi, Kraken, Axle and Carbon) follow the rollout plan. Entity maps remain a separate project.

🤖 Generated with Claude Code

CI and others added 7 commits September 21, 2026 19:49
…ters

Plan 2 of the reporter rollout, observe-only and with no entity maps
(deferred to their own project, decided 2026-09-21).

Written from research rather than recollection, because Plan 1's Fox task
got the data model and the call placement wrong from shallow reading.
Each module was read for its real API samples, its automatic_config()
predicate, its run() control flow, and what it does and does not hold.
Every accessor a reporter calls was checked for side effects, and the one
Sunsynk capacity assertion was confirmed by running the component's code.

What the research changed from the Fox pattern:
- none of the four automatic_config() methods raises. The placement rule
  still holds, but the real reason is each component's own early exit
  after publish (`if first and not live_ok`), or Solis's gated auto-config;
- Solis's derived kWh rests on an inferred voltage (GH#5090), so it is
  reported only when solis_nominal_voltage is configured; the register Ah
  and pack count are always reported;
- Deye and Sunsynk cannot tell a PV-only unit from a hybrid, and their
  automatic_config() treats every device as a battery inverter; the
  records mirror that, because it is what Predbat believes;
- Deye's derive_battery_capacity() logs and writes state, so the reporter
  reads battery_capacity() instead, with a test that it never calls it;
- none holds firmware, and only Deye holds a station ID - attributed only
  when the account has exactly one station, since devices are not held
  per station.

Adds four rules to Plan 1's: no new API calls, stable facts only,
side-effect-free helpers only, and each placement comment giving the
component's real reason.

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

F1: Solis battery_capacity_ah now reports the bank total (register 172 x
    parallel_battery_count), matching publish_entities() and every other reporter.
F2: round derived battery_kwh to 2 dp in Solis and Sunsynk build_discovery() (Deye
    already rounded in its accessor).
F3: Solis reports functions=None for an inverter whose detail has not been read yet,
    instead of a misleading solar-only guess.
F4: fixed the Sunsynk export_limit docstring - it is reported per device, not exactly
    where automatic_config()'s fleet-wide test would bind it.
F5: replaced the Deye/Sunsynk paragraph in docs/discovery-catalogue.md with the
    corrected wording, including Solis's PV-only situation.
F6: added the two missing wiring tests - Solis with the auto-configure gate closed,
    and a second-cycle test proving refresh_discovery() sits outside `if first:`.
F7: documented that Deye and Sunsynk battery ratings follow the latest poll, same as
    soc_max's derivation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 23, 2026 21:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Address the two Sunsynk reporter issues and update the stale plan example.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Medium severity

Open (1)
What changed in this PR

Adds observe-only discovery reporters for AlphaESS, Solis, Deye, and Sunsynk.

Changes:

  • Added reporter implementations and runtime wiring.
  • Added integration, validation, and lifecycle tests.
  • Updated catalogue documentation, rollout plans, and spelling data.
File Summary
docs/​superpowers/​plans/​2026-09-21-discovery-reporters-alphaess-solis-deye-sunsynk.md Rollout plan; one stale capacity example remains.
docs/​discovery-catalogue.md Discovery catalogue documentation updates.
apps/​predbat/​tests/​test_sunsynk_config.py Sunsynk reporter and lifecycle tests.
apps/​predbat/​tests/​test_solis.py Solis reporter tests.
apps/​predbat/​tests/​test_deye_api.py Deye reporter tests.
apps/​predbat/​tests/​test_alphaess_api.py AlphaESS reporter tests.
apps/​predbat/​sunsynk.py Sunsynk reporter and runtime wiring; capacity warning amplification and transient rating loss require changes.
apps/​predbat/​solis.py Solis reporter and runtime wiring.
apps/​predbat/​deye.py Deye reporter and runtime wiring.
apps/​predbat/​alphaess.py AlphaESS reporter and runtime wiring.
.cspell/​custom-dictionary-workspace.txt Added test identifier.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread apps/predbat/sunsynk.py Outdated
Base automatically changed from feat/discovery-phase1-reporters to main September 24, 2026 06:54
CI and others added 3 commits September 24, 2026 08:00
…and warns once per chargeVolt

Addresses the Copilot review on #5206:
- fetch_device_data() rebuilds device_values each poll and omits a battery field the
  endpoint left out. publish_data() then leaves the battery_capacity sensor (and
  soc_max) at its last value; the reporter now does the same instead of filing a
  thinner report and re-filing when the fields return.
- nominal_pack_voltage() warns once per chargeVolt that fits no LiFePO4 stack. It
  was reached two or three times a cycle and logged the same Warn each time.
- The plan's Solis capacity example matched the per-pack figure the final review
  replaced; corrected, and an Amendments section records how the executed code
  differs from the plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed catalogue

A hardware serial is not secret. Solis, Deye and Sunsynk serials are nothing but
digits, so the misfiled-identifier guard treated them as a possible MPAN: it hid
the serial, the device_id built from it and the duplicate_serial observation
naming it, and logged a Warn per inverter on every catalogue read.

- A serial a record declares (hardware_ids.serial or an entry in serials) is
  collected before the walk and removed, as a whole token, before the shape guard
  judges a value. It stays readable wherever it appears; anything left beside it,
  or a longer number it merely sits inside, is still checked.
- A device_id built from its record's own serial and containing none of the
  record's account_ids values is no longer treated as identity-derived, so Deye's
  "deye:{serial}" stays readable beside its pseudonymised station id. Octopus and
  Solcast device_ids, built from account identifiers, are unchanged.
- Other hardware_ids keys keep the all-digit guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Moderate issues remain in serial-boundary redaction and Sunsynk partial-poll rating consistency.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 Medium severity

Open (2)
Resolved since last review (1)

Comment thread apps/predbat/coordinator.py Outdated
Comment thread apps/predbat/sunsynk.py Outdated
…eeps Ah and kWh as a pair

Addresses the second Copilot review on #5206:
- _serial_derived() matched the serial by plain substring and ignored account_ids
  values under MIN_SUBSTITUTE. It now uses the same whole-token rule as the shape
  guard in both directions: the serial must stand as a whole token in the
  device_id, and any account_ids value standing there as a whole token, however
  short, keeps the device_id identity-derived. One _whole_token() helper now
  serves both.
- The Sunsynk reporter kept Ah and kWh independently, so a poll with a new
  capacity but no chargeVolt filed the new Ah beside the old kWh. They are now
  kept as one pair; an install that never reports a chargeVolt still reports its
  Ah alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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