Skip to content

Mixed inverter setups: the charge/discharge rate is read and written by "is the key set", not by each inverter's own entry #5359

Open

Description

@chalfontchubby

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_rate entry holds a plain number, put there by the other inverter's create_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 back battery_rate_max, whatever was last written;
  • writes to charge_rate_percent (and to charge_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

  1. springfall2008 commented on Oct 2, 2026

    @springfall2008
    Owner

    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:

    • Reads: apps/predbat/inverter.py:2092 (get_current_charge_rate()) and :2075 (get_current_discharge_rate()) take the percent path whenever the key exists anywhere in args. A None slot then resolves quietly to the caller's default 100.0 (get_arg()'s float branch in userinterface.py — "a missing value is what the default is for, not an error"), i.e. battery_rate_max_raw, exactly as the issue says.
    • Writes: :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 a None slot hits check_write_entity() (:2320), which logs the No entity_id for ... warning and record_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], both output_charge_control: "power") run through the inverter test module confirms inverter 0 reads back battery_rate_max_raw (6000W), its sibling reads the correct 2460W (41% × 6000W — the issue's own example), two consecutive adjust_charge_rate(2500) calls each log both No entity_id warnings 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:

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions