fix(optimization): let the AC setpoint cap the total charge where the inverter does (#1334)

* fix(optimization): let the AC setpoint cap the total charge where the inverter does

Some hybrid inverters limit the battery's whole charge current to the grid
charge setpoint while grid charging is on. A Deye 12K in time-of-use grid
charging with max_grid_charge_current = 75 A charges at ~80 A even with 8 kW
of PV, and exports the rest - including the surplus of micro inverters on
the grid side. GENETIC modelled an AC slot as "PV surplus first, grid adds
ac_charge x the remaining charge power", so under PV surplus an AC slot
looked at least as good as a DC slot and the optimizer picked a low AC
factor, while the real system exported what the plan meant to store.

New inverter config option devices.inverters[].ac_charge_limits_total_charge
(GENETIC scope, default False, so the existing model is unchanged). When
True, an AC slot caps the battery's raw charge at ac_charge x
max_charge_power_w from all sources: PV surplus above the cap is exported,
the grid only fills what PV leaves of it. The option reaches the optimizer
through InverterCommonSettings.to_genetic_param, i.e. POST /v1/optimize and
the EMS run. GENETIC0 (POST /optimize) is left as it is.

The AC slot logic moves into Inverter (ac_charge_factor,
begin_ac_charge_slot, charge_battery_from_grid) so the simulation and the
tail value curve share it; Battery gets a per-slot charge cap that reset()
lifts again.

* test(optimization): narrow optional simulation arrays for mypy

The annotated fixture indexes GeneticSimulation arrays typed as Optional; assert them first so the locked mypy hook passes.
This commit is contained in:
Andreas
2026-09-21 16:15:00 +02:00
committed by GitHub
parent f5c6d2bc1e
commit 35f77c9b99
7 changed files with 423 additions and 43 deletions
+36
View File
@@ -11249,6 +11249,19 @@
"GENETIC0"
]
},
"ac_charge_limits_total_charge": {
"type": "boolean",
"title": "Ac Charge Limits Total Charge",
"description": "True if the AC charge setpoint caps the battery's total charge power, PV included. Some hybrid inverters (e.g. Deye in time-of-use grid charging) limit the whole charge current to the grid charge current; PV surplus above it is exported, not stored. False keeps the default model: PV surplus charges first and the grid adds ac_charge x max_charge_power_w on top.",
"default": false,
"examples": [
false,
true
],
"x-scope": [
"GENETIC"
]
},
"battery_id": {
"anyOf": [
{
@@ -11647,6 +11660,19 @@
"GENETIC0"
]
},
"ac_charge_limits_total_charge": {
"type": "boolean",
"title": "Ac Charge Limits Total Charge",
"description": "True if the AC charge setpoint caps the battery's total charge power, PV included. Some hybrid inverters (e.g. Deye in time-of-use grid charging) limit the whole charge current to the grid charge current; PV surplus above it is exported, not stored. False keeps the default model: PV surplus charges first and the grid adds ac_charge x max_charge_power_w on top.",
"default": false,
"examples": [
false,
true
],
"x-scope": [
"GENETIC"
]
},
"battery_id": {
"anyOf": [
{
@@ -12061,6 +12087,16 @@
0,
5000
]
},
"ac_charge_limits_total_charge": {
"type": "boolean",
"title": "Ac Charge Limits Total Charge",
"description": "True if the AC charge setpoint caps the battery's total charge power, PV included. PV surplus above it is exported, not stored. False keeps the default model: PV surplus charges first and the grid adds ac_charge x max_charge_power_w on top.",
"default": false,
"examples": [
false,
true
]
}
},
"additionalProperties": false,