Repository navigation
fix(solis): use a stable voltage for amp<->watt conversions - #5095
Merged
Merged
Conversation
The inverter holds its charge/discharge limits as currents, but every watt value Predbat derived from them used the live measured battery voltage, so a fixed 70A limit republished as anything from 3352W to 3726W as the pack moved with state of charge. That dragged battery_rate_max (auto-bound to the max_charge_power entity), the write tolerance computed from it, and the read-back of every rate setpoint along with it - producing spurious rewrites and "didn't complete got Y" warnings with nothing on the inverter changed. get_nominal_voltage() now returns a voltage that holds still: the configured solis_nominal_voltage if set (which previously fed only the capacity calculation), otherwise the first reported batteryVoltage latched and held, otherwise the 48V fallback. The live value is still published as its own sensor for display. The latched reading is deliberately not snapped to a cell count - LiFePO4's curve is flat enough that one 16S pack reads 47.9V to 53.2V, which rounds to 15, 16 or 17 cells, so snapping would be a less predictable guess than holding the measurement (the same reasoning as deye.py's nominal_pack_voltage()). Fixes #5090 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Clarify the diagnostic distinction between a latched voltage reading and the 48V fallback.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This PR stabilizes Solis amp-to-watt conversions using configured, latched, or fallback voltage.
Changes:
- Updated Solis voltage selection and conversion logic.
- Added regression tests for stability and rate setpoints.
- Revised Solis documentation and configuration templates.
File summaries
| File | Summary |
|---|---|
templates/solis_cloud.yaml |
Updated sample configuration guidance. |
docs/components.md |
Documented revised Solis conversion behavior. |
docs/apps-yaml.md |
Updated nominal-voltage configuration documentation. |
apps/predbat/tests/test_solis.py |
Added voltage and setpoint stability tests. |
apps/predbat/solis.py |
Implemented stable voltage selection and conversions. |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The local cspell pre-commit hook does not actually check spelling, so run_pre_commit passing is not evidence CI will. Verified with npx cspell directly: "setpoints" was unknown (added to the workspace dictionary, matching the wording issue #5090 itself uses) and the SN0NOMV01/02 test serials tripped on "NOMV", so they are now SN0VOLT01/02 rather than teaching the dictionary a non-word. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replaces the latch-the-first-reading approach with the strategy the field data in issue #5090 supports. 30 days of history from 30 LV systems shows the live voltage cannot identify the pack at all: 15S averages 49.3-50.4V and 16S 52.6-54.4V, but a full 15S reaches 53.5V and a low 16S drops to 47.1V, so any rule reading a single sample lands in the wrong class. batteryType is no help either - it names the BMS protocol, and PYLON_LV was reported by both classes. inverterDetail does carry the BMS-requested charge voltage, top level as batteryAcvSet and per battery as batteryList[].batteryChargingVoltage, and it separates the classes cleanly: 53.2V on every 15S pack that reported it, 56.5-58.4V on every 16S, leaving 55V in an empty 3.3V gap. Being a setting rather than a measurement it also holds still, which is what #5090 needs. get_nominal_voltage() now returns, in order: solis_nominal_voltage if configured; the live voltage when it exceeds 60V, since that is an HV pack and no sampled LV system passed 57.5V (preserving #4493); 51.2V or 48V by the charge voltage against the 55V threshold; else the 48V fallback, for the one sampled inverter that reports neither field. HV packs still track the live voltage and so still drift - there is no field data to classify them by, and solis_nominal_voltage remains the fix there. Co-Authored-By: Claude Opus 5 (1M context) <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.
This is an automated draft PR generated from issue #5090 — a maintainer should review it before merging.
Fixes #5090
Summary
The inverter holds its charge/discharge limits as currents (CID 7224/7226 and the per-slot currents), but
get_nominal_voltage()re-derived every watt value from the live measuredbatteryVoltage. A fixed 70A limit therefore republished as anything from 3352W to 3726W as the pack moved with state of charge, draggingbattery_rate_max(auto-bound to themax_charge_powerentity), the write tolerance computed from it, and the read-back of every rate setpoint along with it.Implements the strategy from the maintainer's field-data comment (#5090 (comment)).
get_nominal_voltage()now returns, in priority order:solis_nominal_voltagefrom apps.yaml — which previously fed onlyget_capacity_voltage()and did nothing for the rate paths, so there was no workaround for this at all;Two new helpers support this:
get_live_battery_voltage()(the measurement, retaining the last-known fallback so #4493's behaviour is preserved exactly) andget_bms_charge_voltage()(the first non-zero of top-levelbatteryAcvSetor anybatteryList[].batteryChargingVoltage— identical on all 14 sampled systems). The live voltage is still published as its own sensor for display.Why not the original suggestion
The issue description's first idea — infer a nominal from a live reading and snap it to a pack class — does not work, and the field data is what establishes that: the LV classes overlap (a full 15S reaches 53.5V, a low 16S drops to 47.1V), so any rule reading a single sample lands in the wrong class.
batteryTypecannot arbitrate either, since it names the BMS protocol andPYLON_LVwas reported by both classes. The charge voltage is the only field that separates them, and being a setting rather than a measurement it also holds still — which is what the bug needs.Testing
coverage/run_pre_commit— all 12 hooks pass with no auto-fixes, and the bundledrun_all --quickpasses (All tests passed (4 slow tests skipped, total time: 120.45s)).npx cspellrun separately over the changed files (the local cspell hook does not actually check spelling): clean.Two new tests in
test_solis.py, both of which fail without the fix and pass with it. Verified by restoringapps/predbat/solis.pytoorigin/mainwhile keeping the test file, and driving the two tests directly:test_nominal_voltage_sources_and_stability— red withExpected the 48V fallback when no charge voltage is reported, got 51.3test_rate_setpoints_do_not_drift_with_battery_voltage— red withmax_charge_power moved from 3647W to 3352W at 47.89V with the current unchanged, which lands inside the 3352W–3726W range the reporter measured over three daysThe second is the test shape the issue asked for: it writes a slot rate in watts, moves the live voltage between the write and the next publish, and asserts the published watts and the
battery_rate_maxsource entity are unchanged and that the write round trip closes. The first covers both charge-voltage field names, the 53.2 / 54.9 / 55.0 / 56.5 / 58.4 boundary, stability across the full 47.1–53.5V live range, the HV path plus its last-known fallback, and the no-charge-voltage fallback.One note on the red run: the pre-fix
solis.pyreadsnominal_voltage_last_known, which this PR renames, so the red driver sets that attribute on the mock to keep the failure behavioural rather than anAttributeError.Notes
solis_nominal_voltageis the only fix there, which the docs now say. Worth a follow-up if there are HV systems to sample.solis_nominal_voltageis configured it now wins for power conversions too, not just capacity. Three assertions intest_publish_entitiesencoded the opposite — that a configured 512V nominal is ignored in favour of a live 52.3V — and I updated them rather than working around them, since that preference is the bug.soc_maxauto-binding left disabled. The original "would wobble cycle to cycle" rationale is now gone, but withoutsolis_nominal_voltagethe capacity is still an inference, andset_arg_auto()defaults tooverwrite=True, so enabling it would replace a correct hand-enteredsoc_maxwith an approximate one. That is a maintainer's judgement rather than part of this fix; the line stays commented and the comment now states the current reasoning.test_get_nominal_voltage_and_capacity_voltageuses 533V, which is above the 60V threshold, so it now passes via the HV branch. It reads like general coverage but exercises only HV — the LV path is covered by the new test.get_nominal_voltage()has 17 call sites, all withinsolis.py, no external callers, and no signature change.docs/apps-yaml.md,docs/components.mdandtemplates/solis_cloud.yamlare not incidental — all three told users that the max charge/discharge power sensors use the live voltage and thatsolis_nominal_voltageis only needed for capacity, which is no longer true.batteryChargingCurrent/batteryDischargeLimitinghold the BMS current limits separately from the inverter's own CID 7224/7226 settings. That belongs intools/debug-journal.mdrather than in this fix.🤖 Generated with Claude Code