Two controllers writing one actuator is the failure this system exists to
avoid. "Exactly one writer" was true, but only as a convention held up by
careful reading - which does not survive an EV charger and a heat pump wanting
the same battery.
Strategies now return claims and arbiter.py resolves them:
highest-priority `set` wins (none at all means 0 W), then every `limit` whose
priority is >= that set's applies, most restrictive first; contradictory
limits command 0 W and are flagged as the bug they are.
The second clause is the whole point. "Money outranks maintenance" used to be a
hand-written exception inside a Jinja template; it is now a consequence of the
priorities - the charge-only limit binds the loop but cannot bind a
higher-priority peak claim.
Also: maintenance shaping moved out of control.py, which is a controller again
and not a policy engine; the loop now tracks the arbiter's actual output rather
than its own last wish, so it does not jump when it regains control; and every
decision explains itself ("loop -> 0 W, limited by maintenance(charge-only)")
in the UI and the log.
19 new assertions in test_arbiter.py, each one a precedence question someone
will eventually ask in the field. Deployed to the reference site as 0.2.0 and
holding grid within a few watts of zero.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016NckgXecasQb2eSsPYNSW6
93 lines
4.9 KiB
Markdown
93 lines
4.9 KiB
Markdown
# Changelog
|
|
|
|
## 0.2.0
|
|
|
|
Precedence between strategies is now a first-class object instead of an if/else
|
|
ladder, ahead of there being more than three of them.
|
|
|
|
Every strategy returns a CLAIM each cycle - `set` ("I want X") or `limit` ("the
|
|
result must stay within these bounds") - and `arbiter.py` resolves them by one
|
|
rule:
|
|
|
|
1. Highest-priority `set` wins; no claim at all means 0 W.
|
|
2. Then every `limit` whose priority is >= that set's priority applies, most
|
|
restrictive first.
|
|
3. Contradictory limits are a BUG: command 0 W and say so.
|
|
|
|
Clause 2 is why "money outranks maintenance" is now a consequence of the
|
|
priorities rather than a special case in a Jinja template: the maintenance
|
|
charge-only limit binds the loop, but will not bind a higher-priority peak
|
|
shaving claim when one exists.
|
|
|
|
- Maintenance shaping (charge-only, cheap-window floor) moved out of the control
|
|
law. `control.py` is once again only a controller that tracks the meter.
|
|
- The loop computes from the ARBITER's last output, not its own last wish. If
|
|
something outranked it, that is what the hardware actually did, and tracking
|
|
anything else makes it jump when it regains control.
|
|
- Every decision is explainable: "loop -> 0 W, limited by maintenance
|
|
(charge-only)" now appears in the UI and the log, instead of a bare number.
|
|
- Safety limits (device rating, supervised max_w) bind every strategy including
|
|
the highest, and are still enforced a second time at the point of writing.
|
|
|
|
## 0.1.6
|
|
|
|
Findings from installing this on a live system, replacing a working YAML
|
|
implementation. Every one of these was silent - the add-on looked healthy while
|
|
being completely unable to do its job.
|
|
|
|
- **`run.sh` must use `#!/usr/bin/with-contenv sh`.** The HA base images run
|
|
s6-overlay, which starts services with a SANITISED environment. With a plain
|
|
shebang, SUPERVISOR_TOKEN is simply absent and every Home Assistant call
|
|
returns 401 - while `homeassistant_api: true` makes the permissions look
|
|
correctly granted. Startup now logs the token length and probes the Core API,
|
|
so the next person sees it in one line.
|
|
- **Bump `version:` for every change.** Supervisor keys the built image by
|
|
version, so editing source and rebuilding silently reuses the old image. Two
|
|
fixes appeared not to work because of this.
|
|
- **Dependencies come from apk, not pip.** Alpine is musl and there are no musl
|
|
wheels for aiohttp; pip would compile it on the client's Pi.
|
|
- **paho-mqtt 1.x and 2.x are both supported.** Alpine ships 1.x, which has no
|
|
`CallbackAPIVersion`; that raised and took the whole add-on down with it.
|
|
- **MQTT can no longer take down control.** Publisher construction is wrapped -
|
|
observability must never stop the controller.
|
|
- **MQTT discovery is published from `on_connect`.** paho drops QoS-0 publishes
|
|
issued before the CONNACK, so announcing straight after `connect()` published
|
|
nothing at all while logging "MQTT connected".
|
|
- **Repeated failures log at most once a minute.** The control loop retries every
|
|
second; unthrottled warnings rolled the log buffer and destroyed the startup
|
|
diagnostics needed to debug the 401 above.
|
|
- **`auto_start` works.** The store's defaults supplied `auto: False`, so the
|
|
fallback to the option could never fire.
|
|
|
|
Known issue: after deleting the MQTT entities from the registry during
|
|
development, Home Assistant would not re-adopt them from retained discovery -
|
|
not even after clearing the retained topics and reconnecting. The add-on
|
|
publishes correct discovery and live state (verified on the broker); this is an
|
|
HA-side adoption problem and affects status entities only, never control.
|
|
|
|
## 0.1.0
|
|
|
|
First packaged release. Ports the control loop and the monthly maintenance
|
|
cycle from the reference Home Assistant implementation into an add-on.
|
|
|
|
- Grid-following control: gain/slew/clamp/deadband with anti-windup, all tuned
|
|
against measured hardware behaviour (see FIELD-GUIDE.md §14).
|
|
- Saturation freeze **with the duration term** — three consecutive diverging
|
|
cycles, not one. The instantaneous test fires on every large correction,
|
|
because the plant itself needs 3-6 s to settle.
|
|
- Monthly maintenance cycle as an ownership state machine: drain / charge /
|
|
hold, with exactly one writer of the setpoint at any moment.
|
|
- Capacity-tariff awareness: the maintenance charge is capped by quarter-hour
|
|
peak headroom, and peak shaving outranks the maintenance schedule.
|
|
- Failsafe behaviour: commands 0 W on missing inputs, on stop, and on shutdown.
|
|
Never replays a stale setpoint - the reference implementation did, and the
|
|
hardware watchdog cannot catch that.
|
|
- Ingress UI with a commissioning checklist that names problems in words.
|
|
- Optional MQTT discovery for status entities.
|
|
|
|
Known limits:
|
|
|
|
- Home Assistant OS / Supervised only (add-ons cannot run on Container/Core).
|
|
- The inverter protocol is reverse-engineered; no vendor contract.
|
|
- Without the optional RS485 e-stop, nothing covers the host machine dying.
|