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.
|
"""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
|
A provider that names `FALLBACKS` (app/price.py) is read through here: first the
|
||||||
instead of straight from `load_prices()`. The order is fixed:
|
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)
|
`gate_for()` is the maintenance charge rule for such a site: the current
|
||||||
2. each id in its `FALLBACKS` (energy-charts.info)
|
15-minute price is at or below today's average, as the Home Assistant pair gave.
|
||||||
3. the prices the history store kept from the last fetch that worked
|
DECISIONS/PRICE-01-own-price-server.md says how equal, and where it is not."""
|
||||||
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@@ -74,13 +62,8 @@ class Resolved:
|
|||||||
|
|
||||||
|
|
||||||
class PriceWatch:
|
class PriceWatch:
|
||||||
"""Which step the prices come from, and one log line per change of it.
|
"""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."""
|
||||||
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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.step = None
|
self.step = None
|
||||||
@@ -123,20 +106,14 @@ async def resolve(opts: dict, since: datetime, until: datetime, transport,
|
|||||||
history, stamp: datetime, watch: PriceWatch,
|
history, stamp: datetime, watch: PriceWatch,
|
||||||
load) -> Resolved:
|
load) -> Resolved:
|
||||||
"""Walk the chain and return the first step that has a price for `stamp`.
|
"""Walk the chain and return the first step that has a price for `stamp`.
|
||||||
|
`load` is the caller's `load_prices`, so static_tariff.py stays the one module
|
||||||
`load` is the caller's `load_prices`, passed in so that `static_tariff.py`
|
that fetches. Never raises for a source condition: `no_data` names every step."""
|
||||||
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.
|
|
||||||
"""
|
|
||||||
pid = provider_id(opts)
|
pid = provider_id(opts)
|
||||||
cls = PROVIDERS[pid]
|
cls = PROVIDERS[pid]
|
||||||
reasons = []
|
reasons = []
|
||||||
for step, sid in enumerate((pid,) + tuple(cls.FALLBACKS)):
|
for step, sid in enumerate((pid,) + tuple(cls.FALLBACKS)):
|
||||||
# A fallback is asked for ITS OWN feed. The operator's feed override is
|
# A fallback is asked for ITS OWN feed; the operator's override is for
|
||||||
# for the first provider and would send energy-charts' request to a
|
# the first provider.
|
||||||
# URL it was never meant for.
|
|
||||||
o = opts if step == 0 else dict(opts, price_provider=sid,
|
o = opts if step == 0 else dict(opts, price_provider=sid,
|
||||||
price_feed_url="")
|
price_feed_url="")
|
||||||
read = await load(o, since, until, transport)
|
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",
|
series=lambda r: "price_history: it has no price for now",
|
||||||
no_data=lambda r: r.reason))
|
no_data=lambda r: r.reason))
|
||||||
watch.note(STEP_NONE, reasons, opts)
|
watch.note(STEP_NONE, reasons, opts)
|
||||||
# The reason is short on purpose: it is printed on the status page and in
|
# Short on purpose: it is on the status page and in a WARNING every
|
||||||
# a WARNING every refresh. What each step said is on the DEBUG line.
|
# refresh.
|
||||||
return Resolved(PriceRead.no_data(
|
return Resolved(PriceRead.no_data(
|
||||||
"no price source has a price for now"), STEP_NONE,
|
"no price source has a price for now"), STEP_NONE,
|
||||||
tuple(scrub(r, opts) for r in reasons))
|
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)
|
@dataclass(frozen=True)
|
||||||
class Gate:
|
class Gate:
|
||||||
"""What the 30 s tick takes from the standing plan.
|
"""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
|
||||||
`covered` is whether a published price covers now. When it is False,
|
the cover ended, or None."""
|
||||||
`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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
cheap: bool
|
cheap: bool
|
||||||
covered: bool
|
covered: bool
|
||||||
@@ -189,24 +162,16 @@ NO_PLAN = Gate(False, False, None, "No prices yet")
|
|||||||
|
|
||||||
|
|
||||||
def _round5(value: float) -> float:
|
def _round5(value: float) -> float:
|
||||||
# The Home Assistant template rounded the average to 5 decimals, and a
|
# The Home Assistant template rounded both sides to 5 decimals; so do we,
|
||||||
# price to 5 as well. Both sides are compared as it did, so a quarter that
|
# for the same answer.
|
||||||
# sits within 0.00001 EUR/kWh of the average gets the same answer.
|
|
||||||
return round(float(value), 5)
|
return round(float(value), 5)
|
||||||
|
|
||||||
|
|
||||||
def gate_for(published, now: datetime) -> Gate:
|
def gate_for(published, now: datetime) -> Gate:
|
||||||
"""The charge gate for `now`, from the series the chain published.
|
"""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,
|
||||||
THE RULE (owner's decision, PRICE-01): the current 15-minute price is at or
|
as `now_p <= avg_p` was on the HA pair (owner, PRICE-01). Not
|
||||||
below the average of today's prices, today being the Brussels day. This is
|
`TariffPlan.cheap_now()`. With no price for now the charge waits."""
|
||||||
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.
|
|
||||||
"""
|
|
||||||
if published is None:
|
if published is None:
|
||||||
return NO_PLAN
|
return NO_PLAN
|
||||||
if not published.covers(now):
|
if not published.covers(now):
|
||||||
|
|||||||
Reference in New Issue
Block a user