Precedence arbiter: one rule instead of an if/else ladder
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
This commit is contained in:
@@ -1,5 +1,34 @@
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user