UX-08 2d: plain log lines, the rest of the status page, and DOCS
checks / unit (pull_request) Successful in 8m55s
infra-pin / fresh (pull_request) Successful in 1s

Every WARNING and ERROR line is one plain line of at most 25 words, with
no card ids, capitals or semicolons; the reasoning of the ten longest is
on a DEBUG line. The ux08 allowlist is empty and its cap is 0 (43 -> 0).

The status page: the month-peak and peak-guard rows, the stale-reading
row, the top table (Why, Tariff, Arbitrage, Shadow mode, labels), the
Stopped banner and the line under the buttons, which now shows the plain
hint of each event instead of the raw action log. The Supervisor question
at start logs one line with a real reason.

Refusals are counted as shown (labels expanded, long cases driven), seven
were shortened, the card and parse errors lost their internals, a blank
value reads "empty", and the panel no longer files inverters.max_w under
the flat max_w. Option names lose "(JSON)", the NL texts are checked too.

DOCS gets a glossary (control board, isolator, Developer Tools) and card
ids move from headings to a "For installers" line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p
This commit is contained in:
2026-09-27 08:01:16 +02:00
co-authored by Claude Opus 5.5
parent a02cd4b713
commit 6a95447248
38 changed files with 1294 additions and 742 deletions
+164 -88
View File
@@ -7,45 +7,73 @@ balance cells and recalibrate its coulomb counter.
Installers: the printed field guide (`docs/GoodWe-RS485-Field-Guide.pdf`) is
v0.1.0 and outdated; where it disagrees with this page, this page wins.
## Words used on this page
- **Control board**: the small computer wired to the inverter's RS485 meter
bus (a T-CAN485 board with an ESP32 chip). It passes the add-on's power
request to the inverter. If the add-on goes quiet for about 30 seconds, it
sets the inverter to 0 W by itself.
- **Isolator**: the switch that cuts an inverter off from the mains. Switching
it off stops the inverter, whatever the add-on or the control board asks.
- **Developer Tools**: the Home Assistant page that lists every entity and its
current value, on its *States* tab. Use it to check that a sensor the add-on
reads is really there and updating.
- **Shadow mode**: the add-on makes every decision but sends nothing, so every
inverter stays at 0 W. Pages and notifications say what it *would* have done.
- **Safety stop**: the add-on has set one inverter, or all of them, to 0 W and
keeps it there until you start control on the add-on page.
- **Peak limit**: the 15-minute average grid use you want to stay under this
month, because your capacity tariff bills the highest one.
- **Peak guard**: the part of the add-on that uses the battery to keep each
15-minute period under the peak limit.
- **Peak reserve** and **battery cut-out level**: the charge the peak guard
keeps back, and the level where the battery switches itself off.
## Before you start
You need:
- A GoodWe **ES / BP family** inverter (AA55 / RS485 meter-bus generation)
- The vendor's meter-emulating controller **disconnected** from the bus
- A T-CAN485 (ESP32) flashed with `firmware/goodwe-master.yaml`
- A control board: a T-CAN485 (ESP32) flashed with
`firmware/goodwe-master.yaml`
- A grid-power sensor already working in Home Assistant, updating every ~5–10 s
After installing, turn on **Watchdog** on the add-on's Info tab. A crashed
control loop stops the add-on at 0 W, and the Supervisor restarts it only when
Watchdog is on (it is off by default). See
`DECISIONS/SAFETY-61-control-task-death.md`.
After installing, turn on **Watchdog** on the add-on's Info tab. If control
stops by accident, the add-on sets the inverters to 0 W and exits, and Home
Assistant starts it again only when Watchdog is on. It is off by default.
## The safety model, in short
The inverter **holds its last command forever** — it has no meter-timeout. So:
The inverter **keeps its last power request forever**. It has no timeout of its
own. So:
- The ESP32 commands 0 W if this add-on stops refreshing for ~30 s, and keeps
commanding it.
- This add-on commands 0 W when its inputs go missing, when you stop control,
and when it shuts down.
- This add-on commands 0 W **and latches there** if what it is commanding and
what the meter reads point opposite ways for three minutes — see "The
- The control board sets the inverter to 0 W if this add-on stops sending for
about 30 seconds, and keeps it there.
- This add-on sets the inverters to 0 W when a reading goes missing, when you
stop control, and when it shuts down.
- This add-on sets the inverters to 0 W **and keeps them there** if the power
goes the opposite way to what it asks for three minutes. See "The
sign-disagreement failsafe" below.
- The **optional RS485 e-stop** is the only thing that covers this machine
dying. Without it, a failed host leaves the battery latched at its last
command until someone intervenes.
- This add-on **raises an alarm — and nothing more —** when the meter keeps
answering with a reading that never changes while the command sits at the
rail. See "The frozen-meter alarm" and, before you rely on it, "Capacity-tariff
exposure of the grid-following loop".
- The **optional emergency stop** is the only thing that covers this computer
failing. Without it, the battery keeps its last power request until someone
steps in.
- This add-on **raises an alarm, and does nothing more,** when the meter keeps
answering with a value that never changes while the battery is asked for
full power. See "The frozen-meter alarm" and, before you rely on it,
"Capacity-tariff exposure of the grid-following loop".
**If anything looks wrong: stop the add-on.** That commands 0 W and the
hardware holds it there.
**If anything looks wrong: stop the add-on.** That sets the inverters to 0 W,
and the control board keeps them there.
*For installers: a crashed control task is SAFETY-61, see
`DECISIONS/SAFETY-61-control-task-death.md`.*
## Configuration
### ⚠️ There are TWO places to configure this add-on (UX-02)
### ⚠️ There are TWO places to configure this add-on
*For installers: card UX-02.*
Since **UX-02** the ingress panel has a **Configuration** page of its own,
beside Home Assistant's *Configuration* tab. Both edit the **same** settings —
@@ -108,7 +136,9 @@ One column, big targets, sections collapsed until you open them, and the
refusals pinned where you cannot scroll past them. It is the screen for
standing in front of the inverter.
### What the Configuration tab actually shows you (UX-01)
### What the Configuration tab actually shows you
*For installers: card UX-01.*
Since **UX-01** the add-on ships `translations/en.yaml` and `translations/nl.yaml`,
so Home Assistant's *Configuration* tab renders human labels and a description
@@ -179,10 +209,10 @@ The label and description each setting shows on the Configuration tab, in tab or
| Tariff | Supplier markup (EUR/kWh) | `price_markup_eur_kwh` | Your supplier's margin. It is added only when the price source does not already include it. |
| Tariff | Distribution, levies and VAT (EUR/kWh) | `price_taxes_eur_kwh` | Grid costs, levies and VAT per kWh, preset for Flanders. Check it against your own grid operator's rates. |
| Tariff | Injection price (EUR/kWh) | `export_price_eur_kwh` | What you are paid per kWh fed into the grid, all in. It may be negative. |
| Tariff | Fixed or day/night tariff (JSON) | `static_profile` | Your fixed or day/night contract as one line of JSON, prices all in. Fill it in even on a dynamic contract, as the fallback when the price feed fails. |
| Tariff | Day/night levies and VAT (JSON) | `price_taxes_profile` | Leave it empty unless your grid operator charges a different day and night rate, as in Wallonia and Brussels. Set this or the flat figure above, not both. |
| Tariff | Your own tariff formula (JSON) | `price_formula` | For custom_formula only. Type the numbers from your supplier's tariff card as one line of JSON, and the add-on checks them against the card. |
| Tariff | Your own injection formula (JSON) | `price_export_formula` | Optional. The injection formula from your tariff card, in the same shape as your own tariff formula. Leave it empty to use the flat injection price. |
| Tariff | Fixed or day/night tariff | `static_profile` | Your fixed or day/night contract on one line, prices all in, as the documentation shows. Fill it in even on a dynamic contract, as the fallback. |
| Tariff | Day/night levies and VAT | `price_taxes_profile` | Leave it empty unless your grid operator charges a different day and night rate, as in Wallonia and Brussels. Set this or the flat figure above, not both. |
| Tariff | Your own tariff formula | `price_formula` | For custom_formula only. Type the numbers from your supplier's tariff card on one line, as the documentation shows. The add-on checks them against the card. |
| Tariff | Your own injection formula | `price_export_formula` | Optional. The injection formula from your tariff card, in the same shape as your own tariff formula. Leave it empty to use the flat injection price. |
| Tariff | Price CSV file | `price_csv_path` | For manual_csv only. Where in /share the add-on reads your price file. |
| Tariff | Price feed URL override | `price_feed_url` | Leave it empty to use the source's own feed. An override is sent exactly as typed and often returns only one day. |
| Tariff | Price look-ahead (hours) | `price_horizon_h` | How many hours of prices to ask the source for. |
@@ -218,9 +248,9 @@ The label and description each setting shows on the Configuration tab, in tab or
| Advanced | Ignore errors below (W) | `deadband_w` | Errors smaller than this are ignored. Keep it small, since any resting point inside it holds. |
| Advanced | Meter resting point (W) | `target_grid_w` | What the meter should show when nothing happens. A slight export avoids paying for tiny imports all day. |
| Advanced | Command rounding (W) | `step_w` | The command is rounded to a multiple of this, so noise does not change it every cycle. |
| Advanced | Inverter at limit gap (W) | `saturation_w` | When an inverter delivers this much less than asked, it counts as at its limit. The per-inverter wrong-direction threshold is half of this. |
| Advanced | At-limit shortfall (W) | `saturation_w` | When an inverter delivers this much less than asked, it counts as at its limit. The per-inverter wrong-direction threshold is half of this. |
| Advanced | Cycles at limit before freezing | `saturation_cycles` | How many cycles in a row at the limit before control stops building up error. Do not set it to 1. |
| Advanced | Error build-up limit (W) | `integrator_max_w` | Caps the error that control saves up from past cycles. 0 means “Inverter rating”. Do not raise it above the rating. |
| Advanced | Catch-up limit (W) | `integrator_max_w` | Limits how hard control keeps pushing while the battery falls behind. 0 means “Inverter rating”. Do not raise it above the rating. |
| Advanced | Command refresh interval (s) | `heartbeat_s` | How often the command is sent again even when unchanged. Keep it well under the control board's own 30 s limit. |
| Advanced | Missing inputs time limit (s) | `stale_input_s` | How long inputs may be missing before every inverter is set to 0 W. With a P1 meter it starts after “Reading too old after”. |
| Advanced | Longest time between cycles (s) | `cycle_floor_s` | A deadline between control cycles that a healthy meter never reaches. Raise it only for a meter slower than about 10 s. |
@@ -246,7 +276,7 @@ When the add-on refuses to start, each reason names the setting by its label and
- **Peak reserve** (`peak_soc_floor`): derived, not picked (SAFETY-24). The reference battery holds 13.8 kWh usable and the worst modelled peak day asks for 878 Wh, or 6.4 %. 20 % sits ten points above the cut-out level and leaves about 1.6 worst days of room. It bounds peak limiting only: ordinary use goes on down to the cut-out level. It must stay above “Battery cut-out level”.
- **Dynamic price source** (`price_provider`): selecting a provider is what starts the fetch, every half hour, whether or not trading is on. It may not be combined with the two older price entities. engie_dynamic, totalenergies_dynamic and eneco_eddy are withdrawn and refuse to start: their endpoints never existed. They stay in the list only so such a site gets that explanation.
- **Supplier markup** (`price_markup_eur_kwh`): under manual_csv, whose prices are all in, it is ignored and logged as ignored.
- **Distribution, levies and VAT** (`price_taxes_eur_kwh`): 0.11736 EUR/kWh, derived in TARIFF-18 from the Eneco residential card for Flanders of September 2026 with 6 % VAT: Fluvius distribution for Antwerpen 5,35 plus excise 4,876 plus green power and CHP levy 1,51 c/kWh. Check it against your own DSO: across the eight Fluvius areas distribution runs from 5,28 to 6,70 c/kWh, so the Flemish total runs from about 0.117 to 0.131. Changing it moves no trading decision, because the same amount lands on every hour. It does move every price shown, and the rule that refuses to draw the battery down into an hour priced below zero. See [What `price_taxes_eur_kwh` is made of](#what-price_taxes_eur_kwh-is-made-of-and-why-yours-is-different-tariff-18).
- **Distribution, levies and VAT** (`price_taxes_eur_kwh`): 0.11736 EUR/kWh, derived in TARIFF-18 from the Eneco residential card for Flanders of September 2026 with 6 % VAT: Fluvius distribution for Antwerpen 5,35 plus excise 4,876 plus green power and CHP levy 1,51 c/kWh. Check it against your own DSO: across the eight Fluvius areas distribution runs from 5,28 to 6,70 c/kWh, so the Flemish total runs from about 0.117 to 0.131. Changing it moves no trading decision, because the same amount lands on every hour. It does move every price shown, and the rule that refuses to draw the battery down into an hour priced below zero. See [What `price_taxes_eur_kwh` is made of](#what-price_taxes_eur_kwh-is-made-of-and-why-yours-is-different).
- **Day/night levies and VAT** (`price_taxes_profile`): for Wallonia and Brussels, where distribution has a day and a night rate (ORES 13,27 against 7,39 c/kWh, AIESH 15,17 against 8,21, Sibelga 9,39 against 7,10). The figures are levies alone, added to the market price. The add-on refuses to start if it and the flat figure disagree. See [Levies and VAT by time of day](#levies-and-vat-by-time-of-day-price_taxes_profile).
- **Injection price** (`export_price_eur_kwh`): one number because no supplier here publishes a dynamic injection curve. In Belgium it is often near zero, and at a sunny midday it can be negative.
- **Fixed or day/night tariff** (`static_profile`): the periods must cover every day type exactly once with no gap and no overlap, and a gap is refused rather than filled. A window 22:00-07:00 on mon-fri does not cover Saturday morning, so a weekend row is required. See [Static day/night and fully-fixed tariffs](#static-daynight-and-fully-fixed-tariffs-static_profile).
@@ -259,19 +289,19 @@ When the add-on refuses to start, each reason names the setting by its label and
- **Draw-down and recharge rate** (`maintenance_discharge_w`, `maintenance_charge_w`): the draw-down rate above the rating is cut back, not refused. A recharge rate above the rating would ask for more than the limit allows and resolve to 0 W, so maintenance would never charge. That is why the add-on refuses it.
- **Draw down to** (`maintenance_soc_floor`): the monthly cycle goes this deep on purpose, while “Peak reserve” is a depth peak limiting never reaches. 11 % against 20 % is why they are two settings.
- **Wrong-direction threshold and time** (`sign_disagree_w`, `sign_disagree_s`): measured, not chosen (SAFETY-02). On the captured healthy day (2026-08-23) signs agree on 93.4 % of gated samples, on the fault day (2026-08-20) 0.0-10.5 %, with opposed runs up to 1752 s. The longest legitimate opposed run is 30 s, and the fault day still trips 13 times at 300 s, so 180 s sits in a wide gap. The threshold must stay below “Inverter rating”.
- **Per-inverter wrong-direction threshold and time** (`unit_sign_disagree_w`, `unit_sign_disagree_s`): two or more inverters only. Derived from 1890 closed-loop test runs (SAFETY-30) with nothing faulty. 250 W is half the at-limit gap. 85 s is 8.5 times the worst legitimate opposition (10.0 s) and 2.8 times the control board's 30 s link limit.
- **Per-inverter wrong-direction threshold and time** (`unit_sign_disagree_w`, `unit_sign_disagree_s`): two or more inverters only. Derived from 1890 closed-loop test runs (SAFETY-30) with nothing faulty. 250 W is half the at-limit shortfall. 85 s is 8.5 times the worst legitimate opposition (10.0 s) and 2.8 times the control board's 30 s link limit.
- **Site wrong-direction threshold** (`site_sign_disagree_w`): the single-inverter 300 W carried across deliberately (SAFETY-30), because near zero the sign is noise whatever is behind the one meter.
- **Site wrong-direction time** (`site_sign_disagree_s`): derived from the same 1890 runs (SAFETY-30). The worst legitimate opposition at 300 W is 110 s with one inverter and 215 s with two, so 180 s would false-trip. 325 s is 1.51 times that and inside the 900 s billing period.
- **Stuck meter alarm after** (`meter_freeze_s`): measured, not chosen (SAFETY-18). Over 2399 s of this house at its operating point, the longest run without a new reading is 15.4 s, and 300 s is 19.5 times that. It raises an alarm only and does not set 0 W.
- **Opposing inverters threshold** (`circulation_w`): measured on 924 closed-loop test runs (SAFETY-31). The worst legitimate opposition drops from 505 s at 100 W to 5 s at 400 W, the lowest value accepted. 700 W is 1.75 times that knee, with margin because the second inverter is still a copy of the first.
- **Opposing inverters time** (`circulation_hold_s`): derived (SAFETY-31). 75 s is fifteen control cycles, 15 times the worst legitimate episode, and 2.5 times the control board's 30 s link limit.
- **Not delivering alarm after** (`delivery_hold_s`): two or more inverters only. It reports only (SAFETY-42): nothing is commanded differently. Derived from 2640 closed-loop test runs with nothing faulty. The worst legitimate episode is 35.0 s and 185 s is 5.29 times that. The schema allows 30 s, below that, so expect false reports there. A battery at or below its own cut-out level is never reported. Re-derive with admin/goodwe `sim/delivery_window.py --campaign --blag`.
- **Shadow mode** (`shadow_mode`): the whole decision path runs and nothing reaches the inverter. The status page and action log show what would have been sent. A safety stop that is already up stays up. See [Shadow mode](#shadow-mode-shadow_mode-safety-10).
- **Shadow mode** (`shadow_mode`): the whole decision path runs and nothing reaches the inverter. The status page and action log show what would have been sent. A safety stop that is already up stays up. See [Shadow mode](#shadow-mode-shadow_mode).
- **Control gain** (`gain`): measured on real hardware and already at the limit.
- **Largest change per cycle** (`slew_w`): 0 means follow the rating: about a ninth of it, derived (SAFETY-39) from a measured ~18 s step response against a 2 s meter tick. A fixed value faster than the rating allows is refused. At 1000 W against a 5000 W rating control swung the meter from 3.4 kW export to 2.8 kW import on 2026-09-05.
- **Ignore errors below, Meter resting point** (`deadband_w`, `target_grid_w`): the meter's import counter bills every positive watt, so a resting point at 0 W leaks about half the deadband as billed import, roughly 0.2-0.35 kWh a day at 15 W. -10 W moves that leak to export.
- **Inverter at limit gap** (`saturation_w`): the per-inverter wrong-direction threshold is derived as half of it, so moving this moves that argument too.
- **Error build-up limit** (`integrator_max_w`): measured on the closed-loop simulator with a 4000 W load dropped to zero: at a limit equal to the rating the command is 1000 W two cycles later, at 1.5 times the rating 1800 W. Lowering it is the useful direction.
- **At-limit shortfall** (`saturation_w`): the per-inverter wrong-direction threshold is derived as half of it, so moving this moves that argument too.
- **Catch-up limit** (`integrator_max_w`): measured on the closed-loop simulator with a 4000 W load dropped to zero: at a limit equal to the rating the command is 1000 W two cycles later, at 1.5 times the rating 1800 W. Lowering it is the useful direction.
- **Command refresh interval** (`heartbeat_s`): the control board refuses a link that has been quiet for 30 s.
- **Longest time between cycles** (`cycle_floor_s`): keep it under “Reading too old after”, since past that there is no reading left to act on.
@@ -286,9 +316,9 @@ When the add-on refuses to start, each reason names the setting by its label and
| `batt_invert` | | Flip if needed |
| `capacity_kwh` | | This pack's usable capacity. Read only to divide the site target between packs by **energy** headroom — see below |
| `setpoint_entity` | yes | The ESPHome `number.*_goodwe_setpoint_w` |
| `pv_entity` | | Optional. **One** solar inverter's power sensor, `+` = producing. Measurement only — it changes no control behaviour. See [PV production](#pv-production-pv_entity-tel-11) |
| `pv_entities` | | Optional. **Several** solar inverters, one row each — use this instead of `pv_entity` when your roof has more than one. Setting it **ignores** `pv_entity`. See [Several PV inverters](#several-pv-inverters-pv_entities-tel-12) |
| `pv_invert_entities` | | Optional. The PV entities above that read **negative** while producing; each one's sign is flipped. See [The sign](#the-sign-and-pv_invert_entities-tel-14) |
| `pv_entity` | | Optional. **One** solar inverter's power sensor, `+` = producing. Measurement only — it changes no control behaviour. See [PV production](#pv-production-pv_entity) |
| `pv_entities` | | Optional. **Several** solar inverters, one row each — use this instead of `pv_entity` when your roof has more than one. Setting it **ignores** `pv_entity`. See [Several PV inverters](#several-pv-inverters-pv_entities) |
| `pv_invert_entities` | | Optional. The PV entities above that read **negative** while producing; each one's sign is flipped. See [The sign](#the-sign-and-pv_invert_entities) |
Use the ESP32's readings rather than the inverter's cloud or dongle sensors:
those serve cached values, and a stale reading here ends the maintenance charge
@@ -344,7 +374,7 @@ inverter's own **AC** power. If it is DC the AC-side ceiling is nearer 4324 W.
| `soc_entity` | | This pack's state of charge |
| `batt_entity` | | This unit's battery power, `+` = discharging |
| `batt_invert` | | Flip if needed |
| `capacity_kwh` | | This pack's usable capacity. State it on every entry or on none — see [OPT-09](#how-the-site-target-is-divided-between-the-packs-opt-09) |
| `capacity_kwh` | | This pack's usable capacity. State it on every entry or on none — see [OPT-09](#how-the-site-target-is-divided-between-the-packs) |
⚠️ **Nothing is inherited.** An entry that leaves out `soc_entity` has **no SoC
sensor** — it does *not* fall back to the flat one. That is deliberate: every
@@ -387,7 +417,9 @@ in the log rather than silent, but it is an outage until someone puts the
single-inverter entity back in that field. **Before downgrading, write down the
`setpoint_entity` of the unit you want to keep running.**
#### How the site target is divided between the packs (OPT-09)
#### How the site target is divided between the packs
*For installers: card OPT-09.*
Two packs do not get half each **from 0.8.0 onwards**. ⚠️ **On 0.7.0 and every
version before it the split is a fixed 50/50 regardless of what either pack can
@@ -508,8 +540,9 @@ accumulator is doing:
| `not-running` | control is stopped **and** shadow mode is off, so the loop did not run |
⚠️ **`Loop reason` is not the `Why` row on the status page, and the two will
often disagree.** `Why` is the *arbiter's* sentence — `loop -> -1200 W` — which
names the strategy that won and the watts it won with. It has never carried the
often disagree.** `Why` is the *arbiter's* answer in words — *Keeping the grid
near zero: -1 200 W* — which names the strategy that won and the watts it won
with. The log keeps the arbiter's own `loop -> -1200 W`. It has never carried the
strategy's own reason, so a loop that is slew-limited or frozen says so **only**
here.
@@ -567,10 +600,12 @@ and `Inverter B` — are two names to the validator and one id once slugged. The
add-on gives the second one a `_2` suffix so nothing collides, but the ids you
get then depend on roster order. Give your inverters plainly different names.
### PV production (`pv_entity`, TEL-11)
### PV production (`pv_entity`)
*For installers: card TEL-11.*
> **More than one solar inverter?** Use `pv_entities` instead — see
> [Several PV inverters](#several-pv-inverters-pv_entities-tel-12) below. The
> [Several PV inverters](#several-pv-inverters-pv_entities) below. The
> rest of this section applies to both.
@@ -604,7 +639,9 @@ Only the third is a software defect, and **none of them can be told apart
without a record of production.** This entity is that record. The attribution
itself is OPT-10's, with a week of history behind it.
#### The sign, and `pv_invert_entities` (TEL-14)
#### The sign, and `pv_invert_entities`
*For installers: card TEL-14.*
**Positive must mean producing.** Some sensors — typically a kWh meter or an
ESPHome power clamp in the PV circuit — read negative while producing and a few
@@ -642,7 +679,9 @@ pictures, which is exactly the distinction the whole card exists to preserve.
A failed read also **clears** the previous good one: the value never outlives
its own sensor.
### Several PV inverters (`pv_entities`, TEL-12)
### Several PV inverters (`pv_entities`)
*For installers: card TEL-12.*
A multi-string or multi-orientation roof routinely carries **two or three PV
inverters**, each publishing its own power sensor into Home Assistant. List them
@@ -1025,7 +1064,9 @@ behind it would trip the firmware watchdog on a system that is working fine.
| `delivery_hold_s` | 185 | **Two or more inverters only.** How long one inverter may stand at a discharge share it is not producing - short by more than `saturation_w`, with no reason this add-on can read - while at least one sibling is **not** in that state, before it is named. **It reports; it does not act.** Nothing is commanded differently and the site goes on running short by that unit's share |
| `shadow_mode` | `auto` | **Run every decision and command nothing.** `auto` = shadow mode while control is stopped, ordinary control once you press Start; `on` = shadow mode always; `off` = ordinary control. See *Shadow mode* below |
#### Shadow mode (`shadow_mode`, SAFETY-10)
#### Shadow mode (`shadow_mode`)
*For installers: card SAFETY-10.*
Shadow mode runs the **whole** decision path — the price strategy, the capacity
guard, the maintenance shaping, the grid-following loop, the arbiter that ranks
@@ -1192,11 +1233,13 @@ If the setpoint this add-on is emitting and the grid figure it is regulating on
point **opposite ways** — charging the battery while the house imports, or
discharging it while the house exports — with both over `sign_disagree_w`, for
`sign_disagree_s` **continuously**, the add-on commands **0 W** and holds it
there. The banner reads *Command and meter point opposite ways*, the add-on
there. The banner reads *The meter saw power go the wrong way*, the add-on
log says which two numbers disagreed and for how long, and the `Controller
status` entity reports `failsafe`.
#### ⚠️ At two or more inverters this is TWO checks, not one (SAFETY-30)
#### ⚠️ At two or more inverters this is TWO checks, not one
*For installers: card SAFETY-30.*
The paragraph above describes what a **one-inverter** install does, and it is
unchanged: there is one meter, one actuator, and comparing the command against
@@ -1285,7 +1328,9 @@ agreement is 93.4 %; on the four captured fault clusters it is 0.3 %, 0.9 %,
says so.
#### ⚠️ A unit that stops DELIVERING is a third fault again (SAFETY-42)
#### ⚠️ A unit that stops DELIVERING is a third fault again
*For installers: card SAFETY-42.*
The two checks above both key on **direction**. A unit sitting at **0 W against
a non-zero share** has no direction: it is not pushing the wrong way, it is
@@ -1393,7 +1438,9 @@ correct toward that wrong value instead of holding a stale command. It is bounde
happened to a lying meter whose value merely jittered — bit-exact repetition was never a
safety property.
#### When the freeze lets go, and when it deliberately never does (SAFETY-13)
#### When the freeze lets go, and when it deliberately never does
*For installers: card SAFETY-13.*
While the loop is frozen the command's **magnitude may fall but never rise**. That is
the anti-windup rule, and at `prev_w == 0` it reads as a refusal to *start*: with
@@ -1668,8 +1715,9 @@ exactly like the SoC drift the monthly maintenance cycle exists to correct.
capacity-peak guard will not spend the pack for money below 20 %"*, and
**nothing more**. See below for what that costs on a real evening.
- **When it binds you are told**, once per episode, with notification
`goodwe_peak_soc_floor` and a status-page row reading `PEAK GUARD
FLOOR-LIMITED`: the quarter **will** go over the ceiling, so shedding load is
`goodwe_peak_soc_floor` and a red status-page row reading *Battery at its
peak reserve* (`PEAK GUARD FLOOR-LIMITED` in the log): the quarter **will**
go over the ceiling, so shedding load is
the only thing left that changes the bill. Failing to shave is never silent —
the euro figure is the whole point of the guard.
- **An unreadable SoC is treated as floor-limited, not as a full battery.** A
@@ -1685,7 +1733,9 @@ Why 20 %: the reference pack is 13.8 kWh usable, so 10 points of margin over the
BMS cut-out is about 1.6× the guard's worst measured day. If your pack is
smaller, or your quarters are worse, raise it.
#### ⚠️ What `peak_soc_floor` does **not** protect (SAFETY-25)
#### ⚠️ What `peak_soc_floor` does **not** protect
*For installers: card SAFETY-25.*
**On a sustained-load evening this add-on will take your battery to the BMS
cut-out, whatever `peak_soc_floor` says.** That is measured, not argued. Replayed
@@ -1878,7 +1928,9 @@ unchanged is 12 W, which is inside the 15 W deadband — so the command there is
parked at 0 W and the two halves of the rule cannot both be true. At the schema
floor of 60 s that figure is 60 W and only the margin is left.
### The circulating-power alarm (SAFETY-31)
### The circulating-power alarm
*For installers: card SAFETY-31.*
> **Only at two or more inverters.** At the shipped roster of one this quantity
> is identically zero for every reading, no claim is ever filed, and the status
@@ -2345,7 +2397,9 @@ distinct state from "prices, all of them zero": the plan is marked provisional
and arbitrage is disabled, per the degradation matrix. It never falls back to a
price of zero, which would read as free electricity.
#### The hours the feed has not published yet (FCST-04)
#### The hours the feed has not published yet
*For installers: card FCST-04.*
A day-ahead auction publishes tomorrow's prices once a day, at **14–16 h CET**.
Before that, the feed is serving today and nothing else — so a `price_horizon_h`
@@ -2388,29 +2442,28 @@ firm/provisional split whenever that split is what refused a trade. And
since TARIFF-11 that row carries the provider plan itself:
```
Tariff: no static profile (recomputed this tick); provider plan fetched 412 s
ago for a 48 h horizon: dynamic_dayahead from generic_dayahead,
60 points over 33 h, 36 firm / 24 carried, 900+3600 s spacing,
provisional
Tariff: No fixed tariff set. Price plan from 412 s ago, for 48 hours ahead:
day-ahead prices from the price feed, 60 prices over 33 hours,
36 published and 24 estimated, 900+3600 s apart, not final yet
```
⚠️ **The two halves of that row are not computed at the same moment, and the
row says so.** Everything before the semicolon is the static-profile or Home
row says so.** Everything before "Price plan" is the static-profile or Home
Assistant price-entity sentence, regenerated on the **30 s tick** from options
alone — no network. Everything after it is the **background refresh's**
resolution, which runs every 1800 s and is served for up to 5400 s before it is
retired; `fetched N s ago` is the age of the fetch that produced it and is the
retired; `Price plan from N s ago` is the age of the fetch that produced it and is the
only figure in the row that tells you whether the feed is still answering. On a
site with a provider configured and no usable plan the row says that too,
rather than falling silently back to the static half.
Read the plan half as five separate facts: which *product* the feed publishes
and which *plugin* read it, how many **points** it carries and how many **whole
hours** those points cover, how many of the points are **firm** (published for
their own interval) against **carried** (filled from yesterday under §8.3), the
**spacing** of the points in seconds — `900+3600` is the normal mixed steady
state, quarter-hourly firm points beside hourly carried ones — and `provisional`
whenever any point is carried.
and whether it came from the feed or from your fixed tariff, how many
**prices** it carries and how many **whole hours** they cover, how many are
**published** (firm, for their own interval) against **estimated** (carried
from yesterday under §8.3), how far **apart** they are in seconds — `900+3600`
is the normal mixed steady state, quarter-hourly firm points beside hourly
carried ones — and `not final yet` (provisional) whenever any is estimated.
⚠️ **Points and hours are two different numbers and this row used to print one
of them under the other's name.** `words` was `f"… {len(points)} h"`, so a
@@ -2585,7 +2638,9 @@ uses the result as a cheap-window hint for the maintenance charge. They are
— configure two of them and the add-on refuses to start, naming the conflict,
because the add-on reads exactly one and the loser is silent. Use one.
### The three buckets — which price components can move a decision (TARIFF-17)
### The three buckets — which price components can move a decision
*For installers: card TARIFF-17.*
**Read this before adding a price component of any kind.** The optimiser reads
exactly two quantities off a price series, and nothing else:
@@ -2641,7 +2696,9 @@ Three consequences worth having in mind:
is the only ORDER component the add-on has. Anything added next belongs in one
of the three, and saying which is part of adding it.
### What `price_taxes_eur_kwh` is made of, and why yours is different (TARIFF-18)
### What `price_taxes_eur_kwh` is made of, and why yours is different
*For installers: card TARIFF-18.*
**It ships at `0.11736` EUR/kWh.** That is the all-in non-energy part of a
Flemish residential bill — everything on your invoice that is *not* the energy
@@ -3095,8 +3152,8 @@ does not protect*.
P1 meter, so every other guard, which reads that net figure, stays quiet. This
has happened on this hardware once for 6 h 25 min. The alarm is latched,
survives restarts, and is raised again every 5 min until control is started.
The watts and the time window are in the log; the *Circulating power check*
row shows the live watts. See *The circulating-power alarm*.
The watts and the time window are in the log. The *Check for inverters working
against each other* row shows the live watts. See *The circulating-power alarm*.
**An inverter is not delivering.** SAFETY-42. The unit stood well short of its
share of the site target for longer than `delivery_hold_s`. A unit at 0 W is
@@ -3114,16 +3171,19 @@ and command figures are in the log. See *The frozen-meter alarm*.
## The Web UI
The ingress panel shows live values, why the controller is commanding what it
is, and a **Commissioning** checklist that names any problem in words. It also
carries the three buttons: start/stop control, force a maintenance cycle, and
abort one.
The add-on page shows live values, why the battery does what it does, and a
**Commissioning** checklist that names any problem in words. It also has three
buttons: start or stop control, force a maintenance cycle, and abort one.
Under the buttons is the last thing that happened, in a few words.
Since **UX-02** it has a second page, **Configuration** — see *There are TWO
places to configure this add-on*, above. ⚠️ **It adds nothing you can do to the
inverter.** The configuration page reads and writes the add-on's own settings
and can restart the add-on; the set of commands that reach the hardware is
exactly the three buttons it always had.
It has a second page, **Configuration**. See *There are TWO places to configure
this add-on*, above. **It adds nothing you can do to the inverter.** It reads
and writes the add-on's own settings and can restart the add-on. Only the three
buttons reach the inverter.
*For installers: the Configuration page is UX-02. The line under the buttons is
`messages.hint()`, the plain form of the newest entry of the add-on's own
action log (`Controller.events`), which the page no longer shows.*
## Status page
@@ -3144,14 +3204,16 @@ ends. That row says how to clear it.
| Meter | The grid reading, its age, how many readings were refused, and how long its value has not changed. |
| Stuck-meter check | Whether the add-on can warn you about a stuck meter. |
| Meter reading may be stuck | See *Meter reading may be stuck* under Notifications. |
| *month*: peak … | This month's peak, and how close it is to your peak limit. |
| Peak guard, Load too high for battery, Battery at its peak reserve | What the add-on does to keep your peak down. |
| Month peak | This month's highest 15-minute average, when it happened, and how close it is to your peak limit. Amber from 80 %. |
| 15-minute periods measured this month | How many 15-minute periods the add-on has measured and saved this month. |
| Peak guard, Load too high for battery, Battery at its peak reserve | What the add-on does to keep your peak down. *Idle, heading for …* is where this 15-minute period is heading. *Waiting for meter readings* after a restart means the battery does not charge until the add-on can measure the period again. |
| Battery level, Battery power, Grid power | The reading of that entity. *No usable reading* means the entity is missing, or its reading is too old. Check it in Developer Tools before replacing it. |
| Inverter control | Whether the inverter can be controlled, and the power range it accepts. |
| Inverter control | Whether the inverter can be controlled, and how much it can charge and give. |
| *name*: battery power reading stopped changing | The reading did not change while the add-on asked for a change. It may be stuck, or the battery may be refusing. The add-on sets the inverters to 0 W until it changes. |
| *name*: … cannot be controlled, commands refused, “Setpoint entity” is not set | Whether each inverter still takes commands. |
| Power request too high, Battery is not keeping up | See those titles under Notifications. Each shows how often it happened since the add-on started, and *fine now* once it has cleared. In shadow mode the row says what *would* have been sent. |
| Direction check, Site direction check | Whether each inverter's power goes the way it is told. *Opposite for 12 of 60 seconds* counts toward a stop. When it stops, the inverter, or every inverter, is set to 0 W until you start control. |
| Circulating power check, Inverters worked against each other | Only with several inverters. See *Inverters working against each other*. |
| Direction check, Site direction check | Whether each inverter's power goes the way the add-on asks. *Opposite for 12 of 60 seconds* counts toward a stop. When it stops, the inverter, or every inverter, is set to 0 W until you start control. |
| Check for inverters working against each other, Inverters worked against each other | Only with several inverters. See *Inverters working against each other*. |
| Delivery check, *name* is not delivering | Only with several inverters. See *An inverter is not delivering*. |
| Solar production | Your solar production. Sources are numbered in the order of “Solar production entities, per inverter”. A strongly negative reading by day means that entity reads the wrong way round: add it to “Reversed-sign solar entities”. |
@@ -3176,7 +3238,17 @@ ERROR or WARNING line in the add-on log.
***name*: “Setpoint entity” is not set** (amber): a roster entry nobody can
write, so the site refuses to run at part strength and holds 0 W.
- **Direction check, Site direction check** are the sign checks (SAFETY-02/30).
**Circulating power check** is SAFETY-31, **Delivery check** SAFETY-42.
**Check for inverters working against each other** is SAFETY-31, **Delivery
check** SAFETY-42.
- **Month peak** and **15-minute periods measured** are TARIFF-01 and
TARIFF-13. The store's state at start is the INFO line `capacity-tariff peak
store: …`, and the peak guard's full reason is on its log line.
- ***name*: battery power reading stopped changing** is SAFETY-37/56/57. The
ERROR line carries the full `STALE_READING` reason and when it clears.
- The table at the top: **Why** is `messages.why()`, the arbiter's answer in
words (`Resolution.explain()` stays on the log). **Tariff**, **Price
history**, **Arbitrage** and **Shadow mode** keep every figure they had
(TARIFF-11), in words.
- **Solar production** reads `pv_entity` or `pv_entities` (TEL-11/12). The
*Reversed-sign solar entities* setting is `pv_invert_entities` (TEL-14).
- **Shadow mode** (SAFETY-10): the banner turns red when a latch is up
@@ -3190,7 +3262,9 @@ battery power, circulating power, state of charge, maintenance phase and
controller status by MQTT discovery. This is observability only — the controller works fine without a
broker, and MQTT problems can never affect control.
## Before you commission inverter B: `tools/rehearse_roster.py` (ARCH-02)
## Before you commission inverter B
*For installers: card ARCH-02. The tool is `tools/rehearse_roster.py`.*
The day the `inverters:` roster is declared, two things change that are
**one-way on a live box and invisible until after the restart that applies
@@ -3371,7 +3445,9 @@ maintenance bookkeeping — and if a cycle ran during it, correct
`unit/<first>/last_completed` by hand before advancing again.** This is a stated
decision, not a discovery made at the inverter.
## Before you deploy: `tools/predeploy_check.py` (PKG-09)
## Before you deploy
*For installers: card PKG-09. The tool is `tools/predeploy_check.py`.*
Two ways a deploy has gone wrong on this system, both of which the Supervisor
reports as success: