TEL-12: several PV inverters, and one absent member is an absent TOTAL
checks / unit (pull_request) Successful in 7m59s
checks / unit (pull_request) Successful in 7m59s
0.11.0 shipped `pv_entity` as a single string. The reference site's owner reported within minutes of it going live that they have several PV inverters and could configure one - which is ordinary for a multi-string roof, not a property of one house. `pv_entities` is the roster and the row carries the SITE total. ⚠️ THE HARD PART IS ABSENCE, NOT THE LIST. Three configured, one `unavailable`, and the sum of the other two is a SMALLER NUMBER INDISTINGUISHABLE FROM A REAL ONE. Read back by OPT-10 that says "there was less PV than there was" - the "no irradiance" conclusion the channel exists to TEST. So `pv.total()` answers `None` the moment any configured entity is absent: the topic carries HA's own `None`, the entity goes to `unknown`, the recorder records a GAP. The alternative (the partial sum beside a contributor count) was implemented and run as mutation N1b; it loses because the MQTT row is read COLD a week later by an attribution that would believe it, while the qualifying count would live in a second entity with its own history. The partial information is not thrown away - the STATUS PAGE names every entity that reported, with its watts, and every one that did not, with its reason, because that surface is read LIVE by a human who wants the diagnostic. ⚠️ AND THE MIRROR: a member honestly reporting 0 W is SUMMED. The test is `is None`, never falsiness - a roster that blanked the site on a zero would report an absence on every clear evening. THE SURFACE IS MEASURED, NOT PREFERRED. `pv_entity` could not become a list: tools/supervisor_shape_probe.py §G shows Supervisor 2026.08.0 answering an 0.11.0 install's stored string with `Invalid list for option 'pv_entity'`, and deepmerge's list strategy is `override` so a `[]` default does not rescue it - the live add-on would not start. `match(.*)?` does accept both shapes and is rejected for a different, measured reason: it validates nothing (a string, a list and the integer 417 all pass) and renders as a plain text box. So `pv_entities` joins `pv_entity` and REPLACES it when set, CFG-04's rule for `inverters:`, with a status caution saying which key won - a union would double-count the inverter the old key still names. STILL NOT A CONTROL INPUT. tools/tel11_pv_trace.py was extended rather than replaced and now grades five claims over three document sets. The control hash is UNMOVED - 846a10a5b89e… on `unset` and on `one`, on both trees - and the roster control document is byte-identical between the two trees. 2/5 is a claim TEL-11 could not make: with 0.11.0 as the baseline, "one entity behaves exactly as 0.11.0 does" is a real cross-tree identity over control AND telemetry. ⚠️ TEL-11's OWN `--compare` REFUSAL HAD TO CHANGE, and that is MUTATION.md habit 24: a discriminator defined as "the baseline lacks this feature" dies on the release that ships the feature, and dies as a false refusal. Twelve mutations, each on a fresh purged `cp -a`, no survivors. N5 is +0 B (`sum` -> `max`, the same three characters) and was run on a fresh copy with `cmp` confirming the edit on disk. The fixture is three distinct NON-ZERO contributions in two units with absence driven at EVERY position - a member contributing 0 W could go absent without moving the sum of the rest, which is habit 23 with a coinciding value in the arithmetic rather than in the plant. 31/31 in the CI image. Not deployable as merged: config.yaml stays at 0.11.0, so predeploy_check.py reports a NO-OP until PKG cuts a version. A CHANGELOG entry is wanted and deliberately not written - 11b forbids `## Unreleased` and the bump is PKG's call; DECISIONS/TEL-12-several-pv-inverters.md is the material it needs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
This commit is contained in:
@@ -139,7 +139,8 @@ either direction, and the check is mutation-tested both ways.
|
||||
| `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. Your **solar** inverter's power sensor, `+` = producing. Measurement only — it changes no control behaviour. See [PV production](#pv-production-pv_entity-tel-11) |
|
||||
| `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) |
|
||||
|
||||
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
|
||||
@@ -416,6 +417,11 @@ get then depend on roster order. Give your inverters plainly different names.
|
||||
|
||||
### PV production (`pv_entity`, TEL-11)
|
||||
|
||||
> **More than one solar inverter?** Use `pv_entities` instead — see
|
||||
> [Several PV inverters](#several-pv-inverters-pv_entities-tel-12) below. The
|
||||
> rest of this section applies to both.
|
||||
|
||||
|
||||
**Optional, off by default, and it changes no control behaviour whatsoever.**
|
||||
Set it and the add-on records what your solar is producing beside what the
|
||||
meter and the battery are doing. Leave it empty and the add-on reads no entity,
|
||||
@@ -476,7 +482,7 @@ believed a week later.
|
||||
|
||||
| what happened | `sensor.goodwe_pv_power` | on the status page |
|
||||
|---|---|---|
|
||||
| `pv_entity` not set | the entity does not exist | no row at all |
|
||||
| neither `pv_entity` nor `pv_entities` set | the entity does not exist | no row at all |
|
||||
| set, sensor answered `4100 W` | `4100.0` | `PV 4 100 W` |
|
||||
| set, sensor answered `0 W` | `0.0` | `PV 0 W` |
|
||||
| set, sensor unavailable / missing / bad unit | `unknown` | `PV —`, plus a caution naming the reason |
|
||||
@@ -490,6 +496,69 @@ 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)
|
||||
|
||||
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
|
||||
all and the add-on records the **site total**:
|
||||
|
||||
```yaml
|
||||
pv_entities:
|
||||
- sensor.pv_east_power
|
||||
- sensor.pv_west_power
|
||||
- sensor.pv_garage_power
|
||||
```
|
||||
|
||||
In the Configuration tab this is a repeatable field: press the **+** and add a
|
||||
row per inverter. Each entity's **own** `unit_of_measurement` is read, so a
|
||||
kW inverter beside a W one adds up correctly.
|
||||
|
||||
#### It replaces `pv_entity` — clear that one
|
||||
|
||||
Setting `pv_entities` makes `pv_entity` **ignored**, and the status page says so
|
||||
until you clear it. That is deliberate rather than tidy: if the two were *added*
|
||||
together, an upgrade where you leave your first inverter in `pv_entity` and also
|
||||
list it here would count it **twice**, and a doubled total is as wrong as a
|
||||
missing one in the one channel that exists to be believed a week later.
|
||||
|
||||
#### ⚠️⚠️ If ANY of them stops reporting, there is NO site total
|
||||
|
||||
This is the whole reason the key is shaped this way, so it is worth being plain
|
||||
about:
|
||||
|
||||
| what happened | `sensor.goodwe_pv_power` | on the status page |
|
||||
|---|---|---|
|
||||
| all three reporting 1200 / 2100 / 450 W | `3750.0` | `PV 3 750 W` |
|
||||
| all three reporting, one of them **0 W** | `1650.0` | `PV 1 650 W` |
|
||||
| one of the three `unavailable` | `unknown` | `PV —`, plus a caution naming which entity, why, and what the others reported |
|
||||
| all three gone | `unknown` | as above |
|
||||
|
||||
**It does not publish the sum of the ones that answered.** Two inverters out of
|
||||
three add up to a smaller number that is *indistinguishable from a real one* —
|
||||
and read back a week later that says *"there was less sun than there was"*,
|
||||
which is precisely the conclusion this recording exists to **test** rather than
|
||||
to manufacture. A gap makes the analysis say *"I do not know"*; a partial makes
|
||||
it say something false. So the total goes to `unknown` and the recorder stores a
|
||||
gap, exactly as it does for a single dead sensor.
|
||||
|
||||
The diagnostic is not thrown away — it is put where a person will see it. The
|
||||
status page names the entity that is missing, the reason, **and** what the
|
||||
others reported. That page is read live by a human who wants to fix something;
|
||||
the recorded number is read cold by an analysis that would believe it.
|
||||
|
||||
Note the **third row against the second**: a member honestly reporting `0 W` is
|
||||
**summed**, not treated as missing. Shading, a tripped string and nightfall are
|
||||
all real zeros, and blanking the site whenever one member read zero would report
|
||||
an absence on every clear evening.
|
||||
|
||||
#### ⚠️ Not "just use a template sensor that adds them up"
|
||||
|
||||
You may legitimately prefer a template sensor, and `pv_entity` will happily
|
||||
point at one. But it is **not** the answer to this problem: a template sensor
|
||||
summing an `unavailable` source yields the partial total *silently*, which is
|
||||
the same defect one layer down — in a place this add-on cannot see and cannot
|
||||
report to you.
|
||||
|
||||
#### ⚠️ What it does NOT do
|
||||
|
||||
It is **instrumentation**. It is not an input to the control law, the
|
||||
|
||||
Reference in New Issue
Block a user