feat(battery): plan a periodic full charge of the stationary battery (#1486)

LFP batteries can only recalibrate their state of charge close to full
charge, so many systems expect a full charge every few weeks and some
start one on their own at an arbitrary, possibly expensive time. EOS can
now plan it instead (GENETIC only, off by default).

New battery settings: full_charge_interval_days (unset = off),
full_charge_soc_percentage, full_charge_detect_percentage,
full_charge_allow_grid and the informational cell_chemistry. The last
full charge is read from the measured SoC history; nothing is stored.
When the next one is due within the horizon (or overdue), the maximum
SoC is raised to the target. With full_charge_allow_grid the optimizer
must reach the target once before the deadline, which is at least 24 h
away (penalty battery_full_charge_miss on the best SoC reached), so it
picks the cheapest time itself. Without it, PV surplus may fill the
battery but the target is not enforced.

Signed-off-by: SchaafAlexander <86607129+SchaafAlexander@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
SchaafAlexander
2026-11-08 07:40:13 +01:00
committed by GitHub
co-authored by Claude Opus 5.5
parent b33953d445
commit a06473b6d6
12 changed files with 1085 additions and 6 deletions
+22 -2
View File
@@ -65,7 +65,12 @@ config path from ``self.device_id`` without needing an external index.
1.0
],
"min_soc_deadline_datetime": null,
"min_soc_max_duration_h": null
"min_soc_max_duration_h": null,
"cell_chemistry": null,
"full_charge_interval_days": null,
"full_charge_soc_percentage": 100,
"full_charge_detect_percentage": 99,
"full_charge_allow_grid": false
}
},
"max_batteries": 1,
@@ -102,7 +107,12 @@ config path from ``self.device_id`` without needing an external index.
1.0
],
"min_soc_deadline_datetime": null,
"min_soc_max_duration_h": null
"min_soc_max_duration_h": null,
"cell_chemistry": null,
"full_charge_interval_days": null,
"full_charge_soc_percentage": 100,
"full_charge_detect_percentage": 99,
"full_charge_allow_grid": false
}
},
"max_electric_vehicles": 1,
@@ -174,6 +184,11 @@ config path from ``self.device_id`` without needing an external index.
],
"min_soc_deadline_datetime": null,
"min_soc_max_duration_h": null,
"cell_chemistry": null,
"full_charge_interval_days": null,
"full_charge_soc_percentage": 100,
"full_charge_detect_percentage": 99,
"full_charge_allow_grid": false,
"measurement_key_soc_factor": "bat0-soc-factor",
"measurement_key_power_l1_w": "bat0-power-l1-w",
"measurement_key_power_l2_w": "bat0-power-l2-w",
@@ -223,6 +238,11 @@ config path from ``self.device_id`` without needing an external index.
],
"min_soc_deadline_datetime": null,
"min_soc_max_duration_h": null,
"cell_chemistry": null,
"full_charge_interval_days": null,
"full_charge_soc_percentage": 100,
"full_charge_detect_percentage": 99,
"full_charge_allow_grid": false,
"measurement_key_soc_factor": "ev0-soc-factor",
"measurement_key_power_l1_w": "ev0-power-l1-w",
"measurement_key_power_l2_w": "ev0-power-l2-w",
+12 -2
View File
@@ -69,7 +69,12 @@
1.0
],
"min_soc_deadline_datetime": null,
"min_soc_max_duration_h": null
"min_soc_max_duration_h": null,
"cell_chemistry": null,
"full_charge_interval_days": null,
"full_charge_soc_percentage": 100,
"full_charge_detect_percentage": 99,
"full_charge_allow_grid": false
}
},
"max_batteries": 1,
@@ -106,7 +111,12 @@
1.0
],
"min_soc_deadline_datetime": null,
"min_soc_max_duration_h": null
"min_soc_max_duration_h": null,
"cell_chemistry": null,
"full_charge_interval_days": null,
"full_charge_soc_percentage": 100,
"full_charge_detect_percentage": 99,
"full_charge_allow_grid": false
}
},
"max_electric_vehicles": 1,
+56
View File
@@ -213,6 +213,11 @@ The behavior of the genetic algorithm can be customized using the following conf
A value of `1.0` (default) means the penalty equals the actual economic loss in €.
Use larger values (e.g. `3.0`) to make the optimizer more aggressively avoid unprofitable
AC charging, or `0.0` to disable this penalty entirely.
- `battery_full_charge_miss`:
Applies a penalty per percent the battery stays below its target when a periodic full
charge is due and enforced (see [Periodic full charge](#periodic-full-charge)).
Default: `10`.
:::
#### Value Formats
@@ -384,6 +389,57 @@ The inverter supports separate AC↔DC conversion efficiencies:
}
```
##### Periodic full charge
LFP batteries have an almost flat voltage curve in the middle of their range. The battery
management system can only recalibrate the state of charge close to full charge, so many
manufacturers expect a full charge every few weeks, and some batteries start one on their own
at an arbitrary time. A periodic full charge lets EOS plan it instead, preferably from PV
surplus or at a cheap price.
The feature is off by default. It is enabled per stationary battery with
`full_charge_interval_days` (GENETIC only):
- `full_charge_interval_days`: maximum number of days between two full charges. `null` (default)
disables the feature. For LFP batteries 21 to 25 days are a reasonable start; check what the
manufacturer requires.
- `full_charge_soc_percentage` (default `100`): the target. It may be above `max_soc_percentage`,
which keeps applying on all other days.
- `full_charge_detect_percentage` (default `99`): the measured state of charge from which the
battery counts as full. Many battery management systems never report exactly 100 %.
- `full_charge_allow_grid` (default `false`): with `false`, EOS only raises the maximum state of
charge while the full charge is due, so that PV surplus can fill the battery; the target is not
enforced. With `true`, the optimizer has to reach the target and charges from the grid if
needed, at the cheapest time before the deadline.
- `cell_chemistry` (`LFP`, `NMC`, `NCA`, `LTO` or `null`): informational only, it does not
enable anything.
The last full charge is taken from the measured state of charge of the battery
(`<device_id>-soc-factor`), so a full charge from PV or one the battery started itself counts as
well. Nothing else is stored. Without a full charge in the last interval, the interval counts
from the oldest measurement, so a new installation is not forced into a full charge on its first
day. The next full charge is due one interval after the last one. EOS acts once that deadline
falls within the optimization horizon or has passed, and logs an INFO message for every run in
which the full charge is due. The optimizer always gets at least 24 hours to finish it, so that
a close or overdue full charge can still use the cheapest time.
```json
{
"devices": {
"batteries": {
"battery1": {
"device_id": "battery1",
"capacity_wh": 10000,
"max_soc_percentage": 90,
"cell_chemistry": "LFP",
"full_charge_interval_days": 21,
"full_charge_allow_grid": true
}
}
}
}
```
#### Home appliance simulation configuration
**Example:**