checks / unit (pull_request) Successful in 7m48s
`price_taxes_eur_kwh` shipped at 0.06 EUR/kWh with no recorded provenance -
no card, no date, no component list - and roughly HALF the true Flemish
all-in non-energy figure. It now ships at 0.11736, and the rows it is made
of are in the code rather than in this message:
Fluvius distribution (Antwerpen) 5,35 c EUR/kWh
Bijzondere accijns, "Alle verbruik" 4,876
Bijdrage groene stroom en WKK Vlaanderen 1,51
Energiebijdrage (0 EUR/MWh since 2026-08-01) 0
------
11,736 c EUR/kWh = 0.11736
Eneco residential card for Flanders, September 2026, incl. 6 % VAT.
`app/price.py`'s FLEMISH_TAXES_COMPONENTS_EUR_KWH carries the rows and
test_price.py section 1b re-adds them - and also compares the total against
the card's own figures typed in the test file, so an edit that moved the
rows and the default together is still caught.
WHAT THIS IS NOT. It is not an arbitrage-correctness fix and it cannot be.
This scalar is a LEVEL component: an additive constant cancels exactly in
`max - min` and leaves the argmin where it was. TARIFF-17 shipped that proof
and it is unchanged - test_price.py 16b drives six flat scalars spanning and
exceeding the whole per-DSO range and gets the identical cheapest hour and
the identical spread to the last digit. DOCS and the CHANGELOG say so in
those words.
WHAT IT IS. A display and guard fix:
* every number an operator reads, which at half the true value made the
add-on look considerably better than it is; and
* §17's negative-price guard in arbitrage.py - the one rung that tests an
ABSOLUTE all-in price rather than a difference. It fires when
`spot < -(markup + taxes)`, so its boundary moves -0.09 -> -0.14736
EUR/kWh of spot. test_arbitrage.py section 2c drives both sides through
the shipped decide() and pins the boundary from below as well.
The default is Antwerpen's real row, near the LOW end of the 0.117-0.131
per-DSO band, and that is deliberate: the error directions are not
symmetric. Too high carries a genuinely negative hour above zero and the
add-on exports into paid-for disposal - §17's own case. Too low only
declines a discharge that would have paid.
TARIFF-17's INTERACTION, WHICH HAD TO BE DECIDED. Nothing in that rung
hardcoded 0.06; it read PRICE_OPTION_DEFAULTS, so moving the default moves
what it refuses. The Supervisor sets `App.options = deepmerge(config.yaml
defaults, stored options)`, so a saved blob carries the key whether it was
typed or not - every stored 0.06 became "non-default" with nobody editing
anything, and a working Walloon or Brussels site with a taxes table would
have met SystemExit(78) on the upgrade over an inert number. price.py now
carries SUPERSEDED_TAXES_DEFAULTS_EUR_KWH = (0.06,) and validate.py reads a
superseded default as untouched too. The rung is NOT weakened: 0.1198 beside
a table is still refused by name, and the refusal now names both figures.
config.yaml and PRICE_OPTION_DEFAULTS moved together; CFG-03's tripwire
(test_price.py section 1, test_defaults_pin.py) stayed green by agreeing.
test_price.py section 0's fourth check was REWRITTEN rather than re-pointed:
it used to justify the file's own grading constants by observing that the
shipped pair was degenerate (0.06 is exactly twice 0.03). TARIFF-18 ends
that coincidence, so the sentence is no longer true and is not kept.
29/29 test files green. Mutation ledger on the PR.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
167 lines
9.0 KiB
Markdown
167 lines
9.0 KiB
Markdown
# TARIFF-15 — the operator's own tariff card, and why the card has to grade it
|
||
|
||
**Status: DECIDED 2026-09-13 — ship `custom_formula` with FIXED coefficients and
|
||
a REQUIRED `card` block whose printed price the add-on reproduces at startup or
|
||
refuses to run. No expression evaluation, ever. Declare `{ENERGY, MARKUP}`,
|
||
never `ALL_IN`.**
|
||
|
||
Filed with TARIFF-15. Follows [[TARIFF-07]], which withdrew three per-supplier
|
||
plugins and named a formula provider as the answer it could not give.
|
||
|
||
---
|
||
|
||
## 1. Why a formula rather than more plugins
|
||
|
||
TARIFF-07 established the negative half: a per-supplier plugin has no source
|
||
for its numbers. This card establishes the positive one.
|
||
|
||
* **There is no machine-readable source of Belgian tariff coefficients
|
||
anywhere.** CREG's consumer tariff-card pages 404. CREG Scan is a
|
||
JavaScript-only wizard. The annual report carries the supplier list and no
|
||
per-product coefficients. VREG was unreachable. Every coefficient found in
|
||
research came from a supplier's **own PDF**, one of them image-only.
|
||
* So there is nothing to scrape, and fourteen plugins would still have no
|
||
source for their numbers — they would be fourteen copies of the invention
|
||
TARIFF-07 deleted three of.
|
||
* **The customer is the only party who can see their own coefficients**, and
|
||
the card they read them from **is the only artefact that can grade them**.
|
||
That second clause is the whole design; everything below follows from it.
|
||
|
||
## 2. The shape is the market's published arithmetic
|
||
|
||
Both suppliers whose 2026 cards were readable print three things on one line:
|
||
the formula, the reference index it assumes, **and the resulting price**.
|
||
|
||
| | computed | the card prints |
|
||
|---|---|---|
|
||
| Eneco `(0.102 × 133.9905 + 1) × 1.06` | **15.5471** | **15,55** |
|
||
| ENGIE `(0.1 × 61.96 + 0.9) × 1.06` | **7.5218** | **7,522** |
|
||
|
||
Two independent suppliers, reproduced to the last printed digit, through the
|
||
shipped code (`test_price.py` 15a runs both, and then runs both again end to
|
||
end through `load_prices()`).
|
||
|
||
That third column is not a nicety. It is the only externally-published number
|
||
in this whole feature, and it is what makes the design safe.
|
||
|
||
## 3. The double-counting risk, and which half of it is new
|
||
|
||
`price.py`'s trap 1 already mechanises that a **plugin** cannot double-count:
|
||
`PluginOptions` refuses the framework keys, `code_leaks()` refuses a plugin
|
||
whose *code* mentions them, `assemble()` adds the complement exactly once.
|
||
|
||
`custom_formula` introduces a risk none of those can see, **because it is not
|
||
in code**: the **operator** double-counting, by folding their network charges
|
||
into `offset` while `price_taxes_eur_kwh` also carries them. `code_leaks()`
|
||
scans code. This is config, typed by a person reading a PDF.
|
||
|
||
It is a real reading of a real card, not a hypothetical. A Belgian card's
|
||
`Tariefformule` line is the **energy component alone** — Eneco tabulates
|
||
network tariffs in one table and levies in a second; ENGIE states in so many
|
||
words that the formula excludes network, distribution, transport, grid levies
|
||
and excise. An operator who wants "the price I actually pay" and adds ~12
|
||
c€/kWh to `offset` has done something entirely reasonable and is now billed for
|
||
network charges twice, on every hour, with §11.4 making the whole class of
|
||
price defect invisible.
|
||
|
||
**Decision: the card grades it.** At startup `validate.py` recomputes the
|
||
card's own printed price from the operator's coefficients and refuses to start
|
||
unless the two agree to within `0.01` — which is the last digit a card prints.
|
||
An inflated `offset` cannot pass, because the printed estimate is by
|
||
construction the energy component: 15.55 becomes 28.27 and the add-on stops and
|
||
says why.
|
||
|
||
**And the limit, stated in the same sentence as the claim**, exactly as
|
||
`price.py` states trap 1's: *it grades the coefficients against the card, not
|
||
the card against reality.* A consistent-but-wrong pair — both numbers off the
|
||
wrong product, or off a card the contract has since rolled over from — agrees
|
||
with itself perfectly and passes. Nothing here or anywhere else can catch that.
|
||
|
||
### 3b. The one unit error the card check cannot see
|
||
|
||
A factor applied to **both** sides — coefficients *and* printed price — cancels
|
||
out of a comparison between them. That is a second quantity, so it is a second
|
||
refusal: the card's own point must land inside 0.02–1.00 EUR/kWh.
|
||
|
||
⚠️ **Its floor is applied to the ENERGY figure and not to the all-in one, and
|
||
that asymmetry was found by a mutation rather than by reasoning.** The shipped
|
||
`price_taxes_eur_kwh` was 0.06 — three times the floor — so a card read a
|
||
hundred times too small comes out at 0.0016 EUR/kWh of energy, is carried over
|
||
0.02 by the flat tax addend alone, and passes a band applied to the total. The
|
||
hundredfold error is exactly what the band exists for. Graded from both
|
||
directions in `test_price.py` 15e/15i and driven red as M10. *(Annotated
|
||
2026-09-13: TARIFF-18 raised the default to 0.11736, nearly six times the
|
||
floor. The asymmetry is unchanged in kind and larger in degree — the flat
|
||
addend that carries a hundredfold-small card over 0.02 got bigger.)*
|
||
|
||
## 4. Fixed coefficients. No expression evaluation of any kind
|
||
|
||
**Decided: never a grammar.** Two arguments, and the second is the one that
|
||
decides it.
|
||
|
||
* **Security.** The add-on serves `POST config/save` → `POST
|
||
/addons/self/options` on its own ingress page, so `price_formula` is
|
||
**settable over HTTP**. `eval` on it is remote code execution in a root
|
||
container holding `SUPERVISOR_TOKEN`, with `homeassistant_api` and
|
||
`hassio_api`, driving an inverter over RS485. `eval(s, {"__builtins__": {}})`
|
||
is not a sandbox and this repository is not going to pretend otherwise.
|
||
* **⚠️ AND THE DECIDING ARGUMENT IS NOT SECURITY: AN EXPRESSION IS
|
||
UNVERIFIABLE.** Two coefficients can be graded against the card's printed
|
||
estimate. An arbitrary expression cannot be graded against **anything** — so
|
||
it makes the invisible-wrong-coefficient problem *strictly worse*, which is
|
||
the exact failure this provider exists to prevent. An expression would buy
|
||
the operator freedom to be wrong in a way nothing could report.
|
||
|
||
All 24 surveyed Belgian products, both injection legs and aWATTar AT fit
|
||
`B × index + A`, so an expression buys nothing the market needs either.
|
||
|
||
**Future shapes become NAMED FIELDS** — `clamp_lo`, `factor_negative`,
|
||
`offset_bands` — each individually refusable and each individually gradeable
|
||
against a card. `parse_price_formula()` therefore **refuses an unknown key**
|
||
rather than ignoring it: an operator who types `"expr"` is told it does not
|
||
exist, instead of being priced on the fields they also typed with the one they
|
||
believed in dropped in silence.
|
||
|
||
`test_price.py` 15f walks the module's AST for `eval`, `exec`, `compile`,
|
||
`__import__` and `literal_eval`, and exercises the scanner's **red** direction
|
||
on every run so it cannot become a scanner that scans nothing.
|
||
|
||
## 5. `{ENERGY, MARKUP}`, not `ALL_IN`
|
||
|
||
The contract's formula **is** the supplier's markup — that is what `B` and `A`
|
||
are — but it is not the levies. Declaring `ALL_IN` would lie on behalf of every
|
||
operator who typed exactly what their card says and would drop
|
||
`price_taxes_eur_kwh` from every hour: measured at 0.2125 → 0.1555 EUR/kWh.
|
||
`price_markup_eur_kwh` is therefore **ignored** under this provider, and logged
|
||
as ignored, which is the framework's existing rule rather than a new one.
|
||
|
||
## 6. What was reused rather than rebuilt
|
||
|
||
The index path is `generic_dayahead`'s: same EPEX BE feed, same
|
||
Brussels-calendar range, same columnar parser, same declared egress hosts. The
|
||
override/range decision moved into one shared `dayahead_feed_url()` that both
|
||
plugins call, because two copies of that rule would be two places for the
|
||
short-horizon defect TARIFF-09 fixed to come back in only one of them.
|
||
|
||
`resolution: hourly` averages each hour's members. ⚠️ **That is not
|
||
`_Series.at()`'s hourly assumption returning (TARIFF-14):** it decides how many
|
||
points are *emitted*; the window each point covers is still derived from its
|
||
neighbours in `_cover_table()`. A feed that is already hourly averages one
|
||
member per hour — the identity — so the option cannot silently assume the
|
||
granularity it was handed. Graded.
|
||
|
||
## 7. What this card did NOT do
|
||
|
||
* **The injection leg.** Eneco uses `0,102` on consumption and `0,100` on
|
||
injection, and injection is VAT-exempt where consumption is at 6 % — a second
|
||
per-contract formula, not a second copy of this one. Separate card.
|
||
* **The hour-varying network charge that bites Wallonia.** Separate card.
|
||
* **Raising `price_taxes_eur_kwh` from 0.06 to ~0.12.** Separate card, and an
|
||
owner's decision. Note §3b: the band's floor is deliberately insensitive to
|
||
whatever that number becomes. *(Annotated 2026-09-13: that card is
|
||
**TARIFF-18** and it shipped on this branch at **0.11736** — the Eneco
|
||
residential card for Flanders, September 2026, incl. 6 % VAT, summed from
|
||
Fluvius Antwerpen distribution 5,35 + bijzondere accijns 4,876 + bijdrage
|
||
groene stroom en WKK 1,51 c€/kWh. §3b held: the floor is still on the energy
|
||
figure and nothing in this module needed a new number.)*
|