Skip to content

Commit c816a8a

Browse files
MaStrclaude
andcommitted
Replace night_surplus_wh with pv_start_battery_wh and forecast_min_battery_wh
- Rename the MQTT metric night_surplus_wh to pv_start_battery_wh (battery level in Wh above MIN_SOC at the next net-charging crossover) - Add forecast_min_battery_wh (minimum battery level over the full forecast horizon, slot-by-slot simulation with floor/ceiling clamping) - Extract ForecastMetrics class in src/batcontrol/forecast_metrics.py - Rename test_night_surplus.py to test_pv_battery_metrics.py - Remove legacy MQTT discovery entry for night_surplus_wh - Add TODO(0.9.1) tombstone for backward-compat removal - Add docs/integrations/forecast-metrics.md with metric descriptions, decision matrix, and use cases (heat pump, EVCC automation) - Update docs/integrations/mqtt-api.md Solar Surplus section accordingly Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 9201f9a commit c816a8a

10 files changed

Lines changed: 545 additions & 319 deletions

File tree

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
# Forecast Metrics
2+
3+
`ForecastMetrics` is a stateless module that derives three battery indicators
4+
from the production/consumption forecast arrays and the current battery state.
5+
These indicators are published via MQTT and are intended to drive downstream
6+
automation decisions such as "should I run the heat pump now or save the battery
7+
for tomorrow's solar charge?"
8+
9+
All three values are updated once per evaluation cycle (every 3 minutes by
10+
default). They are based on the same forecast window that the main optimizer
11+
uses, so their horizon is `min(max available price hours, max available solar
12+
hours)`.
13+
14+
## The Three Metrics
15+
16+
### `solar_surplus_wh` — Current-Window Solar Overflow
17+
18+
**MQTT topic:** `{base}/solar_surplus_wh`
19+
20+
Expected energy in Wh that the solar production in the **current production
21+
window** will generate above what the battery can absorb.
22+
23+
- **solar_active = true (slot 0 is producing):** surplus is the net solar
24+
production of the ongoing window minus the remaining free battery capacity.
25+
`surplus > 0` means that PV power will be exported to the grid even if the
26+
battery is managed optimally.
27+
- **solar_active = false (nighttime or break before next window):** surplus
28+
accounts for the overnight discharge first — the battery will self-discharge
29+
from consumption before solar starts, which creates room. `surplus > 0` means
30+
even after that extra room is created the next solar window will still
31+
overflow.
32+
33+
A value of `0` means the battery can absorb everything the upcoming solar
34+
window produces. A value `> 0` means some PV will inevitably be exported; it
35+
is safe to run flexible loads (heat pump, EV charging) from the grid right now
36+
because the solar surplus will offset them.
37+
38+
**Use case:** If `solar_surplus_wh >= estimated_load_wh`, running a flexible
39+
load (heat pump, EV charging via evcc) has no net grid cost over the forecast
40+
horizon. See [Use Cases](#use-cases) for concrete automation examples.
41+
42+
---
43+
44+
### `pv_start_battery_wh` — Battery Level at Next Charging Point
45+
46+
**MQTT topic:** `{base}/pv_start_battery_wh`
47+
48+
Battery level in Wh (above MIN_SOC) at the moment when solar production first
49+
exceeds household consumption (`net_consumption < 0`). This is the point where
50+
the battery transitions from discharging to charging.
51+
52+
- Simulated slot-by-slot from the current moment forward.
53+
- If the battery hits `0` (MIN_SOC) before solar starts, the value is `0`.
54+
- If the battery has no net-charging slot in the forecast at all, the value
55+
is `0`.
56+
- If slot 0 is already a net-charging slot (solar already exceeds
57+
consumption), the value equals the current stored usable energy.
58+
59+
**Use case:** "How much charge will the battery have left when solar starts
60+
tomorrow?" A low value (e.g. < 500 Wh) means flexible loads tonight should
61+
be reduced; a high value means overnight loads can run freely. See
62+
[Use Cases](#use-cases) for heat pump and evcc automation examples.
63+
64+
!!! note
65+
`pv_start_battery_wh` depends on `net_consumption < 0`, not just on
66+
`production > 0`. The battery does not switch to charging until solar output
67+
exceeds household consumption — on a partly-cloudy morning the cross-over can
68+
happen later than sunrise.
69+
70+
---
71+
72+
### `forecast_min_battery_wh` — Forecast Minimum Battery Level
73+
74+
**MQTT topic:** `{base}/forecast_min_battery_wh`
75+
76+
The lowest battery level in Wh (above MIN_SOC) reached at any point during the
77+
entire forecast horizon, based on slot-by-slot simulation with proper
78+
floor/ceiling clamping.
79+
80+
- A value of `0` means the battery is expected to hit MIN_SOC at some point in
81+
the forecast — a signal that the system will be energy-constrained.
82+
- The simulation respects both the floor (MIN_SOC = 0 usable Wh) and the
83+
ceiling (MAX_SOC = stored_usable + free_capacity), so multi-day
84+
charge/discharge cycles are tracked correctly.
85+
- The horizon covers the full forecast window (same as the optimizer), not
86+
just the next 24 hours.
87+
88+
**Use case:** "Will the battery run out at any point in the planning horizon?"
89+
`forecast_min_battery_wh == 0` means batcontrol may need to grid-charge later —
90+
block or reduce flexible loads. A value comfortably above zero means the battery
91+
has buffer and loads can run. See [Use Cases](#use-cases).
92+
93+
---
94+
95+
## Decision Matrix
96+
97+
The three metrics form a natural 2-D decision space for flexible load control:
98+
99+
| `solar_surplus_wh` | `forecast_min_battery_wh` | Recommended action |
100+
|--------------------|--------------------------|-------------------|
101+
| > 0 | > 0 | Run flexible loads freely — PV will cover them and battery stays healthy |
102+
| > 0 | = 0 | PV surplus exists but battery will be short later — run light loads only |
103+
| = 0 | > 0 | No surplus but battery OK — use `pv_start_battery_wh` to judge night loads |
104+
| = 0 | = 0 | Constrained — block flexible loads, preserve battery |
105+
106+
`pv_start_battery_wh` refines the third row: if it is high, the overnight
107+
discharge is gentle and a moderate flexible load (e.g. heat pump one cycle)
108+
is fine. If it is near zero, defer to the next solar window.
109+
110+
---
111+
112+
## MQTT Topics
113+
114+
| Topic | Unit | Retained | Description |
115+
|-------|------|----------|-------------|
116+
| `{base}/solar_surplus_wh` | Wh | No | PV overflow that cannot be stored in the battery |
117+
| `{base}/solar_active` | bool | No | `true` if solar is producing in slot 0 |
118+
| `{base}/pv_start_battery_wh` | Wh | No | Battery level at next net-charging crossover |
119+
| `{base}/forecast_min_battery_wh` | Wh | No | Minimum battery level over entire forecast horizon |
120+
121+
All values are published after each evaluation cycle (together with the
122+
inverter control decision). See [MQTT API](mqtt-api.md) for the full topic
123+
reference and configuration options.
124+
125+
### Home Assistant Auto-Discovery
126+
127+
The following HA entities are created automatically when
128+
`auto_discover_enable: true` is configured:
129+
130+
- **Solar Surplus** — sensor (energy, Wh)
131+
- **Solar Active** — binary sensor (on/off diagnostic)
132+
- **PV Start Battery** — sensor (energy, Wh)
133+
- **Forecast Min Battery** — sensor (energy, Wh)
134+
135+
---
136+
137+
## Use Cases
138+
139+
### Heat Pump Control
140+
141+
A heat pump is a flexible load: it can pre-heat a buffer tank or run an extra
142+
heating cycle when energy is cheap or free — but running it at the wrong time
143+
can deplete the battery before the next solar window.
144+
145+
The three metrics together answer the key questions for heat pump automation:
146+
147+
**"Can I run a heating cycle right now without net grid cost?"**
148+
149+
Check `solar_surplus_wh >= estimated_cycle_wh`. If yes, the PV will produce
150+
more than the battery can store anyway — running the heat pump consumes what
151+
would otherwise be exported. No additional grid draw over the forecast horizon.
152+
153+
**"Is the battery safe enough for a cycle tonight?"**
154+
155+
Check `pv_start_battery_wh`. A high value (e.g. > 2000 Wh) means the battery
156+
will still have a comfortable charge when solar starts tomorrow; the heat pump
157+
can run. A low value (< 500 Wh) means overnight consumption will nearly deplete
158+
the battery — defer to the next solar window.
159+
160+
**"Should I block flexible loads entirely?"**
161+
162+
Check `forecast_min_battery_wh == 0`. If the battery is expected to hit MIN_SOC
163+
at some point in the forecast, batcontrol may need to grid-charge later;
164+
flexible loads should wait.
165+
166+
A simple Home Assistant automation combining all three:
167+
168+
```yaml
169+
# Allow heat pump if PV will overflow OR battery is healthy and not forecast-constrained
170+
condition:
171+
- condition: or
172+
conditions:
173+
- condition: numeric_state
174+
entity_id: sensor.batcontrol_solar_surplus_wh
175+
above: 1500 # surplus covers one heat pump cycle
176+
- condition: and
177+
conditions:
178+
- condition: numeric_state
179+
entity_id: sensor.batcontrol_pv_start_battery_wh
180+
above: 1000 # enough battery charge at dawn
181+
- condition: numeric_state
182+
entity_id: sensor.batcontrol_forecast_min_battery_wh
183+
above: 0 # battery not forecast to run empty
184+
```
185+
186+
---
187+
188+
### EV Charging via evcc
189+
190+
[evcc](https://evcc.io) supports a `min_soc`/`target_soc` model as well as
191+
charging from PV surplus. The batcontrol metrics integrate naturally with evcc's
192+
MQTT API to let you charge the car only when it does not compete with the
193+
battery.
194+
195+
**Scenario: charge only from true PV overflow**
196+
197+
`solar_surplus_wh` tells you exactly how much energy the PV will produce above
198+
what the battery can absorb. Pass this value to evcc's `pv_action` or use it
199+
in an automation to set the evcc charging mode:
200+
201+
- `solar_surplus_wh > 0` → switch evcc to **PV** mode (charge from surplus)
202+
- `solar_surplus_wh == 0` and `forecast_min_battery_wh > 0` → switch to **Min+PV**
203+
(keep a minimum charge rate, fill up with PV where possible)
204+
- `forecast_min_battery_wh == 0` → switch to **Off** or **Min** only (battery
205+
needs the energy)
206+
207+
**Scenario: opportunistic overnight charge**
208+
209+
Use `pv_start_battery_wh` to decide whether to allow evcc to draw from the
210+
battery overnight. If the battery is forecast to still be above a threshold at
211+
dawn, a slow overnight charge (e.g. 6 A / 1.4 kW) will not noticeably affect
212+
the next day's solar cycle.
213+
214+
---
215+
216+
### General Pattern for Any Flexible Load
217+
218+
| Question | Metric to check | Threshold example |
219+
|---|---|---|
220+
| Is PV overflowing right now? | `solar_surplus_wh` | `> estimated_load_wh` |
221+
| Will battery survive the night? | `pv_start_battery_wh` | `> 500 Wh` |
222+
| Is the battery forecast-constrained? | `forecast_min_battery_wh` | `> 0` |
223+
224+
All three are dimensioned in Wh, so you can directly compare them against the
225+
energy consumption of the load you want to schedule.
226+
227+
---
228+
229+
## Implementation Notes
230+
231+
- All values are computed in `src/batcontrol/forecast_metrics.py` by the
232+
`ForecastMetrics` class.
233+
- Slot 0 is time-adjusted: the elapsed fraction of the current interval is
234+
subtracted so that a slot already 80% elapsed only contributes 20% of its
235+
forecast energy.
236+
- `net_consumption = consumption - production`; negative = battery charging,
237+
positive = battery discharging / grid draw.
238+
- `stored_usable_energy` is the energy above MIN_SOC. `free_capacity` is the
239+
space between the current level and MAX_SOC.
240+
- The slot-by-slot simulation clamps at both ends:
241+
`battery = max(0, min(stored_usable + free_capacity, battery - net))`.
242+
A simple net-sum over slots would overestimate available energy because it
243+
ignores that the battery cannot go below 0 or above MAX_SOC.

docs/integrations/mqtt-api.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,13 @@ Batcontrol publishes data to the following topic structure (assuming base topic
9595
- `house/batcontrol/reserved_energy_capacity` - Energy reserved for discharge in Wh
9696

9797
### Solar Surplus Information
98-
- `house/batcontrol/solar_surplus_wh` - Expected solar surplus energy in Wh (>0 means usable surplus available)
98+
99+
See [Forecast Metrics](forecast-metrics.md) for a detailed explanation of these values and how to use them for flexible load control.
100+
101+
- `house/batcontrol/solar_surplus_wh` - Expected PV overflow in Wh that cannot be stored in the battery
99102
- `house/batcontrol/solar_active` - Whether solar is currently producing (`true`/`false`)
100-
- `house/batcontrol/night_surplus_wh` - Expected battery surplus in Wh at start of next production window
103+
- `house/batcontrol/pv_start_battery_wh` - Battery level in Wh at the next net-charging crossover
104+
- `house/batcontrol/forecast_min_battery_wh` - Minimum battery level in Wh over the entire forecast horizon
101105

102106
### Configuration Limits
103107
- `house/batcontrol/always_allow_discharge_limit` - Always discharge limit (0.0-1.0)

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ nav:
6464
- Price Difference Calculation: features/price-difference-calculation.md
6565
- Integrations:
6666
- MQTT API: integrations/mqtt-api.md
67+
- Forecast Metrics: integrations/forecast-metrics.md
6768
- MQTT Inverter: integrations/mqtt-inverter.md
6869
- EVCC Connection: integrations/evcc-connection.md
6970
- Development:

0 commit comments

Comments
 (0)