# 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.)*