TARIFF-18: price_taxes_eur_kwh stops shipping at half the real figure
checks / unit (pull_request) Successful in 7m48s
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
This commit is contained in:
co-authored by
Claude Opus 5
parent
7796f168a9
commit
9c65ac2cf8
@@ -25,6 +25,7 @@ if it has, it needs 0.10.0. Whoever deploys decides.
|
||||
| _this branch_ | TARIFF-13 - the capacity-tariff row's "quarter(s) banked" stops resetting to 0 at every restart, and a tied peak's quarter stops moving |
|
||||
| _this branch_ | TARIFF-11 - the add-on's account of the tariff plan stops flattering it, and the status page is graded as a surface |
|
||||
| _this branch_ | TARIFF-17 - a price component that varies by hour has somewhere to live, so a Walloon day/night network tariff can change which hour is cheapest |
|
||||
| _this branch_ | TARIFF-18 - `price_taxes_eur_kwh` stops shipping at half the real Flemish figure **⚠️ CHANGED DEFAULT** |
|
||||
|
||||
⚠️ **A CONFIG THAT STARTS ON THE CURRENT VERSION CAN BE REFUSED ON THE NEXT
|
||||
ONE.** TARIFF-07 withdraws `engie_dynamic`, `totalenergies_dynamic` and
|
||||
@@ -131,6 +132,111 @@ figure is correct. This is a product concern rather than a site one: the add-on
|
||||
is sold to clients, and a Walloon or Brussels client could not express their
|
||||
own tariff.
|
||||
|
||||
### TARIFF-18 - `price_taxes_eur_kwh` stops shipping at half the real figure
|
||||
|
||||
⚠️ **A SHIPPED DEFAULT CHANGED: `price_taxes_eur_kwh` 0.06 -> 0.11736 EUR/kWh.**
|
||||
Same shape as 0.8.0's `slew_w` rule, and the same instruction follows — **read
|
||||
the live options before `ha addons update`**, this time to see whether
|
||||
`price_taxes_profile` is set, which is the one shape the move could have
|
||||
refused. The "who is affected" section below is the part that matters, and it
|
||||
is shorter on consequences than a changed default usually is.
|
||||
|
||||
The old `0.06` had **no recorded provenance anywhere**: not a card, not a date,
|
||||
not a component list. It is roughly **half** the Flemish figure. The new default
|
||||
is the Eneco residential card for Flanders, September 2026, incl. 6 % VAT, and
|
||||
its rows are now **in the code** (`app/price.py`'s
|
||||
`FLEMISH_TAXES_COMPONENTS_EUR_KWH`, re-added by `test_price.py` section 1b):
|
||||
|
||||
| component | c€/kWh |
|
||||
|---|---|
|
||||
| Fluvius distribution (**Antwerpen**) | 5,35 |
|
||||
| Bijzondere accijns, *"Alle verbruik"* — no volume bands | 4,876 |
|
||||
| Bijdrage groene stroom en WKK Vlaanderen | 1,51 |
|
||||
| Energiebijdrage — **0 EUR/MWh since 2026-08-01**, merged into the accijns | 0 |
|
||||
| **total** | **11,736 c€/kWh = 0.11736** |
|
||||
|
||||
⚠️ **IT IS PER-DSO, AND THE DEFAULT IS NOT AN AVERAGE.** Only the distribution
|
||||
row moves: across the eight Fluvius DSOs it runs 5,28 c€/kWh
|
||||
(Midden-Vlaanderen) to 6,70 (West), so the Flemish total runs about **0.117 to
|
||||
0.131**. The default is a real DSO's real row, near the **low** end, and DOCS
|
||||
tells an operator to look up their own. A **Walloon or Brussels** site should
|
||||
not use this scalar at all — its distribution tariff has a day and a night band
|
||||
and belongs in TARIFF-17's `price_taxes_profile`.
|
||||
|
||||
⚠️ **THIS IS A DISPLAY AND GUARD FIX. IT DOES NOT IMPROVE ARBITRAGE AND CANNOT.**
|
||||
This scalar is a **LEVEL** component: an additive constant cancels exactly in
|
||||
`max − min` and leaves the argmin where it was. TARIFF-17 shipped the machinery
|
||||
and the proof — six flat scalars spanning and exceeding the whole per-DSO range
|
||||
give the **identical** cheapest hour and the **identical** spread to the last
|
||||
digit (`test_price.py` 16b, printed on every run), and none of that changes
|
||||
here. No trade happens that did not happen before *because of the level*. What
|
||||
does change is two things, and nothing else:
|
||||
|
||||
* **every number an operator reads** — the all-in prices on the status page,
|
||||
`custom_formula`'s startup preview and the recorded price history. At half
|
||||
the true non-energy component they made the add-on look considerably better
|
||||
than it is; and
|
||||
* **§17's negative-price guard** (`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.09000 -> −0.14736
|
||||
EUR/kWh of spot**. A −0.11868 EUR/kWh spot hour used to read **−0.02868**
|
||||
all-in and the discharge was refused as paid-for disposal; at the real tax
|
||||
level it reads **+0.02868** and discharges, because that hour genuinely
|
||||
paid.
|
||||
`test_arbitrage.py` section 2c drives both sides through the shipped
|
||||
`decide()`, and pins the boundary from below as well.
|
||||
|
||||
⚠️ **AND THAT ASYMMETRY IS WHY THE DEFAULT SITS AT THE BOTTOM OF THE BAND.** Set
|
||||
too **high**, the figure carries a genuinely negative hour above zero and the
|
||||
add-on exports into paid-for disposal — §17's own case. Set too **low**, it only
|
||||
declines a discharge that would have paid. One default standing in for eight
|
||||
DSOs belongs at the bottom.
|
||||
|
||||
#### Who is affected
|
||||
|
||||
* **A site that set `price_taxes_eur_kwh` itself: nothing changes.** Its value
|
||||
is kept and is never silently replaced.
|
||||
* **A site whose stored options blob already carries the key: nothing changes
|
||||
either, and this is most existing installs.** The Supervisor sets
|
||||
`App.options = deepmerge(config.yaml defaults, stored options)`
|
||||
(`tools/supervisor_selfapi_probe.py` §D), so a moved default reaches only
|
||||
keys the blob does **not** carry — and the reference install's export carries
|
||||
**70 of the 71**, the one absentee being a key added after its last save.
|
||||
Such a site goes on running at `0.06` and reading numbers that are about half
|
||||
its real all-in price until somebody opens the Configuration tab. ⚠️ **That
|
||||
is a real limit of this card and it is not fixed here**: the change reaches a
|
||||
**fresh install**, and an existing one only when its options are re-saved.
|
||||
⚠️ **NOT re-measured on this branch** — the 70-of-71 figure is 0.9.0's, and
|
||||
PKG-09's `tools/predeploy_check.py --options <live blob>` against the live
|
||||
options is what would confirm it before deploying.
|
||||
* ⚠️ **A site with `price_taxes_profile` SET would have been refused at startup,
|
||||
and is deliberately not.** TARIFF-17 made a taxes period table beside a
|
||||
**non-default** `price_taxes_eur_kwh` a `SystemExit(78)` refusal, comparing
|
||||
against the shipped default precisely because the Supervisor writes the key
|
||||
whether it was typed or not. Moving the default turns every stored `0.06`
|
||||
into "non-default" **without anybody editing anything**, so a working Walloon
|
||||
or Brussels site would have stopped starting on the upgrade over an inert
|
||||
number the table overrides anyway. `price.py` now carries
|
||||
`SUPERSEDED_TAXES_DEFAULTS_EUR_KWH = (0.06,)` and `validate.py` treats a
|
||||
superseded default as *untouched* as well. **The rung is not weakened**: a
|
||||
figure that is neither the shipped nor a superseded default — ORES's 0.1198,
|
||||
say — is still refused, by name, and the refusal now says which of the two
|
||||
figures it will accept (`test_validate.py`, four new checks plus the
|
||||
difference precondition).
|
||||
|
||||
Nothing hardcoded `0.06` in that rung — it read `PRICE_OPTION_DEFAULTS` — which
|
||||
is why the interaction is a *behaviour* change rather than a drift, and why it
|
||||
had to be decided rather than noticed.
|
||||
|
||||
`config.yaml` and `PRICE_OPTION_DEFAULTS` moved together and CFG-03's tripwire
|
||||
(`test_price.py` section 1, `test_defaults_pin.py`) stayed green by agreeing,
|
||||
not by being widened. ⚠️ One check did have to be **rewritten rather than
|
||||
re-pointed**: `test_price.py` section 0 used to justify its 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; the file now asserts per-figure that neither shipped default is
|
||||
what it grades with, which is the property that was actually load-bearing.
|
||||
|
||||
### TARIFF-13 - the coverage figure beside the peak, and which quarter gets it
|
||||
|
||||
**The capacity-tariff status row's `N quarter(s) banked` is read from the
|
||||
|
||||
Reference in New Issue
Block a user