TEL-14: PV sign declared per entity (pv_invert_entities)
checks / unit (pull_request) Successful in 8m39s
checks / unit (pull_request) Successful in 8m39s
Some PV entities (kWh meters, ESPHome clamps in the PV circuit) read positive for consumption and negative for production. `pv_invert_entities` lists the ones to flip; the flip is applied per member inside pv.total(), before the sum, so the MQTT `pv` row and the status page carry the corrected figure. Default [] is byte-identical to 0.12.0. Shape measured in supervisor_shape_probe.py section H (Supervisor 2026.08.0): a parallel list validates the 0.12.0 blob unchanged and renders as its own translated repeatable field; dicts on pv_entities refuse 0.12.0's stored strings; a prefix is invisible syntax; a global bool cannot describe a mixed roster. Status page marks flipped entities and cautions per entity below -50 W, saying ADD or REMOVE. Telemetry only: control documents unchanged. Record: DECISIONS/TEL-14-pv-sign.md (reverses TEL-11 section 2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6e6527224a
commit
7559d21eda
@@ -38,6 +38,8 @@ the trade [[product-not-one-house]] names.
|
||||
|
||||
## 2. The sign convention: **positive is production**, and there is no `pv_invert`
|
||||
|
||||
> Reversed by TEL-14 (flag and negative caution): `DECISIONS/TEL-14-pv-sign.md`.
|
||||
|
||||
### The convention is derived, not assumed
|
||||
|
||||
Two sign rules already ship, and both are recorded in `DOCS.md`'s *Sources*
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# TEL-14 — the PV sign, declared per entity
|
||||
|
||||
Owner report on 0.12.0: `sensor.espressif_vermogen` (ESPHome clamp) and
|
||||
`sensor.kwh_meter_actueel_vermogen` (kWh meter in the PV circuit) read
|
||||
**positive for consumption, negative for production**. At night the page said
|
||||
"✓ PV production: 13 W" — standby draw. By day production would publish negative.
|
||||
|
||||
## 1. This reverses TEL-11 §2, and why
|
||||
|
||||
TEL-11 §2 refused a `pv_invert` because (1) PV is read by nothing in the control
|
||||
path, and (2) a reversed sensor "announces itself as −4 kW at noon" and should be
|
||||
"fixed where it is produced". (1) still holds: the flip is applied inside
|
||||
`pv.total()`, which only feeds the MQTT `pv` row and the status page;
|
||||
`test_pv.py` part 12 re-runs part 6's 40-cycle identity with a flip set, and
|
||||
`tools/tel11_pv_trace.py` on release/1.0 vs this tree gives identical `control`
|
||||
documents for `unset`, `one` and `multi`; the only telemetry change is the new
|
||||
caution in the `negative` (−4200 W) cells. Its `--compare` verdict itself now
|
||||
REFUSES on any honest run, because both trees know `pv_entities` — TEL-12 §3's
|
||||
discriminator has aged out, as TEL-11's did; not fixed here. (2) did
|
||||
not survive contact with a real site: these sensors are not *wrong*, they
|
||||
follow a different convention; at night they read a plausible small positive
|
||||
number, so nothing announces anything; and "fix it where it is produced" means a
|
||||
template sensor, which TEL-12 §2 already names as the place an `unavailable`
|
||||
source goes quietly wrong.
|
||||
|
||||
## 2. The shape: a parallel list, `pv_invert_entities` — measured
|
||||
|
||||
`tools/supervisor_shape_probe.py` §H, Supervisor 2026.08.0, against a 0.12.0
|
||||
blob (`pv_entities` set, no new key):
|
||||
|
||||
```
|
||||
== H. TEL-14: the SHAPE of a per-entity PV sign option ==
|
||||
PASS (a) PREFIX: `-sensor.pv_meter` validates under the shipped `- str` - Supervisor does not look inside the string, ...
|
||||
PASS (b) ⭐ PARALLEL LIST, SHIPPED: the 0.12.0 blob (no `pv_invert_entities` key) validates unchanged and the new key defaults to []
|
||||
PASS ...and one entity marked inverted validates
|
||||
PASS ⭐ ...and the shipped `options:` block with ONLY `pv_invert_entities` removed is REFUSED BY NAME ...
|
||||
Missing option 'pv_invert_entities' in root in GoodWe RS485 Controller (local_goodwe_controller)
|
||||
PASS ...and the Configuration tab renders it as a REPEATABLE string field with its own top-level (translated) label
|
||||
[{"name": "pv_invert_entities", "multiple": true, "required": true, "type": "string"}]
|
||||
PASS (c) LIST OF DICTS ON `pv_entities`: the STRINGS a 0.12.0 install stored are REFUSED ...
|
||||
Invalid dict for option 'pv_entities' in GoodWe RS485 Controller (local_goodwe_controller)
|
||||
PASS (c2) ...as a THIRD key it validates, but renders as a type-schema repeatable section whose per-entry keys reach the screen RAW ...
|
||||
PASS (d) GLOBAL `pv_invert: bool` validates too - it loses on SEMANTICS (one sign for a mixed roster), not on the schema
|
||||
== all cases passed ==
|
||||
```
|
||||
|
||||
| shape | schema | why it lost / won |
|
||||
|---|---|---|
|
||||
| (a) `-sensor.x` prefix | accepted | invisible: the sign is a syntax only the description explains, in a field an installer fills with the entity picker's text |
|
||||
| (b) parallel list | accepted, 0.12.0 blob unchanged | **ships**: its own labelled, translated field ("PV entities whose sign is reversed"); an installer lists the odd ones out |
|
||||
| (c) dicts on `pv_entities` | **refuses 0.12.0's stored strings** | bricks the upgrade — TEL-12's trap again |
|
||||
| (c2) dicts as a third key | accepted | per-entry labels render raw (`entity`, `invert`) — §F; and a third PV surface |
|
||||
| (d) global bool | accepted | one sign cannot describe inverter + meter in one roster |
|
||||
|
||||
(b)'s known cost is a typo in the second list: `pv.stray_inverts()` turns it into
|
||||
a named caution. `[]` in `options:` is load-bearing (§H, isolated counterfactual).
|
||||
Default `[]` flips nothing, so every 0.12.0 install is byte-identical (part 12).
|
||||
|
||||
## 3. The negative caution: per entity, below −50 W
|
||||
|
||||
The owner's two meters draw ~13 W at night, so a *correctly* signed meter reads
|
||||
about −13 W then. −50 W clears that with room for a few such devices behind one
|
||||
entity; a reversed sensor by day reads hundreds to thousands of watts below
|
||||
zero. It is tested **per entity, not on the total** (a deviation from the brief):
|
||||
three correct meters at −20 W sum to −60 W and must stay quiet, while a reversed
|
||||
−1500 W hidden inside a positive total must not. It is a caution, never `bad`;
|
||||
a wrong threshold costs a spurious or missed caution, never data. The text says
|
||||
ADD (unflipped) or REMOVE (already flipped).
|
||||
|
||||
## 4. History
|
||||
|
||||
OPT-10's `sensor.goodwe_pv_power` recorded since 2026-09-24 on an install with
|
||||
such sensors carries the old sign; nothing rewrites it.
|
||||
|
||||
## 5. CHANGELOG entry for the next PKG cut
|
||||
|
||||
Not written into `CHANGELOG.md`: `test_version_gate.py` 11b/11d refuse an
|
||||
`## Unreleased` heading (measured on this branch: 4 reds). For PKG to paste:
|
||||
|
||||
> **`pv_invert_entities` (TEL-14).** A PV sensor that reads negative while
|
||||
> producing — typically a kWh meter or power clamp in the PV circuit — can now
|
||||
> be listed under *PV entities whose sign is reversed* in the Configuration tab;
|
||||
> its sign is flipped before the sum and the status page marks it. Empty (the
|
||||
> default) changes nothing. ⚠️ PV history recorded before you set it keeps the
|
||||
> old sign.
|
||||
Reference in New Issue
Block a user