Files
goodwe-addon/goodwe_controller/DECISIONS/PRICE-01-own-price-server.md
T
glenn schrooyenandClaude 0801776645
checks / unit (pull_request) Successful in 39m0s
infra-pin / fresh (pull_request) Successful in 2s
PRICE-01 round 2: the token on no page, fixed-tariff note, token length, F3 words, test gaps
F1: every password option is shown as ******** (set) or empty in /config/model
(options, running, ui) and in a stale refusal; the classic page draws it as a
password field; a save or validate that sends ******** or nothing keeps the
stored value. F2: a static_profile beside price_service is said once at start
(notification and log), not refused. F3 (owner, 2026-10-14): the no-price row,
Home line and notification only while maintenance_enabled is on. F4: tests for
the reviewer's R01 and R10. F5: price_service_token must be 16+ characters.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
2026-10-14 18:56:51 +02:00

6.7 KiB

PRICE-01 - the owner's price server as the price source, with fallbacks

Status: for review. Tests: test_price_service.py. One option added, price_service_token (password). Follows TARIFF-10, whose section 4 this card answers for one provider. Design: the owner's price service note of 2026-10-05. Owner go 2026-10-05 and 2026-10-14.

What it is

price_provider: price_service reads GET https://prices.kammenstraatha.duckdns.org/v1/dayahead/BE with Authorization: Bearer <token>. The answer is the energy-charts layout plus covers, so the existing columnar parser reads it. The price is wholesale energy only; the framework adds markup and taxes, as for generic_dayahead.

Rules

  1. The host is a constant in the plugin, declared as its only egress host, HTTPS only. The operator's price_feed_url is not read by this provider. Cost: every client talks to the one host, and moving the server is a new add-on version. What replaces the per-site setting: the egress allow-list, TLS, and a token per client that the server can revoke.
  2. The chain is server, then energy-charts, then the saved prices (app/price_chain.py). A step counts only if it has a price for now: a server that answers with old prices is skipped like one that is down. A step down and a step up are each one log line, written when the step changes. The operator's price_feed_url is cleared for the energy-charts step, which is asked for its own feed.
  3. The saved prices are /data/price_history.json, the file PriceHistory already keeps (the last three Brussels days of what was published). They are the all-in prices as stored; a change of markup shows from the next fetch that works. A read from the store is not filed again.
  4. No price for now means no published price covers the present moment. Not "none for the coming hours": before about 13:00 tomorrow's prices are not published on any healthy day, and carried hours are already marked estimated. It sets one status row (Prices), one Home plan line (English and Dutch), and one notification (No prices, English and Dutch), taken down when a price returns. A start-up before the first refresh finished is not a gap. The notification is not re-asserted after a Home Assistant core restart (SAFETY-34 does that for latched safety alarms); the status row stays. Open for the owner: if "coming hours" should mean a look-ahead of N hours, that is a change to gate_for() and PriceWatch, not to the chain.
  5. The maintenance charge gate (owner's decision, 2026-10-14): the charge may run when the current 15-minute price is at or below today's average, today being the Brussels day, computed from the provider's prices. This is the rule the Home Assistant pair gave (price_service.yaml), so a site that moves from the pair to the provider does not change when the maintenance charge runs. It is not TariffPlan.cheap_now() (strictly below the mean of the firm hours of the whole plan). This replaces, for this provider only, TARIFF-10 section 4: its three reasons are answered as follows. (1) Two writers: the pair beside a provider stays refused; static_profile beside price_service is accepted and no longer writes cheap. (2) An expired or absent plan shuts the gate (the charge waits), as an unavailable HA sensor did. (3) The basis is the one above, not cheap_now().
  6. The token is a secret. password? in the schema; hidden on the settings page (uilayout.HIDDEN), which can keep price_service but not choose it; never put in a message by the plugin; taken out of any reason text by price.scrub(); the real transport reports only a status or an exception's name. validate.py refuses price_service with no token, a token with a space or line break, and one under 16 characters (so scrub(), which skips values under 4 characters, always has something to take out). Round 2: the token is in no page and no JSON answer of the web app. Every password option of the schema is shown as ******** (set) or empty, in /config/model (options, running, ui) and in a stale refusal; the classic page draws it as a password field; a save or a validate that sends ******** or nothing keeps the stored value, any other value replaces it. A token can therefore be changed but not cleared from the page.
  7. A fixed tariff beside price_service is said, not refused. It no longer drives the cheap window or the gate, so the add-on warns once at start (notification Fixed tariff not used and a log line).
  8. The no-price words show only with the maintenance charge on (owner, 2026-10-14): the status row, the Home line and the notification say 'maintenance waits', so with maintenance_enabled off none is shown.

Where the gate equals the Home Assistant pair, and where it does not

Equal: test_price_service.py section 8 feeds the same day both ways, to the rules in price_service.yaml (price at the quarter, average of today's quarters rounded to 0.00001 EUR/kWh, <=), over 6 days including both clock-change days, at three instants in every period, and through a real Controller for a whole day. Every answer is the same.

Not equal, by design or by arithmetic:

  • The comparison is made on all-in prices. With a flat taxes figure and a markup and taxes of at most five decimals the offset cancels and the answer is the same. With price_taxes_profile (an hour-varying levy) the all-in prices differ by hour, so the answer can differ from the wholesale comparison the HA sensors made. A site on that profile should check before moving.
  • A quarter within 0.00001 EUR/kWh of the average can differ if markup or taxes have more than five decimals.
  • The plan is retired when no refresh has succeeded for 90 minutes (TARIFF_MAX_AGE_S) and the gate shuts then. How long the HA pair kept an answer after its REST sensor stopped updating was not measured here.
  • The average is over the firm prices the provider holds for today. After a restart in the middle of a day with only the saved part of today, it is the average of that part.

Not covered

  • No real answer of the server was seen. No token was available. The layout is the design's; test_price_service.py uses a mock and, for the parser, the real energy-charts capture reshaped with the design's extra keys. The first capture from the live server (rig test) should replace the mock.
  • The 429 limit (about 60 calls an hour) is not approached: one fetch per 30 minutes, and at most two requests when the server fails.
  • Battery modes read the plan as for generic_dayahead and were not changed.
  • The price_service REST and template sensors in Home Assistant are not touched; they stop being read when the pair is cleared.