- 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>
99 lines
4.9 KiB
Markdown
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.
|