Multiple Inverters / Batteries¶
Since 0.9.1 the inverter: section accepts a list of inverters. batcontrol then
treats all of them as one large battery: it sums their capacities, derives a common
state of charge, and splits every charge command over the individual devices.
The single-inverter form keeps working unchanged — nothing to do if you run one battery.
Not available in the Home Assistant add-on
This feature requires a YAML configuration file, so it is only available for the
Docker, Docker Compose and local Python installations. The
Home Assistant add-on is
configured through the add-on UI, whose options/schema form cannot express a
list of inverters with per-entry type-specific keys. The add-on therefore stays
on a single inverter, and the parameters on this page are intentionally not
mirrored into the add-on configuration.
If you run two batteries under Home Assistant, use the Docker or Compose installation instead — see Installation.
Configuration¶
inverter:
- type: fronius_gen24
address: 192.168.0.10
user: customer
password: YOUR-PASSWORD
max_grid_charge_rate: 5000 # Watt, limit of THIS inverter
- type: fronius-modbus
address: 192.168.0.11
capacity: 5000 # Wh
max_grid_charge_rate: 3000 # Watt, limit of THIS inverter
Every entry is a complete inverter configuration, exactly as described in Inverter Configuration. The types may be mixed freely — a Fronius GEN24 next to a Modbus inverter next to an MQTT bridge is fine.
The order matters: it determines each inverter's number n, which is used for the
MQTT subtree and the Home Assistant entity names (see MQTT topics).
Inserting an inverter in the middle of the list renumbers the ones behind it.
How values are aggregated¶
| Value | Aggregation |
|---|---|
| Installed capacity, usable capacity, free capacity | sum |
| Stored energy, stored usable energy | sum |
| State of charge | capacity-weighted average |
min_soc, max_soc |
capacity-weighted average |
max_grid_charge_rate |
sum |
max_pv_charge_rate |
sum, but see below |
min_pv_charge_rate |
sum |
A battery with twice the capacity therefore carries twice the weight in the group SoC. The logic that decides when to charge works on these aggregated numbers and is unaware of how many physical batteries there are.
max_pv_charge_rate is special¶
For a single inverter 0 means "no limit". That carries over to the group: if any
inverter has max_pv_charge_rate: 0, the whole group counts as unlimited, because
batcontrol cannot bound the total PV charge power any more. Only if every inverter
declares a positive limit is the group limit their sum.
How charge rates are distributed¶
When the logic asks for a charge rate, the group splits it over the inverters
proportional to their free capacity, so an empty battery gets more power than a
nearly full one and both reach their target at roughly the same time. Each share is
capped by that inverter's own max_grid_charge_rate; power that does not fit is
redistributed over the remaining inverters.
Example — 6000 W requested:
| Capacity | SoC | Free capacity | Own limit | Share | |
|---|---|---|---|---|---|
| Inverter 0 | 10000 Wh | 20 % | 7500 Wh | 5000 W | 5000 W (capped) |
| Inverter 1 | 5000 Wh | 80 % | 750 Wh | 3000 W | 1000 W (remainder) |
Inverter 0 would get 5454 W by its share of the free capacity, is capped at its 5000 W limit, and the leftover 1000 W moves to inverter 1.
If the requested rate exceeds the sum of all limits, each inverter simply runs at its own maximum.
The minimum charge rate is never split away¶
Charging a battery with a few hundred watts is inefficient, which is why the logic
layer already raises any group charge rate to MIN_CHARGE_RATE (500 W). Splitting
500 W into 250 W + 250 W would throw that guarantee away, so the distribution works
the other way round:
- Walk the inverters in order of free capacity and take as many as the requested rate can pay a full minimum for.
- Give each of them its minimum.
- Spread whatever is left over those same inverters, proportional to free capacity.
The inverters the rate does not reach get 0 W. The requested total is always kept exactly — batcontrol never buys more grid power than the optimizer asked for.
For two inverters with 7500 Wh and 250 Wh free capacity and a 500 W minimum each:
| Requested | Inverter 0 | Inverter 1 | Comment |
|---|---|---|---|
| 300 W | 300 W | 0 W | below one minimum, goes to the emptier battery |
| 500 W | 500 W | 0 W | pays exactly one minimum |
| 800 W | 800 W | 0 W | not enough for a second minimum |
| 1000 W | 500 W | 500 W | pays two minima, no remainder |
| 1200 W | 694 W | 506 W | two minima plus 200 W split 7500 : 250 |
| 2000 W | 1468 W | 532 W | two minima plus 1000 W split 7500 : 250 |
| 6000 W | 5000 W | 1000 W | inverter 0 capped at its 5000 W limit |
As a result the two batteries drift apart in SoC while the requested rate is small, and converge again once it is large enough to feed both.
min_charge_rate¶
The minimum defaults to 500 W per inverter and can be raised per inverter if a device needs more to charge efficiently. It only takes effect with several inverters — with a single inverter batcontrol already raises every charge rate to 500 W, so there is nothing left to distribute:
inverter:
- type: fronius_gen24
address: 192.168.0.10
user: customer
password: YOUR-PASSWORD
max_grid_charge_rate: 5000
min_charge_rate: 1000 # this inverter wants at least 1000 W
- type: fronius-modbus
address: 192.168.0.11
capacity: 5000
max_grid_charge_rate: 3000
An inverter whose max_grid_charge_rate is below its own min_charge_rate can
never charge efficiently and is skipped unless it is the only candidate.
For PV charge limiting the equivalent floor is min_pv_charge_rate, which defaults
to 0 (no minimum) and therefore keeps the plain proportional split unless you set
it.
A full battery is set to avoid-discharge¶
An inverter that receives no share during grid charging is not left in its previous mode — it is set to avoid discharge. Without this, a full battery would happily discharge into the battery that is being charged from the grid, which would cycle both batteries for nothing and draw grid power that never gets stored.
PV charge limiting¶
MODE_LIMIT_BATTERY_CHARGE_RATE (mode 8) is distributed the same way, with each
share capped by the inverter's own max_pv_charge_rate and floored at its
min_pv_charge_rate. A requested limit of 0 (block charging entirely) is passed
to every inverter without distribution.
MQTT topics¶
Each inverter keeps its own subtree under batcontrol's base topic, numbered by its position in the config list:
batcontrol/inverters/0/SOC
batcontrol/inverters/0/stored_energy
batcontrol/inverters/1/SOC
batcontrol/inverters/1/stored_energy
...
The top-level topics (batcontrol/SOC, batcontrol/max_energy_capacity,
batcontrol/stored_energy_capacity, ...) report the aggregated values of the
whole group, so existing dashboards keep working.
Home Assistant auto-discovery publishes one entity set per inverter, named
Inverter <n> ... with unique IDs batcontrol_inverter_<n>_*.
MQTT inverters need distinct topics¶
If you configure several inverters of type: mqtt, each needs its own base_topic
— otherwise both would read the same status and send their commands to the same
receiver. batcontrol refuses to start on a duplicate:
inverter:
- type: mqtt
base_topic: house/battery_a
capacity: 10000
max_grid_charge_rate: 5000
- type: mqtt
base_topic: house/battery_b
capacity: 5000
max_grid_charge_rate: 3000
Leaving base_topic at its default (default) for all of them is also fine: the
topic is then derived from the inverter number and is unique by construction
(batcontrol/inverters/0/, batcontrol/inverters/1/, ...).
Outage handling¶
The resilient wrapper
is applied per inverter, so enable_resilient_wrapper and
outage_tolerance_minutes are configured per entry. If one inverter becomes
unreachable, that inverter's wrapper reports the outage and batcontrol skips the
control cycle — it does not keep controlling the remaining batteries on a partial
picture, because the aggregated SoC would be wrong.
refresh_api_values (MQTT publishing) and shutdown are best-effort: a failing
inverter is logged and the remaining ones are still served.
Limitations¶
- Commands are not transactional. If an inverter fails midway through a group command, the inverters before it have already been switched. The next control cycle reconciles the state.
- No per-inverter strategy. All inverters follow the same mode; batcontrol does not, for example, charge one battery from the grid while discharging another.
- The
battery_controllimits (max_charging_from_grid_limit,always_allow_discharge_limit,min_grid_charge_soc) apply to the group SoC, not to individual batteries. - Not configurable from the Home Assistant add-on UI, which is limited to a single inverter — see the note at the top of this page.