PRICE-01: price_service provider, fallback chain, and the gate read from it
checks / unit (pull_request) Successful in 39m31s
infra-pin / fresh (pull_request) Successful in 1s

A new price source, `price_provider: price_service`: the owner's price server,
one fixed HTTPS host, a bearer token (new password option
`price_service_token`), wholesale energy only so markup and taxes are added as
for generic_dayahead.

The chain is server, then energy-charts, then the prices the history store
kept. A step counts only if it has a price for now. Each step down and back up
is one log line. With every source down a site plans from what it saved.

No price for now: one status row, one Home plan line and one notification, in
English and Dutch, cleared when a price returns. With this provider the
maintenance charge gate reads the provider's prices and keeps the Home
Assistant rule: the current 15-minute price is at or below today's average.

The token is hidden on the settings page, refused when missing, and taken out
of any error text. Other providers, `off` and the Home Assistant pair take the
road they always took (scripted-day probe against 0ce7d35: one line differs,
the refusal that lists the known providers). See
DECISIONS/PRICE-01-own-price-server.md.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
This commit is contained in:
glenn schrooyen
2026-10-14 16:22:24 +02:00
co-authored by Claude
parent 0ce7d35f80
commit 9ac3a9d2bc
21 changed files with 2169 additions and 32 deletions
+15
View File
@@ -582,6 +582,8 @@ def build_plan(c, now, shadow, running, held=False, settled=True):
cyc, waits = maintenance_runs(c, day0, day1, now, shadow)
runs += cyc
lines += waits
if getattr(c, "price_gap", False):
lines.append(line_no_prices(c.price_gap_since, now))
runs += peak_run(c, day0, day1, now, shadow, limit_w)
if c.p1_enabled:
# The guard's ceiling: the cap, or this month's peak once above it.
@@ -1268,6 +1270,13 @@ def line_cycle_waits(name) -> str:
return _t("cycle_waits", name=name)
def line_no_prices(since, now) -> str:
"""PRICE-01: no price covers now. `since` is an instant or None."""
if since is None:
return _t("no_prices_yet")
return _t("no_prices_since", at=messages.since_text(brussels(since), brussels(now)))
def line_peak_limit(limit_w, shadow) -> str:
return _w(shadow, "step_in", kw=_kw(limit_w))
@@ -1387,6 +1396,12 @@ def _samples() -> list:
("plan", run_house_why()), ("plan", run_house_why(True)),
("plan", run_house_amount()),
("plan", line_cycle_waits("Battery B")),
("plan", line_no_prices(None, datetime(2026, 10, 14, 9, 0,
tzinfo=timezone.utc))),
("plan", line_no_prices(datetime(2026, 10, 13, 22, 0,
tzinfo=timezone.utc),
datetime(2026, 10, 14, 9, 0,
tzinfo=timezone.utc))),
("plan", line_prices(0.16, {"eur_kwh": 0.14, "label": "13:00"},
{"eur_kwh": 0.36, "label": "19:00"})),
("plan", line_prices(None, None, None)),