Repository navigation
Discovery catalogue: AlphaESS, Solis, Deye and Sunsynk reporters - #5206
Merged
Merged
Conversation
…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>
Contributor
There was a problem hiding this comment.
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
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.
…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>
Contributor
There was a problem hiding this comment.
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
Open (2)
Resolved since last review (1)
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

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 hasmainmerged 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 oneself.refresh_discovery()call inrun()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. Noautomatic_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 realrun()down the deferring path, so moving the call below the early return fails a test.What each reporter says
inverter_typeAlphaESSCloudon every system (battery-less systems are dropped at discovery)battery, plussolarwhen the PV rating is non-zeroev_chargerflag only when live telemetry has seen oneSolisCloudonly whereautomatic_config()drives it: the detail doesn't report "no battery" and the SoH parsessolaron every inverter,batteryon battery inverters, none until the detail has been readsolis_nominal_voltageis configuredtou_v2flag;inverterNamenever reportedDeyeCloudon every invertersolarandbatterystation_idonly when the account has exactly one stationSunsynkCloudon every invertersolarandbatteryexport_limitcapabilityEach
inverter_typerule copies the one in its component's ownautomatic_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:batteryamong 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:
battery_capacity_ahis 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, aspublish_entities()already does.battery_kwhis rounded to 2 dp in the Solis and Sunsynk reporters (Deye already rounded).solaronly, which reads as PV-only.First review
Three fixes from the Copilot review, all in a50dc04:
fetch_device_data()rebuildsdevice_valuesevery poll and drops a battery field the endpoint didn't return.publish_data()then leaves thebattery_capacitysensor, and sosoc_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 perchargeVoltthat fits no LiFePO4 stack. It was reached two or three times a cycle and logged the sameWarneach time.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_idbuilt from it and theduplicate_serialobservation naming it, and logged aWarnper inverter on every catalogue read.hardware_ids.serialor an entry inserials) 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. Otherhardware_idskeys keep the all-digit guard.device_idbuilt from its record's own serial is no longer replaced wholesale just because the record carriesaccount_ids. Deye'sdeye:{serial}stays readable while its station ID is still pseudonymised. Octopus and Solcastdevice_ids, which are built from account identifiers, are unchanged.Testing
ESS_LIST_SAMPLE, the Solis inverter details, Deye's live data list, Sunsynk's confirmed-live constants).validate_report()that checks nothing is dropped.derive_battery_capacity(), which logs and writes state.Known issues, not fixed here
export_limitmeans "has the setting" on Fox but "an export limit can be derived" on Sunsynk. Whetherfunctionsrecords 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