# TEL-07 — why inverter A does not get a device of its own, and B does **Status: DECIDED 2026-09-05.** MQTT discovery announces **two** devices at a roster of two: the existing one, unchanged, plus one for the second inverter. It does **not** announce one per inverter. This document exists because the symmetrical design is the obvious one, is tidier, and would destroy the live system's recorded history — so the next person to read `app/mqtt.py` needs the reason written down where they will find it before they "fix" it. Filed with TEL-07. Card `6a9bfa75f0a3550275ba4888`. --- ## 1. The mechanism, in one paragraph `app/mqtt.py`'s own note records the finding, tested on HA 2026.8: Home Assistant composes an entity id from the **device name plus the entity name**, and `object_id` in the discovery payload does **not** override it. So the device name is half of every entity id. The second half of the mechanism is the half that makes it silent. Home Assistant keeps the entity id it composed at **first** discovery. Re-scoping the device therefore does not rename an entity in place; it creates a **new** entity beside the old one. The recorded history stays on the old id, the old id stops updating, and nothing errors. Every dashboard card, template and automation that names it — including `goodwe_master_watchdog_held_*` and `goodwe_master_parked_at_0_w_*`, which are live — keeps pointing at a sensor that has quietly gone flat. `sensor.goodwe_p1_sample_age` is the sharp end of it: the ESP32's stale-input watchdog subscribes to that composed name, and its own comment calls this the failure that "FAILS SILENTLY AND LOOKS CORRECT". ## 2. The two designs | | **one device per inverter** | **what shipped** | |---|---|---| | A's device | new: `inverter-a` | **the one it already has** | | B's device | new: `inverter-b` | new: `goodwe_rs485_controller_inverter_b` | | A's existing entity ids | **all renamed** | **byte-identical** | | A's recorded history | **stranded on dead ids** | kept | | watchdog automations | **silently broken** | untouched | | composed per-unit ids | `sensor.goodwe_inverter_a_*` / `..._b_*` | **the same** | | reads as | symmetrical and tidy | asymmetrical, and needs this file | The last two rows are the whole argument. The tidy design's only real benefit is a pair of symmetric, predictable entity ids — and the shipped design produces **exactly the same ids** by putting the unit's name in the *entity* name for the first roster entry and in the *device* name for the others. There is nothing left to buy, and the price was the live system's history. ## 3. What is per-unit and what is site **Per-unit:** setpoint (this unit's share of the site target, the number its own heartbeat re-emits), battery power (`Inverter.batt`, register `0x0518`, this unit's own reading), SoC (`Inverter.soc` — **this** pack; the site row is the *emptiest* pack, which at N≥2 may be the other one), and the SAFETY-02 sign latch, which is one instance per actuator by construction. **Site:** `grid` and `p1_sample_age_s`. There is one P1 meter and one grid connection, so a per-inverter copy invents two of a thing there is one of — and `p1_sample_age_s` is a **watchdog input**, so a second copy is a second answer to "is the meter fresh?", one of which is always the one nobody is looking at. Circulating power is site because it *is* a property of the set (`sum(|Bᵢ|) − |sum(Bᵢ)|`); so are the controller status, the maintenance phase and the capacity-tariff peak. **Named as absent rather than left looking finished:** the card also lists "inverter AC power" and per-unit heartbeats as per-unit. Neither has a source in the add-on today — nothing reads an AC-power register per unit, and `Inverter.last_beat` is a monotonic loop clock with no wall-clock age published anywhere. Announcing them would create entities nothing ever feeds, which is the hazard the `omit` note in `mqtt.py` exists to refuse. They are gaps. ## 4. Why a roster of one announces nothing Every install running today is a roster of one, and at N=1 the site rows **are** that unit's rows: `battery` is its power, `soc` is its pack. Four extra entities would be four new rows carrying numbers already on the dashboard, on a live system, for no information. `announced_units()` answers `[]` below `PER_UNIT_MIN_ROSTER`, and `Controller.publish()` reads **that same list** — the empty list is the gate on both halves, rather than a predicate written twice that could later disagree. ## 5. How it was verified, and what that does not cover The claim is a **diff of the announced payloads** against `release/1.0`, generated by running that commit's module rather than transcribed from it: `9 topics, 0 missing, 0 changed, 0 added` at a roster of one, and `0 missing, 0 changed, 8 added` at a roster of two. `test_mqtt.py` part 2 shows `_announce()` publishes exactly what is diffed, so the comparison is against the bytes paho is handed. Sixteen mutations were run. Twelve are in the file as a running table, each named with the property it breaks. Three properties in that table exist only because the table found their absence — dropping the unit from the state topic, moving `grid` into the per-unit list, and `units=()` in main()'s publisher call were all green on the first run, as was a roster fallback that named two nameless entries `inverter-a`. **What it does not cover is listed in `test_mqtt.py` part 9, with sizes.** The largest item: Home Assistant itself is not in the loop. The entity-id composition is a **model**, anchored on exactly one measured id (`sensor.goodwe_p1_sample_age`, verified against the live system 2026-08-26). Closing that needs a broker, an HA and the owner present, and this card is not allowed near the live install. ## 6. ⚠️ Inverter B does not exist Not until ~2026-09-12. Every roster of two in this repository's checks is **simulated**; B's `max_w: 4600` is the datasheet's figure for a GW5048D-ES (serial `55048ESU22AW0992`), recorded in `goodwe/NOTES.md` §1.1b, and nothing about B has ever been measured. No entity described here has been seen on a broker. When the second inverter is installed, the first thing to check is that the eight ids in §1 are still updating — not that the new ones appeared.