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
@@ -330,7 +330,9 @@ from .inverter import roster_entries
|
||||
from .maintenance import MAINT_OPTION_DEFAULTS
|
||||
from .p1 import is_enabled, validate_meter_options
|
||||
from .peakguard import PEAK_OPTION_DEFAULTS
|
||||
from .price import PRICE_OPTION_DEFAULTS, PROVIDERS, RETIRED_PROVIDERS
|
||||
from .price import (
|
||||
PRICE_OPTION_DEFAULTS, PROVIDERS, RETIRED_PROVIDERS,
|
||||
SUPERSEDED_TAXES_DEFAULTS_EUR_KWH)
|
||||
from .static_tariff import (
|
||||
ProfileInvalid, TAXES_PROFILE_OPTION, parse_static_profile,
|
||||
parse_taxes_profile)
|
||||
@@ -1444,13 +1446,32 @@ def _price_problems(opts: dict) -> list[str]:
|
||||
# the price-entity/static_profile pair above. The table decides when it
|
||||
# is set, so a non-default scalar left beside it is a number the
|
||||
# operator typed that changes nothing, on the quantity §11.4 makes
|
||||
# hardest to notice. It is compared against the SHIPPED DEFAULT rather
|
||||
# hardest to notice. It is compared against the SHIPPED DEFAULTS rather
|
||||
# than against presence, because the Supervisor writes every schema key
|
||||
# into the options file whether the operator touched it or not.
|
||||
# into the options file whether the operator touched it or not - so
|
||||
# "the operator did not type this" has to be read off the VALUE.
|
||||
#
|
||||
# ⚠️ TARIFF-18: "UNTOUCHED" IS A SET, NOT ONE NUMBER, AND THE DEFECT
|
||||
# THAT MAKES IT ONE IS AN UPGRADE THAT STOPS A BATTERY CONTROLLER
|
||||
# STARTING. The Supervisor sets `App.options = deepmerge(config.yaml
|
||||
# defaults, stored options)`, so a saved blob carries every schema key
|
||||
# whether the operator typed it or not - the reference install's export
|
||||
# is 70 of 71. When TARIFF-18 moved this default 0.06 -> 0.11736, every
|
||||
# such blob went from "equal to the shipped default" to "not equal to
|
||||
# it" without anybody editing anything, and a site that also carries a
|
||||
# taxes table would have met `SystemExit(78)` on the upgrade. The
|
||||
# sentence below is about a number the OPERATOR TYPED; a superseded
|
||||
# shipped default is not one, so it still reads as untouched. The set
|
||||
# is in price.py beside the default it supersedes, because the two only
|
||||
# ever move together.
|
||||
scalar = _number(opts, "price_taxes_eur_kwh",
|
||||
PRICE_OPTION_DEFAULTS["price_taxes_eur_kwh"])
|
||||
shipped = PRICE_OPTION_DEFAULTS["price_taxes_eur_kwh"]
|
||||
if scalar is not None and abs(scalar - shipped) > 1e-12:
|
||||
untouched = (shipped,) + tuple(SUPERSEDED_TAXES_DEFAULTS_EUR_KWH)
|
||||
superseded = ", ".join(
|
||||
str(u) for u in SUPERSEDED_TAXES_DEFAULTS_EUR_KWH)
|
||||
if scalar is not None \
|
||||
and not any(abs(scalar - u) <= 1e-12 for u in untouched):
|
||||
problems.append(
|
||||
f"{TAXES_PROFILE_OPTION} is set AND price_taxes_eur_kwh is "
|
||||
f"{scalar} rather than its shipped {shipped}. THE TABLE WINS: "
|
||||
@@ -1458,7 +1479,10 @@ def _price_problems(opts: dict) -> list[str]:
|
||||
f"the scalar, so the number you typed there would decide "
|
||||
f"nothing and nothing would say so. Set price_taxes_eur_kwh "
|
||||
f"back to {shipped}, or clear {TAXES_PROFILE_OPTION} to keep "
|
||||
f"the flat figure.")
|
||||
f"the flat figure. (A scalar still sitting on a SUPERSEDED "
|
||||
f"default - {superseded} - is accepted and NOT refused: "
|
||||
f"that is a number this add-on wrote into your options "
|
||||
f"blob, not one you typed.)")
|
||||
|
||||
# ⚠️ THE UNITS RUNG, AND IT IS A SECOND QUANTITY RATHER THAN A
|
||||
# RESTATEMENT. Every Belgian DSO prints these bands in c EUR/kWh
|
||||
|
||||
Reference in New Issue
Block a user