Repository navigation
Mixed inverter setups: the charge/discharge rate is read and written by "is the key set", not by each inverter's own entry #5359
Description
Posted by Claude on Rik's behalf.
Update, 3 Oct: #4645 was closed in favour of #5373, which fixes #3311 without a dummy entity. So the "#4645" columns below, and the new warning described in scenario 2, no longer apply. What the "main" columns show still stands.
A third mixed-fleet case belongs here too. Next to a "current"-mode inverter, a script-driven inverter's
charge_rateentry holds a plain number, put there by the other inverter'screate_missing_arg(). That number counts as configured, so #5373's remembered rate is never used and the script-driven inverter reads back the maximum.
Follow-up to #4645, found while reviewing it. It only affects setups with more than one inverter, where the
inverters control their rate in different ways.
Problem
get_current_charge_rate() / get_current_discharge_rate() and adjust_charge_rate() / adjust_discharge_rate()
pick their path by whether the key exists anywhere in apps.yaml, for example "charge_rate_percent" in self.base.args.
They don't check whether this inverter's entry in the list is set. As soon as one inverter is set up with
charge_rate_percent, every other inverter:
- reads its rate from
charge_rate_percent, finds no entry for itself, uses the 100% default, and so always
reads backbattery_rate_max, whatever was last written; - writes to
charge_rate_percent(and tocharge_rate) even when its own entry is empty, which logs a warning
and sets a warning on the status every time the rate changes.
For a script-driven "power" inverter such as the Solax SX4 template, that read is the #3311 fault: the script's
{power} is always the maximum. The rate also never reads back as the value written, so it is written again
every time the plan moves the rate.
Scenario 1: a GivEnergy 3-phase on GE Cloud at index 0 and a script-driven Solax at index 1
GE Cloud sets the 3-phase unit up through charge_rate_percent: [number.…] and leaves charge_rate: [None] (#4908).
| main | #4645 | |
|---|---|---|
Solax gets a charge_rate entity |
no | yes |
| Solax reads back after writing 2500W | 6000W (max) | 6000W (max) |
| Warnings per rate change | 2 (charge_rate, charge_rate_percent) |
1 (charge_rate_percent) |
This is already wrong on main, and #4645 doesn't fix it for this setup. The entity #4645 creates is there, but the
reader never looks at it, because it takes the percent path first.
Scenario 2: a script-driven Solax at index 0, and an inverter set up with only charge_rate_percent at index 1
charge_rate_percent: [None, number.inv1_pct], with no charge_rate.
| main | #4645 | |
|---|---|---|
| Solax reads back after writing 2500W | 6000W (max) | 6000W (max) |
| Solax warnings per rate change | 1 (charge_rate_percent) |
1 (charge_rate_percent) |
| Inverter 1 reads back | 2460W (correct, 1% step) | 2460W (correct) |
| Inverter 1 warnings per rate change | none | 1 (No entity_id for charge_rate) |
Inverter 1's warning is new with #4645. #4645 creates charge_rate: [dummy for 0], so the key now exists, and
inverter 1 then tries to write charge_rate for its own missing entry. The Solax half is already wrong on main.
Suggested fix
Choose the path for each inverter from its own entry rather than from whether the key exists: read
charge_rate_percent only when this inverter's entry is set, and skip a write whose entry is empty. That's the same
per-entry rule #4645 already uses when deciding whether to create the entity. It fixes both scenarios, and would let
the entity #4645 creates in scenario 1 actually be used.
Not affected
Setups with a single inverter, and setups where every inverter controls its rate the same way. Neither is affected
on main or with #4645.
Activity
springfall2008 commentedon Oct 2, 2026
Automated first-pass triage — a maintainer will review before any action is taken.
Classification:
bug. Priority:priority_low, matching the label already on the ticket: it needs a fleet where one inverter's rate is percentage-controlled and another's is not — a narrow family — though within that family the failure is real (for a script-driven inverter the planned rate's carrier doesn't exist, so the read never matches the write and the rate is re-issued every cycle).Verified against main at
b161949d(v9.3.3-24-gb161949d). All four path-selection sites confirmed on current main:apps/predbat/inverter.py:2092(get_current_charge_rate()) and:2075(get_current_discharge_rate()) take the percent path whenever the key exists anywhere inargs. ANoneslot then resolves quietly to the caller's default100.0(get_arg()'s float branch inuserinterface.py— "a missing value is what the default is for, not an error"), i.e.battery_rate_max_raw, exactly as the issue says.:2155/:2164(adjust_charge_rate()) and:2204/:2213(adjust_discharge_rate()) write both registers whenever either key exists, regardless of this inverter's own entry. A write aimed at aNoneslot hitscheck_write_entity()(:2320), which logs theNo entity_id for ...warning andrecord_status(..., had_errors=True)— so each rate change also pins an error onto the status sensor, slightly worse than the issue's wording.I also reproduced it locally: a two-inverter probe (
charge_rate_percent: [None, number.charge_rate_percent],charge_rate: [None, number.charge_rate], bothoutput_charge_control: "power") run through the inverter test module confirms inverter 0 reads backbattery_rate_max_raw(6000W), its sibling reads the correct 2460W (41% × 6000W — the issue's own example), two consecutiveadjust_charge_rate(2500)calls each log bothNo entity_idwarnings and re-write, while the sibling with its entry set warns nothing. (The probe was added temporarily for triage only.)Two refinements for the maintainer's read of the issue:
charge_rate: [None]— it deletes the key when the device has no watts register (gecloud.py:1662, the GECloud: Don't control charge rate percentage if we have a rate setting #4217 rule; confirmed by the GEC (GivEnergy Cloud) with ge_cloud_automatic: true uses hardcoded 2600W default instead of correctly-fetched 20000W inverter capability (3-phase HV inverter) #4908 device inventory that 3-phase units have only the percent control). With the key deleted, main's write side warns once (percent only) rather than twice; a list that stops short instead addsOut of range indexread warnings. The mechanism is the same either way.inverter.py:583checksget_arg("charge_rate", indirect=False, index=self.id)per entry before falling back. Extending that per-entry rule to the four rate sites — as the issue suggests — would also make the entity fix(inverter): auto-create charge_rate entity for script-driven "power" inverters (#3311) #4645 creates usable in the GE-Cloud-3-phase + Solax shape, since the reader would then look at the slot that was actually filled.Related, not duplicates: #3311 (the fault that motivated PR #4645), #4908 (why 3-phase GEC is percent-only and why battery sizing is already per-entry), #5252 (rate writes one cycle behind on service-driven inverters), #4824 (vendor auto-configs overwriting each other's args — the same mixed-fleet shape from a different angle). PR #4645's body already points here in its Follow-up section, and this matches its assessment: the entity creation is per-entry, but the readers and writers are not, and that gap is not fixed by #4645.
No further information needed — the report is a code-level finding from the #4645 review and reproduced from source alone.
test(inverter): model the mixed fleet as a per-inverter list, sweep e…
fix(inverter): read and write the rate per inverter, so a mixed GE Cl…
fix(inverter): no Freeze Export on GivEnergy 3-phase, where the charg…