From 72ccfd788cfe4b3be1d4d8b242cf1971131595ce Mon Sep 17 00:00:00 2001 From: glenn schrooyen Date: Sun, 1 Nov 2026 01:19:29 +0100 Subject: [PATCH] MAINT-20 part 1: price_chain.py comments trimmed Co-Authored-By: Claude --- goodwe_controller/app/price_chain.py | 87 +++++++++------------------- 1 file changed, 26 insertions(+), 61 deletions(-) diff --git a/goodwe_controller/app/price_chain.py b/goodwe_controller/app/price_chain.py index bdf93e2..57b1d41 100644 --- a/goodwe_controller/app/price_chain.py +++ b/goodwe_controller/app/price_chain.py @@ -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):