Repository navigation
Commit ff3587d
feat(spotprice): German supplier prices — Ostrom, Octopus Energy Germany, aWATTar, EWS (#5431)
* feat(spotprice): Ostrom provider
Read Ostrom's (DE) dynamic tariff from its public API with an OAuth2
client-credentials token (cached until shortly before expiry, renewed
once on a 401). The contract is looked up once per run: the single
active dynamic contract, or spotprice_ostrom_contract_id. Prices are
requested at the contract's postcode so they carry the local taxes,
levies and variable grid fees, and the documented per-kWh price
(grossKwhPrice + grossKwhTaxAndLevies, VAT included) is used as-is,
like Tibber. A reply with no levies at all is rejected.
Suppliers now share a generic "supplier" source with its own fetch
time, back-off and cache. With spotprice_provider unset, the one
supplier whose credentials are set is used; credentials for several
suppliers, or half an Ostrom pair, are a single configuration error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(spotprice): Octopus Energy Germany provider
Read the active electricity agreement of an Octopus Energy Germany
account from its Kraken GraphQL API (api.oeg-kraken.energy). The API
key is exchanged for a JWT through obtainKrakenToken(APIKey), cached
until shortly before its exp claim, sent without a Bearer prefix and
replaced once when Kraken stops accepting it. The account number is
looked up from the key unless spotprice_octopus_de_account is set.
Rates come from the agreement under properties -> electricityMalos:
a dynamic tariff's unitRateForecast slots as published, otherwise the
time-of-use timeslots repeated daily in local time, or a fixed unit
rate over the window, clipped to the agreement's validity. These are
gross unit rates and are used as-is, like Tibber. Every query was
checked against the live schema without credentials.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(spotprice): aWATTar (tado Energy HOURLY) provider
Add provider awattar: hourly spot prices for DE-LU and AT from the
public aWATTar market data API (api.awattar.de / api.awattar.at, no
key), the prices tado Energy's dynamic HOURLY tariff (formerly
aWATTar's) is billed on. It falls back to ENTSO-E (with a token) and
then Energy-Charts; other zones go straight to the fallbacks.
Add spotprice_markup_percent, a percentage of the absolute spot price
added before VAT, for any spot provider. It defaults to the published
3% for awattar and to 0 otherwise. The fixed 1.5 ct/kWh and the grid
fees stay in spotprice_markup.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(spotprice): EWS Schönau provider
Add provider ews for EWS Schönau's "Ökostrom Dynamisch": quarter-hourly
gross prices for today and tomorrow from EWS's dynamic price API
(api.ews-schoenau.de/v1/dynamicprices/EWS-OEKO-DYN), authenticated
with an X-API-Key header. EWS issues keys on request. The totals are
gross cents per kWh with grid fees and levies included and are used
as-is, like Tibber.
The response shape (today/tomorrow lists of startsAt/total) is only
known from third-party use, so a reply whose typical value looks like
euros rather than cents is rejected instead of pricing every interval
a hundred times too low.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(spotprice): fail loudly on ambiguous supplier replies
Octopus Energy Germany:
- Expand a forecast entry holding several time-of-use rates by its
timeslot rules instead of taking the first rate.
- Price a dynamic agreement only from its forecast. With no forecast
yet, record an error and publish nothing rather than a flat rate.
- Require spotprice_octopus_de_malo when active agreements sit on
several market locations. Hand over to the next agreement on the
location when the current one ends inside the window, and skip
revoked agreements.
- Expand timeslot rules in Europe/Berlin, not the local timezone.
- Clip forecast slots to the agreement's validity.
- Retry on KT-CT-1125 (token keys rotated) rather than 1139, per the
Kraken error code reference.
Ostrom: price ACTIVE contracts only; with none, report an error.
Cache: the name carries a digest of the configured Ostrom contract or
Octopus account and market location.
aWATTar: average quarter-hourly fallback prices to hourly, as the
HOURLY tariff bills.
Docs: an "Unverified assumptions" note under each supplier.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(spotprice): single-successor handover, import-only averaging, explicit flat rates
Octopus Energy Germany:
- Chain agreements one handover at a time: the single non-revoked
agreement starting at, or first after, the current end. Two
claiming the same start are an error, and overlapping rates are
refused rather than published.
- Without a forecast, price only an explicit fixed rate (Simple, not
a time-of-use product) or a time-of-use rate whose every price has
timeslots. Dynamic detection also reads the product's names and
contract type, as the schema has no market-linked flag.
- Map the repeated autumn hour to its first occurrence and skipped
spring times to the jump, so rule boundaries agree.
- KT-CT-1139 on a query is an auth error and is not retried.
Ostrom: ACTIVE matches in any case. When no contract has a status
field at all, the single dynamic contract is used with one warning.
aWATTar: average fallback quarter hours to hourly for import only;
export keeps the native quarter hours.
Cache: cached supplier prices carry the detected contract or account
and market location, and are dropped when detection finds another.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(spotprice): key the supplier cache on a digest of the credential
Swapping a Tibber token, Ostrom client ID, or Octopus Energy Germany or
EWS API key, with no contract or market location set, kept the same
cache name. The old account's prices were then restored and counted as
fresh for up to six hours. The cache name now carries a salted SHA-256
of the credential and selectors, truncated to 12 hex characters. The
credential itself is never written to the name or the stored data.
The Kraken JWT stays bare, as the evcc project's Octopus Energy
Germany tariff sends it; a test now pins that.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(spotprice): re-check the Ostrom contract daily and after a postcode error
The Ostrom contract and postcode were looked up once and kept until a
restart, so after a move the old postcode stayed priced. Without
spotprice_ostrom_contract_id, the contract is now looked up again once
a day, and straight after a 400/404/422 on /spot-prices, retrying once
with the new postcode. A changed contract or postcode drops the prices
held for the old one. With no active contract left, the usual "no
active Ostrom contract" error is raised and nothing is kept for the
old address. A configured contract is not re-checked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(spotprice): address German supplier review findings
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>1 parent 308c3cd commit ff3587d
7 files changed
Lines changed: 2495 additions & 66 deletions
File tree
- .cspell
- apps/predbat
- tests
- docs
- templates
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
| 46 | + | |
46 | 47 | | |
47 | 48 | | |
48 | 49 | | |
| |||
171 | 172 | | |
172 | 173 | | |
173 | 174 | | |
| 175 | + | |
174 | 176 | | |
175 | 177 | | |
176 | 178 | | |
177 | 179 | | |
178 | 180 | | |
179 | 181 | | |
| 182 | + | |
180 | 183 | | |
181 | 184 | | |
182 | 185 | | |
| |||
374 | 377 | | |
375 | 378 | | |
376 | 379 | | |
| 380 | + | |
| 381 | + | |
377 | 382 | | |
| 383 | + | |
| 384 | + | |
378 | 385 | | |
379 | 386 | | |
380 | 387 | | |
| |||
473 | 480 | | |
474 | 481 | | |
475 | 482 | | |
| 483 | + | |
476 | 484 | | |
477 | 485 | | |
478 | 486 | | |
| |||
578 | 586 | | |
579 | 587 | | |
580 | 588 | | |
| 589 | + | |
| 590 | + | |
581 | 591 | | |
582 | 592 | | |
583 | 593 | | |
| |||
655 | 665 | | |
656 | 666 | | |
657 | 667 | | |
| 668 | + | |
658 | 669 | | |
659 | 670 | | |
660 | 671 | | |
| |||
679 | 690 | | |
680 | 691 | | |
681 | 692 | | |
| 693 | + | |
682 | 694 | | |
683 | 695 | | |
684 | 696 | | |
| |||
767 | 779 | | |
768 | 780 | | |
769 | 781 | | |
| 782 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
583 | 583 | | |
584 | 584 | | |
585 | 585 | | |
586 | | - | |
587 | | - | |
| 586 | + | |
| 587 | + | |
588 | 588 | | |
589 | 589 | | |
590 | 590 | | |
591 | 591 | | |
592 | 592 | | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
593 | 600 | | |
| 601 | + | |
| 602 | + | |
594 | 603 | | |
595 | 604 | | |
596 | 605 | | |
| |||
601 | 610 | | |
602 | 611 | | |
603 | 612 | | |
604 | | - | |
605 | | - | |
606 | | - | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
607 | 617 | | |
608 | 618 | | |
609 | 619 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2871 | 2871 | | |
2872 | 2872 | | |
2873 | 2873 | | |
2874 | | - | |
| 2874 | + | |
2875 | 2875 | | |
2876 | 2876 | | |
2877 | 2877 | | |
2878 | 2878 | | |
| 2879 | + | |
| 2880 | + | |
| 2881 | + | |
| 2882 | + | |
| 2883 | + | |
| 2884 | + | |
| 2885 | + | |
2879 | 2886 | | |
| 2887 | + | |
2880 | 2888 | | |
2881 | 2889 | | |
2882 | 2890 | | |
| |||
0 commit comments