MAINT-20 part 1: price_chain.py comments trimmed
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
c432d4e419
commit
72ccfd788c
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user