Skip to content

feat(spotprice): German supplier prices — Ostrom, Octopus Energy Germany, aWATTar, EWS - #5431

Open
mgazza wants to merge 9 commits into
feat/spotprice-componentfrom
feat/spotprice-suppliers
Open

mgazza wants to merge 9 commits into
feat/spotprice-componentfrom
feat/spotprice-suppliers

Conversation

@mgazza

@mgazza mgazza commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to #5340 (stacked on feat/spotprice-component; retarget to main once #5340 merges). Adds German supplier price sources to the spotprice component, so users of these suppliers get their actual prices rather than spot plus an estimated markup.

Provider Credentials Prices
ostrom OAuth2 client ID + secret from developer.ostrom-api.io All-in (grossKwhPrice + grossKwhTaxAndLevies at the contract's postcode); hourly
octopus_de Octopus Energy Germany API key (Kraken, api.oeg-kraken.energy) All-in, from the active agreement: dynamic forecast, time-of-use slots (expanded in Europe/Berlin), or a fixed rate
awattar none (public marketdata API, DE-LU and AT) Spot + spotprice_markup_percent (default 3%) + markup + VAT, matching the HOURLY tariff. Falls back to ENTSO-E/Energy-Charts, with import averaged to hourly and export kept at native resolution
ews API key from EWS Schönau (on request) All-in, quarter-hourly

Behaviour

  • Ostrom, Octopus DE and EWS share a "supplier" source with its own back-off and cache. The cache records which contract/account/MaLo it belongs to and is dropped when that changes.
  • Ambiguous replies fail loudly: the status shows an error and nothing is published rather than a plausible but wrong price. Cases covered:
    • a dynamic agreement with no forecast
    • several active MaLos without spotprice_octopus_de_malo
    • two agreements claiming the same start
    • no active Ostrom contract
    • overlapping rates
  • Kraken tokens are cached until expiry. On KT-CT-1111/1124/1125/1143 the token is renewed and the query retried once; KT-CT-1138/1139 are auth errors.
  • Provider inference: an explicit spotprice_provider always wins. Otherwise exactly one supplier's credentials selects it, and several is a config error.

New keys: spotprice_ostrom_client_id, spotprice_ostrom_client_secret, spotprice_ostrom_contract_id, spotprice_octopus_de_api_key, spotprice_octopus_de_account, spotprice_octopus_de_malo, spotprice_ews_api_key, spotprice_markup_percent.

Tests

70 spotprice tests (29 new), fixture-based, registered in the runner and mutation-checked. Live smoke:

  • aWATTar DE/AT prices fetched.
  • Kraken queries are schema-valid, and a bad key is reported as rejected.
  • Ostrom and EWS reject bad credentials.

No real Ostrom, Octopus DE or EWS accounts were available. Each provider's docs section lists its unverified assumptions.

🤖 Generated with Claude Code

mgazza and others added 8 commits October 7, 2026 21:43
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>
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>
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>
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>
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>
…licit 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>
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>
…de 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>
@mgazza
mgazza force-pushed the feat/spotprice-suppliers branch from 58da796 to b3d31d1 Compare October 7, 2026 20:47

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant