Files
goodwe-addon/goodwe_controller/DECISIONS/SAFETY-112-waiting-for-home-assistant.md
T
adminandClaude Opus 5.5 df6bc0f76b
checks / unit (pull_request) Successful in 36m27s
infra-pin / fresh (pull_request) Successful in 1s
SAFETY-112 review: grade the waiting flag where hass.py sets it
- a 502 or refused read turns waiting on, the next good read off, and a
  read timeout never (two mutants survived without this)
- the flag changes no write with live readings, not only with 502 ones
- the page script's amber dot and batteries-tab banner for "waiting"
- DECISIONS: about 5 minutes per start stage, not "at most"

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 02:10:27 +02:00

99 lines
4.9 KiB
Markdown

# SAFETY-112 - control waits for Home Assistant to finish starting
**Status: for review.** Tests: `test_safety112.py`, `test_messages.py`.
Control is unchanged. The add-on now says plainly that it is waiting.
## Cause
The Supervisor's proxy refuses every add-on request with 502 until Core's
state is `RUNNING`. The add-on reads every battery reading and writes every
power request through that proxy, so it cannot control until then. Readings
that are already live inside Home Assistant do not help.
Source, Supervisor 2026.08.0 (the image the live site ran at CFG-04):
- `supervisor/api/proxy.py`, `APIProxy.api()` (REST, used for reads and
`number.set_value`) and `APIProxy.websocket()` both start with
`if not await self.sys_homeassistant.api.check_api_state(): raise HTTPBadGateway`.
- `supervisor/homeassistant/api.py`, `check_api_state()` returns
`state.core_state == "RUNNING" or state.offline_db_migration`. The state
comes from Core's `/api/core/state`.
- Upstream `main` has the same gate in both handlers (read 2026-10-13).
- A Core restart restarts only the Core container
(`homeassistant/core.py`, `restart()`). The add-on keeps running.
The add-on itself has no wait. Nothing in `app/` reads Core's state or waits
for an event. The loop reads its inputs every second, a 502 gives `None`,
and missing inputs give 0 W. P1 backoff is not involved: the live site reads
the meter straight from the HomeWizard (`homewizard_local`), not through Home
Assistant.
## Measured
Repro stack `safety112-*`, all removed afterwards:
- Home Assistant 2026.9.4, as live.
- A test integration whose `async_setup` sleeps, so Core stays `NOT_RUNNING`.
- A fake meter and two fake inverters that update every 2 s from load.
- The Supervisor's own `APIProxy` and `HomeAssistantAPI` from the 2026.08.0
image. Only the add-on token lookup, Core's access token and the container
check are stubbed.
- The add-on on `meter_source: homewizard_local` against a fake HomeWizard.
Times are seconds after `docker restart` of Home Assistant:
| run | Core `NOT_RUNNING` | entities live in HA | Core `RUNNING` | proxy 502 | first write | wait from live readings |
|---|---|---|---|---|---|---|
| base 3050fa5, N=2 | 13.1 | 17.1 | 253.1 | 0.9-252.7 (1 132 requests) | 254.7 | 237.6 |
| base 3050fa5, N=1 | 13.1 | 18.1 | 256.1 | 1.5-255.1 (573 requests) | 257.2 | 239.1 |
| this change, N=2 | 12.2 | 17.2 | 313.2 | 1.4-312.5 (1 406 requests) | 317.6 | 300.4 |
| this change, N=1 | 12.2 | 17.2 | 255.2 | 1.2-254.1 (571 requests) | 257.1 | 240.0 |
The first write comes 1.0-4.4 s after `RUNNING`, so the whole wait is the
proxy's. With a 400 s sleep Core moved on at 300.88 s on its own (bootstrap's
stage timeout), so one slow integration holds a start stage for about 5
minutes. The whole start can take longer: the live one took 414 s.
Before this change the page said "Not ready — Battery level: no usable
reading", which sends the reader to Developer Tools for an entity that is
fine.
## Rule
1. No way around the Supervisor. The add-on still talks to Home Assistant
only through the proxy.
2. While the proxy does not answer on the API (`hass.OUTAGE.waiting()`), the
first status row is red: "Home Assistant is not answering, perhaps it is
still starting. Control resumes by itself when it answers." The banner
reads "Not ready — Waiting for Home Assistant to finish starting". The Home
pill reads "Waiting for Home Assistant", in English and Dutch.
3. A safety stop, a stalled control loop or shadow mode keeps its own banner
and state over the wait, because none of them clears by itself.
4. The log has one WARNING when Home Assistant stops answering, an INFO line
every 5 minutes while it is down, and an INFO line with the length of the
wait when it answers again.
5. Only the API counts. The meter websocket alone does not, because the meter
has its own row.
## Why it is safe
The change is text only. `cycle()`, the writes, the heartbeat, the stand-down
on shutdown and every watchdog are untouched. `test_safety112.py` part 3 shows
the same targets and writes with the flag on and off, for N=1 and N=2.
## Open, for the owner
The add-on cannot control while Home Assistant starts. These ways around it
were not taken:
- **Talk to Core directly with a long-lived token** (`http://homeassistant:8123`).
Core's own API answers while `NOT_RUNNING`. In the repro `/api/core/state`
answered from 12-13 s, and a direct `number.set_value` 10 s into
`NOT_RUNNING` returned 200 and set the value. The cost is a secret stored
at every client site, which `hass.py` exists to avoid.
- **Talk to the control board directly** (ESPHome native API), so control
does not need Home Assistant at all. This is a large change.
- **Remove the slow start at the site.** On 2026-10-12 the Solis
integration's discovery held the start. Fixing or disabling it shortens the
wait there, but not on a client site with another slow integration.