MAINT-20 part 1: price_chain.py comments trimmed
checks / unit (pull_request) Failing after 38m0s
infra-pin / fresh (pull_request) Successful in 2s

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
glenn schrooyen
2026-11-01 01:19:29 +01:00
co-authored by Claude
parent c432d4e419
commit 72ccfd788c
+26 -61
View File
@@ -1,27 +1,15 @@
"""PRICE-01 - where the prices come from when the first source does not answer.
A provider that names `FALLBACKS` (app/price.py) is read through this module
instead of straight from `load_prices()`. The order is fixed:
A provider that names `FALLBACKS` (app/price.py) is read through here: first the
provider (the owner's price server), then each id in `FALLBACKS`, then the
prices the history store kept from the last fetch that worked. A step counts
only if its answer holds a price for NOW (a server answering 200 with
yesterday's prices is as useless as one that is down). Other providers never
come here (`uses_chain()` is False).
1. the provider itself (the owner's price server)
2. each id in its `FALLBACKS` (energy-charts.info)
3. the prices the history store kept from the last fetch that worked
A step counts only if its answer holds a price for NOW. A server that answers
200 with yesterday's prices is as useless to a battery as one that is down, so
it is skipped, with the same log line.
Every other provider never comes here (`uses_chain()` is False for it), so
`generic_dayahead`, `manual_csv`, `custom_formula` and `off` take the road they
always took.
THE GATE. `gate_for()` is the maintenance charge rule for a site on such a
provider: the current 15-minute price is at or below today's average. It is the
rule the Home Assistant pair (`price_now_entity`, `price_avg_entity`) gave, read
from the provider's own prices, so moving a site from the pair to the provider
does not change WHEN the charge runs. DECISIONS/PRICE-01-own-price-server.md
says how equal, and where it is not.
"""
`gate_for()` is the maintenance charge rule for such a site: the current
15-minute price is at or below today's average, as the Home Assistant pair gave.
DECISIONS/PRICE-01-own-price-server.md says how equal, and where it is not."""
import logging
from dataclasses import dataclass
@@ -74,13 +62,8 @@ class Resolved:
class PriceWatch:
"""Which step the prices come from, and one log line per change of it.
The line is written when the step CHANGES and not before. Nothing repeats
while a source stays down, and nothing is written for a refresh that
changes nothing. The reasons (a status code, a timeout) go on a DEBUG line
beside it, with the token taken out.
"""
"""Which step the prices come from, and one log line per change of it (nothing
repeats while a source stays down). Reasons go on a DEBUG line, token removed."""
def __init__(self):
self.step = None
@@ -123,20 +106,14 @@ async def resolve(opts: dict, since: datetime, until: datetime, transport,
history, stamp: datetime, watch: PriceWatch,
load) -> Resolved:
"""Walk the chain and return the first step that has a price for `stamp`.
`load` is the caller's `load_prices`, passed in so that `static_tariff.py`
stays the one module that fetches prices. Never raises
for a source condition: a read that gets nowhere is a `no_data` whose
reason names every step, so the §11.4 row applies exactly as for a single
provider that failed.
"""
`load` is the caller's `load_prices`, so static_tariff.py stays the one module
that fetches. Never raises for a source condition: `no_data` names every step."""
pid = provider_id(opts)
cls = PROVIDERS[pid]
reasons = []
for step, sid in enumerate((pid,) + tuple(cls.FALLBACKS)):
# A fallback is asked for ITS OWN feed. The operator's feed override is
# for the first provider and would send energy-charts' request to a
# URL it was never meant for.
# A fallback is asked for ITS OWN feed; the operator's override is for
# the first provider.
o = opts if step == 0 else dict(opts, price_provider=sid,
price_feed_url="")
read = await load(o, since, until, transport)
@@ -158,8 +135,8 @@ async def resolve(opts: dict, since: datetime, until: datetime, transport,
series=lambda r: "price_history: it has no price for now",
no_data=lambda r: r.reason))
watch.note(STEP_NONE, reasons, opts)
# The reason is short on purpose: it is printed on the status page and in
# a WARNING every refresh. What each step said is on the DEBUG line.
# Short on purpose: it is on the status page and in a WARNING every
# refresh.
return Resolved(PriceRead.no_data(
"no price source has a price for now"), STEP_NONE,
tuple(scrub(r, opts) for r in reasons))
@@ -171,13 +148,9 @@ async def resolve(opts: dict, since: datetime, until: datetime, transport,
@dataclass(frozen=True)
class Gate:
"""What the 30 s tick takes from the standing plan.
`covered` is whether a published price covers now. When it is False,
`cheap` is False and the charge waits: no price is not a cheap price.
`since` is when the cover ended, for the status line, or None when nothing
was ever published.
"""
"""What the 30 s tick takes from the standing plan. `covered` False means `cheap`
is False and the charge waits (no price is not a cheap price). `since` is when
the cover ended, or None."""
cheap: bool
covered: bool
@@ -189,24 +162,16 @@ NO_PLAN = Gate(False, False, None, "No prices yet")
def _round5(value: float) -> float:
# The Home Assistant template rounded the average to 5 decimals, and a
# price to 5 as well. Both sides are compared as it did, so a quarter that
# sits within 0.00001 EUR/kWh of the average gets the same answer.
# The Home Assistant template rounded both sides to 5 decimals; so do we,
# for the same answer.
return round(float(value), 5)
def gate_for(published, now: datetime) -> Gate:
"""The charge gate for `now`, from the series the chain published.
THE RULE (owner's decision, PRICE-01): the current 15-minute price is at or
below the average of today's prices, today being the Brussels day. This is
what `now_p <= avg_p` was on the Home Assistant pair. It is NOT
`TariffPlan.cheap_now()`, which is strictly below the mean of the firm
hours of the whole plan, and it is not meant to be.
`published` is a series or None. With no price for now there is no gate
answer: the charge waits.
"""
"""The charge gate for `now`, from the series the chain published: the current
15-minute price is at or below the average of today's (Brussels day) prices,
as `now_p <= avg_p` was on the HA pair (owner, PRICE-01). Not
`TariffPlan.cheap_now()`. With no price for now the charge waits."""
if published is None:
return NO_PLAN
if not published.covers(now):