|
| 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. |
0 commit comments