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
@@ -683,6 +683,72 @@ except vol.Invalid as e:
|
||||
str(e))
|
||||
|
||||
|
||||
print("\n== H. TEL-14: the SHAPE of a per-entity PV sign option ==")
|
||||
# Four candidate shapes, each against a 0.12.0 blob (`pv_entities` set, no new
|
||||
# key). Transcript and decision: DECISIONS/TEL-14-pv-sign.md.
|
||||
PV12 = {"pv_entity": "", "pv_entities": ["sensor.pv_inverter", "sensor.pv_meter"]}
|
||||
_pv12 = MERGER.merge(copy.deepcopy(defaults), copy.deepcopy(PV12))
|
||||
_nk = "pv_invert_entities"
|
||||
|
||||
|
||||
def _ui_of(sch, name):
|
||||
return [e for e in UiOptions(None)(sch) if e.get("name") == name]
|
||||
|
||||
|
||||
# (a) a `-` prefix on a list item, under the shipped `- str`.
|
||||
ok_a, out_a = validate(dict(_pv12, pv_entities=["sensor.pv_inverter",
|
||||
"-sensor.pv_meter"]))
|
||||
show("(a) PREFIX: `-sensor.pv_meter` validates under the shipped `- str` - "
|
||||
"Supervisor does not look inside the string, so the sign would live in a "
|
||||
"syntax that only the description text explains",
|
||||
ok_a and out_a.get("pv_entities") == ["sensor.pv_inverter",
|
||||
"-sensor.pv_meter"],
|
||||
"" if ok_a else out_a)
|
||||
|
||||
# (b) a parallel list - THE SHIPPED SHAPE.
|
||||
ok_b, out_b = validate(_pv12)
|
||||
show("(b) ⭐ PARALLEL LIST, SHIPPED: the 0.12.0 blob (no `pv_invert_entities` "
|
||||
"key) validates unchanged and the new key defaults to []",
|
||||
ok_b and out_b.get(_nk) == []
|
||||
and out_b.get("pv_entities") == PV12["pv_entities"],
|
||||
"" if ok_b else out_b)
|
||||
ok_b2, out_b2 = validate(dict(_pv12, **{_nk: ["sensor.pv_meter"]}))
|
||||
show("...and one entity marked inverted validates",
|
||||
ok_b2 and out_b2.get(_nk) == ["sensor.pv_meter"], "" if ok_b2 else out_b2)
|
||||
ok_b3, out_b3 = validate({k: v for k, v in defaults.items() if k != _nk})
|
||||
show("⭐ ...and the shipped `options:` block with ONLY `pv_invert_entities` "
|
||||
"removed is REFUSED BY NAME - its `[]` default is load-bearing, as "
|
||||
"`pv_entities: []` is",
|
||||
(not ok_b3) and f"Missing option '{_nk}' in root" in str(out_b3), out_b3)
|
||||
_ui_b = _ui_of(schema, _nk)
|
||||
show("...and the Configuration tab renders it as a REPEATABLE string field "
|
||||
"with its own top-level (translated) label",
|
||||
len(_ui_b) == 1 and _ui_b[0].get("multiple") is True
|
||||
and _ui_b[0].get("type") == "string", json.dumps(_ui_b))
|
||||
|
||||
# (c) a list of dicts - on `pv_entities` itself, and as a new key.
|
||||
_s_dicts = dict(schema, pv_entities=[{"entity": "str", "invert": "bool?"}])
|
||||
ok_c, out_c = validate_with(_s_dicts, _pv12)
|
||||
show("(c) LIST OF DICTS ON `pv_entities`: the STRINGS a 0.12.0 install stored "
|
||||
"are REFUSED - that shape would stop the add-on starting on upgrade",
|
||||
(not ok_c) and "pv_entities" in str(out_c), out_c)
|
||||
_s_src = dict(schema, pv_sources=[{"entity": "str", "invert": "bool?"}])
|
||||
ok_c2, out_c2 = validate_with(_s_src, dict(_pv12, pv_sources=[]))
|
||||
_ui_c = _ui_of(_s_src, "pv_sources")
|
||||
show("(c2) ...as a THIRD key it validates, but renders as a type-schema "
|
||||
"repeatable section whose per-entry keys reach the screen RAW "
|
||||
"(`entity`, `invert`) on this frontend - see section F",
|
||||
ok_c2 and len(_ui_c) == 1 and _ui_c[0].get("type") == "schema"
|
||||
and _ui_c[0].get("multiple") is True,
|
||||
json.dumps(_ui_c)[:200] if ok_c2 else out_c2)
|
||||
|
||||
# (d) one global bool.
|
||||
_s_bool = dict(schema, pv_invert="bool")
|
||||
ok_d, out_d = validate_with(_s_bool, dict(_pv12, pv_invert=False))
|
||||
show("(d) GLOBAL `pv_invert: bool` validates too - it loses on SEMANTICS (one "
|
||||
"sign for a mixed roster), not on the schema", ok_d, "" if ok_d else out_d)
|
||||
|
||||
|
||||
# ⚠️ THE EXIT CODE IS PART OF THE INSTRUMENT. Without it the probe prints FAIL
|
||||
# and still succeeds, and a transcript is only evidence if a wrong answer is
|
||||
# distinguishable from a right one by something other than reading it.
|
||||
|
||||
Reference in New Issue
Block a user