TARIFF-18: price_taxes_eur_kwh stops shipping at half the real Flemish figure #86

Merged
admin merged 1 commits from TARIFF-18 into release/1.0 2026-09-13 16:33:38 +02:00
Owner

The number

price_taxes_eur_kwh shipped at 0.06 EUR/kWh with no recorded provenance
anywhere
— 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 (app/price.py's FLEMISH_TAXES_COMPONENTS_EUR_KWH),
not only here:

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 €/kWh

Eneco residential card for Flanders, September 2026, incl. 6 % VAT. The exact sum
is shipped rather than the rounded 0.1174, so test_price.py section 1b can
assert an equality rather than a tolerance.

The zero row is deliberate. "Vanaf 1 augustus 2026 bedraagt de
energiebijdrage 0 EUR/MWh"
— it was merged into the excise. Carried as a row
rather than an absence so the next person re-deriving this from an older guide
finds it already accounted for instead of adding it a second time. test_price.py
1b grades the row's presence, because deleting it does not change the sum.

Why Antwerpen and not an average. Only the distribution row moves between
Fluvius DSOs (5,28 Midden-Vlaanderen → 6,70 West; total 0.117–0.131). The default
is a real DSO's real row, and it sits near the low end on purpose: the two
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.

What this does NOT do

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 none of it
changes — 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 does do

  • Every number an operator reads — at half the true value the status page,
    custom_formula's preview and the price history made the add-on look
    considerably better than it is.
  • §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 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 2c
    drives both sides through the shipped decide() and pins the boundary from
    below
    as well.

TARIFF-17's rung — what was found, and what was chosen

Nothing there hardcoded 0.06. validate.py read
PRICE_OPTION_DEFAULTS["price_taxes_eur_kwh"], so moving the default moves what
the rung refuses — a behaviour change rather than a drift, which is why it had to
be decided rather than noticed.

The Supervisor sets App.options = deepmerge(config.yaml defaults, stored options) (tools/supervisor_selfapi_probe.py §D; the reference install's export
is 70 of 71 keys), so a saved blob carries price_taxes_eur_kwh whether the
operator typed it or not. Moving the default turns every stored 0.06 into
"non-default" with nobody editing anything
— and a working Walloon or Brussels
site with a price_taxes_profile would have met SystemExit(78) on the
upgrade
, over an inert number the table overrides anyway.

Chosen: "untouched" becomes a set. price.py carries
SUPERSEDED_TAXES_DEFAULTS_EUR_KWH = (0.06,) beside the default it supersedes,
and validate.py reads a superseded default as untouched too. The rung's subject
is a number the operator typed; a default this add-on wrote into the blob is
not one. The rung is not weakened — 0.1198 beside a table is still refused by
name, and the refusal now says which figures it accepts. Graded by four new
checks plus a difference precondition (habit 1) and a non-empty-set guard, and
M5/M5b/M6 below drive each half red on its own.

CFG-03

config.yaml and PRICE_OPTION_DEFAULTS moved together; test_price.py section
1 and test_defaults_pin.py stayed green by agreeing, not by being widened —
M8 reverts config.yaml alone and section 1 goes red.

⚠️ One check had to be rewritten rather than re-pointed. test_price.py
section 0's fourth check justified 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. The file now asserts per figure that neither shipped default is what it
grades with, which is the property that was actually load-bearing (and is
stronger than the tuple comparison it replaces, which one matching figure
satisfied).

Mutation ledger

Harness: /home/homelab/.cache/t18-mut/mutate.sh — mktemp backup per
invocation
inside that directory (habit 18), the graded run in a container with
the tree mounted :ro so no __pycache__ can exist at all (TEST-15's
structural immunity, stronger than -B), the mutated line grep-ed on disk
before the run, and the revert fingerprinted against the pre-mutation
sha256 afterwards. Every revert verified.

# mutation file byte delta result
M1 default 0.11736 → 0.12 app/price.py −3 B RED — test_price.py §1 mirror-vs-config.yaml, §1b "the 4 rows re-add to the shipped default: 0.11736 against 0.12", §1b card-literal; test_price_forecast.py §7 "…and it still holds TARIFF-18's Flemish figure"
M2 Fluvius row 0.0535 → 0.0528 (Midden-Vlaanderen's row) app/price.py +0 B ⚠️ RED — §1b "the 4 rows re-add to the shipped default: 0.11666 against 0.11736". The card-literal check stayed green, which is the point of having two sides
M3 delete the energiebijdrage zero row app/price.py −79 B RED — §1b "the energiebijdrage is carried as a row worth ZERO rather than left out…". The sum check stayed green — deleting a zero changes no total, which is exactly why presence is graded separately
M4 SUPERSEDED_TAXES_DEFAULTS_EUR_KWH = (0.06,) → () app/price.py −5 B RED — test_price.py §1b "…it is no longer the superseded None"; test_validate.py difference-precondition, non-empty-set guard, and the refusal-sentence check. ⚠️ Both files read the tuple defensively so that emptying it is a named red rather than an IndexError inside an f-string — a crash takes a file red with nothing on the record saying which claim broke
M5 untouched drops the superseded half app/validate.py −43 B RED — test_validate.py "…a blob that still carries the SUPERSEDED default 0.06 beside the table STARTS"
M5b untouched drops the shipped half app/validate.py −13 B RED — "…the SHIPPED scalar beside the table is NOT refused" + the accepted baseline. Both halves individually live; no masking (MUTATION.md TEST-16)
M6 the rung stops refusing anything (and False) app/validate.py −47 B RED — 4 named checks, including "…a value that is NEITHER the shipped default NOR a superseded one is still REFUSED"
M7 §17 guard given the old margin as a tolerance: price < 0 → price < -0.06 app/arbitrage.py +4 B RED — 8 named checks, incl. both new 2c ones ("at the SUPERSEDED 0.06 … §17 REFUSES", "a spot hour at −0.14836 … is still refused") and SAFETY-26's ε-sweep
M8 config.yaml default reverted to 0.06, mirror left alone config.yaml −3 B RED — CFG-03's tripwire, test_price.py §1 "…each shipped default equals the in-code mirror"
M9 the per-DSO warning loses its needle in the Supervisor description translations/en.yaml +4 B RED — test_translations.py "Uncited: ['price_taxes_eur_kwh does not cite 'YOUR OWN DSO'']"
M10 ⚠️ rows AND default AND config.yaml moved together to West-Vlaanderen's row (0.0670 / 0.13086) app/price.py + config.yaml +0 B / +0 B ⚠️ RED — but only on the card literal typed in the test file and the per-DSO band check. ok the 4 rows re-add to the shipped default: 0.13086 against 0.13086 — the re-sum check cannot see this, which is why §1b grades the total from two independent sides

Ten mutations, ten red, every revert fingerprinted against its pre-mutation
sha256. Two are +0 B (M2, M10) — the delta this repository treats as
risky — and both were produced under the :ro mount, where no .pyc can exist
to go stale.

What the battery cannot see, with sizes. test_price.py 1b grades the
default against the card as a number; it cannot grade the card against
Belgium. Two figures copied off the wrong month agree with each other perfectly
and pass — TARIFF-15's own §7 limit, one option over. And M1 left
test_validate.py green, correctly: that file reads the default out of the
dict, so it is insensitive to its value by design and the CFG-03 pin is what
covers it.

Suite

29/29 test files green, baseline established first at 7796f16 (29/29 before
any edit). Every run in the checks container with the worktree mounted :ro.

What is NOT in this branch

  • No version number. ## Unreleased only — the owner's decision, pending
    whether 0.9.0's image has been built and pulled.
  • No deploy. Nothing touched /addons/, the box or the inverter.
  • An existing install does not pick this up on the upgrade alone. The
    Supervisor's deepmerge keeps the stored 0.06, so the new default reaches a
    fresh install and an existing one only when its options are re-saved. Said
    plainly in the CHANGELOG rather than rounded off, and tools/predeploy_check.py --options <live blob> is what would confirm the live shape before deploying.

Do not merge.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU

## The number `price_taxes_eur_kwh` shipped at **0.06 EUR/kWh** with **no recorded provenance anywhere** — 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** (`app/price.py`'s `FLEMISH_TAXES_COMPONENTS_EUR_KWH`), not only here: | 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 €/kWh** | Eneco residential card for Flanders, September 2026, incl. 6 % VAT. The exact sum is shipped rather than the rounded 0.1174, so `test_price.py` section 1b can assert an **equality** rather than a tolerance. **The zero row is deliberate.** *"Vanaf 1 augustus 2026 bedraagt de energiebijdrage 0 EUR/MWh"* — it was merged into the excise. Carried as a **row** rather than an absence so the next person re-deriving this from an older guide finds it already accounted for instead of adding it a second time. `test_price.py` 1b grades the row's **presence**, because deleting it does not change the sum. **Why Antwerpen and not an average.** Only the distribution row moves between Fluvius DSOs (5,28 Midden-Vlaanderen → 6,70 West; total 0.117–0.131). The default is a **real DSO's real row**, and it sits near the **low** end on purpose: the two 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. ## What this does NOT do **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 none of it changes — `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 does do * **Every number an operator reads** — at half the true value the status page, `custom_formula`'s preview and the price history made the add-on look considerably better than it is. * **§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 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` **2c** drives both sides through the shipped `decide()` and pins the boundary **from below** as well. ## TARIFF-17's rung — what was found, and what was chosen **Nothing there hardcoded `0.06`.** `validate.py` read `PRICE_OPTION_DEFAULTS["price_taxes_eur_kwh"]`, so moving the default moves what the rung refuses — a behaviour change rather than a drift, which is why it had to be decided rather than noticed. The Supervisor sets `App.options = deepmerge(config.yaml defaults, stored options)` (`tools/supervisor_selfapi_probe.py` §D; the reference install's export is **70 of 71 keys**), so a saved blob carries `price_taxes_eur_kwh` whether the operator typed it or not. Moving the default turns **every stored `0.06` into "non-default" with nobody editing anything** — and a working Walloon or Brussels site with a `price_taxes_profile` would have met **`SystemExit(78)` on the upgrade**, over an inert number the table overrides anyway. **Chosen: "untouched" becomes a set.** `price.py` carries `SUPERSEDED_TAXES_DEFAULTS_EUR_KWH = (0.06,)` beside the default it supersedes, and `validate.py` reads a superseded default as untouched too. The rung's subject is a number the **operator typed**; a default this add-on wrote into the blob is not one. **The rung is not weakened** — 0.1198 beside a table is still refused by name, and the refusal now says which figures it accepts. Graded by four new checks plus a difference precondition (habit 1) and a non-empty-set guard, and `M5`/`M5b`/`M6` below drive each half red on its own. ## CFG-03 `config.yaml` and `PRICE_OPTION_DEFAULTS` moved together; `test_price.py` section 1 and `test_defaults_pin.py` stayed green **by agreeing**, not by being widened — `M8` reverts `config.yaml` alone and section 1 goes red. ⚠️ One check had to be **rewritten rather than re-pointed**. `test_price.py` section 0's fourth check justified 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. The file now asserts **per figure** that neither shipped default is what it grades with, which is the property that was actually load-bearing (and is stronger than the tuple comparison it replaces, which one matching figure satisfied). ## Mutation ledger Harness: `/home/homelab/.cache/t18-mut/mutate.sh` — `mktemp` backup **per invocation** inside that directory (habit 18), the graded run in a container with the tree mounted **`:ro`** so no `__pycache__` can exist at all (TEST-15's structural immunity, stronger than `-B`), the mutated line `grep`-ed on disk before the run, and the **revert fingerprinted** against the pre-mutation `sha256` afterwards. Every revert verified. | # | mutation | file | byte delta | result | |---|---|---|---|---| | **M1** | default `0.11736` → `0.12` | `app/price.py` | −3 B | **RED** — `test_price.py` §1 mirror-vs-`config.yaml`, §1b *"the 4 rows re-add to the shipped default: 0.11736 against 0.12"*, §1b card-literal; `test_price_forecast.py` §7 *"…and it still holds TARIFF-18's Flemish figure"* | | **M2** | Fluvius row `0.0535` → `0.0528` (Midden-Vlaanderen's row) | `app/price.py` | **+0 B** ⚠️ | **RED** — §1b *"the 4 rows re-add to the shipped default: 0.11666 against 0.11736"*. The card-literal check stayed green, which is the point of having two sides | | **M3** | delete the `energiebijdrage` **zero** row | `app/price.py` | −79 B | **RED** — §1b *"the energiebijdrage is carried as a row worth ZERO rather than left out…"*. The **sum** check stayed green — deleting a zero changes no total, which is exactly why presence is graded separately | | **M4** | `SUPERSEDED_TAXES_DEFAULTS_EUR_KWH = (0.06,)` → `()` | `app/price.py` | −5 B | **RED** — `test_price.py` §1b *"…it is no longer the superseded **None**"*; `test_validate.py` difference-precondition, non-empty-set guard, and the refusal-sentence check. ⚠️ Both files read the tuple **defensively** so that emptying it is a *named* red rather than an `IndexError` inside an f-string — a crash takes a file red with nothing on the record saying which claim broke | | **M5** | `untouched` drops the **superseded** half | `app/validate.py` | −43 B | **RED** — `test_validate.py` *"…a blob that still carries the SUPERSEDED default 0.06 beside the table STARTS"* | | **M5b** | `untouched` drops the **shipped** half | `app/validate.py` | −13 B | **RED** — *"…the SHIPPED scalar beside the table is NOT refused"* + the accepted baseline. Both halves individually live; no masking (MUTATION.md TEST-16) | | **M6** | the rung stops refusing anything (`and False`) | `app/validate.py` | −47 B | **RED** — 4 named checks, including *"…a value that is NEITHER the shipped default NOR a superseded one is still REFUSED"* | | **M7** | §17 guard given the old margin as a tolerance: `price < 0` → `price < -0.06` | `app/arbitrage.py` | +4 B | **RED** — 8 named checks, incl. **both** new 2c ones (*"at the SUPERSEDED 0.06 … §17 REFUSES"*, *"a spot hour at −0.14836 … is still refused"*) and SAFETY-26's ε-sweep | | **M8** | `config.yaml` default reverted to `0.06`, mirror left alone | `config.yaml` | −3 B | **RED** — CFG-03's tripwire, `test_price.py` §1 *"…each shipped default equals the in-code mirror"* | | **M9** | the per-DSO warning loses its needle in the Supervisor description | `translations/en.yaml` | +4 B | **RED** — `test_translations.py` *"Uncited: ['price_taxes_eur_kwh does not cite 'YOUR OWN DSO'']"* | | **M10** | ⚠️ **rows AND default AND `config.yaml` moved together** to West-Vlaanderen's row (`0.0670` / `0.13086`) | `app/price.py` + `config.yaml` | **+0 B / +0 B** ⚠️ | **RED** — but *only* on the **card literal typed in the test file** and the per-DSO band check. `ok the 4 rows re-add to the shipped default: 0.13086 against 0.13086` — the re-sum check **cannot see this**, which is why §1b grades the total from two independent sides | Ten mutations, ten red, **every revert fingerprinted** against its pre-mutation `sha256`. Two are `+0 B` (**M2**, **M10**) — the delta this repository treats as risky — and both were produced under the `:ro` mount, where no `.pyc` can exist to go stale. **What the battery cannot see, with sizes.** `test_price.py` 1b grades the default against the card as a **number**; it cannot grade the card against Belgium. Two figures copied off the wrong month agree with each other perfectly and pass — TARIFF-15's own §7 limit, one option over. And `M1` left `test_validate.py` **green**, correctly: that file reads the default out of the dict, so it is insensitive to its value by design and the CFG-03 pin is what covers it. ## Suite **29/29 test files green**, baseline established first at `7796f16` (29/29 before any edit). Every run in the checks container with the worktree mounted `:ro`. ## What is NOT in this branch * **No version number.** `## Unreleased` only — the owner's decision, pending whether 0.9.0's image has been built and pulled. * **No deploy.** Nothing touched `/addons/`, the box or the inverter. * **An existing install does not pick this up on the upgrade alone.** The Supervisor's `deepmerge` keeps the stored `0.06`, so the new default reaches a fresh install and an existing one only when its options are re-saved. Said plainly in the CHANGELOG rather than rounded off, and `tools/predeploy_check.py --options <live blob>` is what would confirm the live shape before deploying. **Do not merge.** 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
admin added 1 commit 2026-09-13 16:25:11 +02:00
`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
admin merged commit 4dcd9b0fc6 into release/1.0 2026-09-13 16:33:38 +02:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: admin/goodwe-addon#86