TARIFF-07: three plugins that never fetched a price are withdrawn, and a site configured on one is told so
checks / unit (pull_request) Successful in 7m12s
checks / unit (pull_request) Successful in 7m12s
engie_dynamic, totalenergies_dynamic and eneco_eddy are removed from PROVIDERS and from every surface that listed them. They did not stop working - they never worked. Re-verified by hand on 2026-09-13 before deleting anything: 404 with Kong's "no Route matched with those values"; a TLS handshake that fails because api.totalenergies.be is absent from the certificate's SAN list; 404 with an empty body. All three are also structurally impossible - the coefficients are per-contract and behind a customer login, there is no regulator index, and "Eddy" is not an Eneco product at all. ⚠️ BACKWARD-INCOMPATIBLE. A config legal on the current version is refused on the next. That is the point of the change: §11.4 turned every failed fetch into a silent fall back, so a site on one of these ids has been running on static_profile - or on nothing - since the day it was set, with nothing on its status page ever differing from a working provider that never found a cheap hour. validate.py now refuses a retired id with a message naming the id, the endpoint, what that endpoint answers, which tariff has really been in force, and what to set instead. No version number is picked here; that depends on a fact about the live box. ⚠️ config.yaml's price_provider enum deliberately KEEPS all three ids. The Supervisor validates the options file against that enum before the add-on starts, so removing them would make an existing config schema-invalid and the refusal above would never be reached on exactly the sites that need it. PROVIDERS is two; the enum stays six; test_price.py asserts the schema offers exactly PROVIDERS | RETIRED_PROVIDERS | {"off"}, derived rather than typed, and register() refuses to re-register a retired id. PriceProduct.REALTIME, _WIDE, Uncertainty.REALTIME_PRODUCT and WIDEN_FACTOR's 2.5 are KEPT with no shipped declarer - it is the AC2 seam, and a reviewer finding a mechanism with zero callers would reasonably delete it. Recorded in DECISIONS/TARIFF-07-withdrawing-three-plugins.md and pinned by a check. False claims corrected: every "Belgian imbalance product" claim about eneco_eddy (DOCS.md, price_forecast.py, price_history.py, static_tariff.py, FCST-04, CHANGELOG - the two released entries annotated in place rather than rewritten); the quarter-hourly examples now cite Belgian day-ahead, which really does settle per quarter since the 15-minute MTU; and export_price_eur_kwh's "no shipped supplier publishes a dynamic injection curve", which is false - Eneco publishes 0,1 x BELPEX-H - 1,188 and ENGIE (B x eSpot_15) - A, same two-coefficient shape with its own coefficients and its own VAT treatment, so the scalar is a simplification and not the market. DOCS.md's provider table is two rows and now says honestly what a Belgian dynamic customer should do today, including that generic_dayahead cannot express B x spot + A and that price_markup_eur_kwh: float(0,1) could not hold a negative offset. The formula provider is referenced as planned, not built. _points_from_json() went with the plugins it served: it read a shape no feed anybody here has ever reached publishes. The one place it survives is a negative stimulus, named row_shape_body(). Nine mutations driven, all red; table in the decision record. 29/29 suites green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
This commit is contained in:
+95
-13
@@ -1369,7 +1369,7 @@ does, and only after you have read why.
|
||||
|
||||
| option | meaning |
|
||||
|---|---|
|
||||
| `price_provider` | `off` (default) or one of the five plugins below |
|
||||
| `price_provider` | `off` (default) or one of the two plugins below |
|
||||
| `price_markup_eur_kwh` | Your supplier's margin, EUR/kWh. **Added only when the chosen plugin's feed does not already contain it** |
|
||||
| `price_taxes_eur_kwh` | Levies and VAT, EUR/kWh. Same rule |
|
||||
| `export_price_eur_kwh` | The **injection** price. A separate scalar, not a series — see below |
|
||||
@@ -1386,11 +1386,76 @@ arbitrage decision wrong by that amount, in a direction nothing reports.
|
||||
| plugin | product | the published figure contains | the add-on adds |
|
||||
|---|---|---|---|
|
||||
| `generic_dayahead` | day-ahead | energy only (wholesale EPEX BE) | markup **and** taxes |
|
||||
| `engie_dynamic` | day-ahead | energy + Engie's markup | taxes |
|
||||
| `totalenergies_dynamic` | day-ahead | energy + TotalEnergies' markup | taxes |
|
||||
| `eneco_eddy` | **real-time / imbalance** | everything, all-in | nothing |
|
||||
| `manual_csv` | day-ahead | everything, all-in | nothing |
|
||||
|
||||
⚠️ **Three plugins were withdrawn in TARIFF-07, and if your configuration names
|
||||
one of them, read this.** `engie_dynamic`, `totalenergies_dynamic` and
|
||||
`eneco_eddy` are gone. They did not stop working — **they never worked.** Each
|
||||
pointed at an endpoint that does not exist and never did:
|
||||
|
||||
| id | its endpoint | what that endpoint actually answers |
|
||||
|---|---|---|
|
||||
| `engie_dynamic` | `https://api.engie.be/dynamic/be` | **404**, gateway error `no Route matched with those values` |
|
||||
| `totalenergies_dynamic` | `https://api.totalenergies.be/prices/dynamic` | **TLS failure** — that hostname is not in the server's certificate |
|
||||
| `eneco_eddy` | `https://api.eneco.be/eddy/prices` | **404**, empty body |
|
||||
|
||||
Every fetch any of them ever made failed, and a failed fetch falls back to the
|
||||
static tariff **silently** — so a site set to one of these has been running on
|
||||
`static_profile`, or on nothing at all, since the day it was configured, with
|
||||
nothing on the status page saying so. The add-on now **refuses to start** on one
|
||||
of these ids and tells you all of the above by name. That is deliberate: a
|
||||
silent fallback is what hid this for as long as it lasted.
|
||||
|
||||
There is no automatic migration and there cannot be one. These were *retail*
|
||||
tariffs of the form `B x spot + A` with **per-contract** coefficients that live
|
||||
behind your supplier's customer login; there is no public index to read them
|
||||
from. Any alias would have to invent the numbers your battery is priced against.
|
||||
(`eneco_eddy` was doubly wrong: **"Eddy" is not an Eneco product at all** —
|
||||
Eneco's dynamic product is *Zon & Wind Dynamisch*, and it is indexed to
|
||||
Belpex/EPEX **day-ahead**, not to imbalance.)
|
||||
|
||||
#### So what should a Belgian dynamic-tariff customer do today?
|
||||
|
||||
**Honestly: `generic_dayahead` gets you close, and you should know exactly how
|
||||
close.** It reads the wholesale EPEX BE day-ahead curve and adds your
|
||||
`price_markup_eur_kwh` and `price_taxes_eur_kwh` as **flat** scalars. Your real
|
||||
Belgian dynamic contract is a **formula**:
|
||||
|
||||
```
|
||||
consumption: price = B x spot + A <- B and A are in YOUR contract
|
||||
```
|
||||
|
||||
The add-on cannot express that yet. Two gaps, and the second is the harder one:
|
||||
|
||||
* **No multiplier.** `B` is typically slightly above 1 (it scales the wholesale
|
||||
price before anything is added). A flat markup cannot scale anything, so the
|
||||
*shape* of your curve is right but its *amplitude* is not — the peaks and
|
||||
troughs the optimiser trades against are compressed relative to your bill.
|
||||
* **No negative offset.** `A` is frequently **negative** on injection and can be
|
||||
on consumption. `price_markup_eur_kwh` is `float(0,1)` in the schema — it
|
||||
**cannot hold a negative number at all**, so that contract shape is currently
|
||||
inexpressible rather than merely approximate.
|
||||
|
||||
What that means in practice: arbitrage decisions made on `generic_dayahead` are
|
||||
made against a curve that is *correlated* with your bill, not equal to it. For
|
||||
deciding **which hours** are cheap that is usually good enough; for deciding
|
||||
**whether a cycle pays for itself** — which is what `cycle_cost_threshold_eur`
|
||||
compares against — it is an approximation, and one that errs by however far `B`
|
||||
is from 1.
|
||||
|
||||
A **formula provider** (`B x spot + A`, with your own coefficients entered once)
|
||||
is the planned answer and is a separate card. It is not implemented here. Until
|
||||
it ships, the two supported honest options are:
|
||||
|
||||
1. **`generic_dayahead`** with your markup and taxes set as well as you can —
|
||||
accepting the approximation above; or
|
||||
2. **`manual_csv`**, where you compute the all-in prices yourself from your own
|
||||
contract's formula and hand the add-on the answers. Nothing is approximated,
|
||||
but nothing is automatic either.
|
||||
|
||||
If you are on a **fixed** or **day/night** tariff — most Belgian households —
|
||||
neither of those applies: use `static_profile` and leave `price_provider: off`.
|
||||
|
||||
You cannot get this wrong by editing the markup: each plugin *declares* what its
|
||||
feed contains and the add-on adds only the rest. Set a markup under a plugin
|
||||
that already carries one and it is **ignored, with a line in the log saying so**
|
||||
@@ -1404,10 +1469,13 @@ whether a plugin described its supplier's feed truthfully — that is a claim
|
||||
about somebody else's tariff, and no code can verify it. If you write a plugin,
|
||||
the `sources` declaration is the part a reviewer has to read.
|
||||
|
||||
`eneco_eddy` is a real-time/imbalance product rather than a day-ahead one. It is
|
||||
flagged as high-volatility, which widens arbitrage thresholds once the
|
||||
optimiser lands. That comes from the plugin's own declaration, not from its
|
||||
name.
|
||||
**Real-time / imbalance products still have a place in the machinery, but no
|
||||
plugin declares one.** A plugin that declares `PriceProduct.REALTIME` is flagged
|
||||
high-volatility and gets widened arbitrage thresholds; that comes from the
|
||||
plugin's own declaration, never from its name. Both shipped plugins are
|
||||
day-ahead, so nothing exercises that path on a real install today — it is kept
|
||||
on purpose so the first genuine imbalance product does not have to rebuild it.
|
||||
See `DECISIONS/TARIFF-07-withdrawing-three-plugins.md`.
|
||||
|
||||
**`generic_dayahead` and the shape of the public feed.** It reads Fraunhofer
|
||||
ISE's Energy-Charts API (`https://api.energy-charts.info/price?bzn=BE`, no key
|
||||
@@ -1519,9 +1587,10 @@ applies to those hours, and asking the plan for one raises rather than
|
||||
answering. An hour with no price is not a free hour, and the fill does not
|
||||
invent a neighbour, a mean or a zero for it.
|
||||
|
||||
**A quarter-hourly feed keeps all four of its prints.** `eneco_eddy` settles
|
||||
per quarter, and the fill decides *which whole hours need carrying* — never what
|
||||
a published hour may contain. Every point the provider published inside the
|
||||
**A quarter-hourly feed keeps all four of its prints.** Belgian day-ahead
|
||||
settles per quarter — the 15-minute MTU took effect on 2025-10-01, so
|
||||
`generic_dayahead` reads 96 points a day — and the fill decides *which whole
|
||||
hours need carrying*, never what a published hour may contain. Every point the provider published inside the
|
||||
horizon is passed through untouched; a carried point is added only for a whole
|
||||
hour the feed did not print at all.
|
||||
|
||||
@@ -1571,8 +1640,21 @@ volume being full exactly as it applies to the volume being absent.
|
||||
|
||||
**Injection is priced separately** (`export_price_eur_kwh`). In Belgium it is
|
||||
often near zero, which by itself strongly favours self-consumption over export.
|
||||
It is a single number rather than a curve because no supported supplier
|
||||
publishes a dynamic injection tariff; negative values are accepted.
|
||||
Negative values are accepted — you can be paid to *not* export at midday in
|
||||
summer.
|
||||
|
||||
⚠️ It is a single number rather than a curve, and the reason given here until
|
||||
TARIFF-07 — *"no supported supplier publishes a dynamic injection tariff"* — was
|
||||
**false**. Suppliers do. Eneco publishes `0,1 x BELPEX-H − 1,188` on the same
|
||||
tariff card as its consumption formula (identical on the January and the
|
||||
September 2026 cards), and ENGIE publishes one in the same shape,
|
||||
`(B x eSpot_15) − A`. What is true is narrower: an injection curve is the **same
|
||||
two-coefficient formula** as the consumption curve but with **its own**
|
||||
coefficients and **its own VAT treatment** — injection is VAT-exempt where
|
||||
consumption is at 6% — so it is a second per-contract formula rather than a copy
|
||||
of the first, and the add-on cannot evaluate either one yet. Until the formula
|
||||
provider ships, **this scalar is a simplification you enter by hand — an average
|
||||
of what you are actually paid — and not a reflection of the market.**
|
||||
|
||||
**The old Home Assistant price sensors.** `price_now_entity` and
|
||||
`price_avg_entity` still work and are unchanged: the add-on compares them and
|
||||
|
||||
Reference in New Issue
Block a user