Skip to content

Share forecasting and cheapest-window charging across Pricing modules - #757

Draft
MikeBishop wants to merge 4 commits into
ngardiner:mainfrom
MikeBishop:refactor/pricing-forecast-base
Draft

MikeBishop wants to merge 4 commits into
ngardiner:mainfrom
MikeBishop:refactor/pricing-forecast-base

Conversation

@MikeBishop

@MikeBishop MikeBishop commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

This change does options 1 and 2 from #755. Option 3, making Static and aWATTar leave out a price they don't know yet, waits on the discussion there, so this is a draft until that settles.

Shared base. ForecastingPricing lives in Pricing/Forecast.py, next to the window helpers it uses. It reads the module's config section (enabled, cheapWindow) and provides forecastMinPrice/forecastMaxPrice, cheapWindowStart, cheapWindowActive, getPriceForecast and getCheapestWindow. cheapWindowStart goes through latchedWindow, so a module can't search again mid-window and cut charging short. A module supplies its prices, updateForecast() to refresh, and _forecast(), which returns (slots, now, tz) without blocking. It also sets slotMinutes (default 30), so the base assumes no slot length and no refresh cadence. Amber, Tesla and aWATTar now use it.

Changes to Amber's behaviour:

  • forecastMinPrice/forecastMaxPrice are absent until the first forecast arrives, as Tesla's already were, so the base carries the "absent, not 0" rule forward.
  • The window is worked out when policy reads it, from the cached forecast. Reading it never fetches, because policy reads these values on the main loop. Only updateForecast fetches, from the background task or the HTTP API.
  • getPriceForecast always fetches 48 hours. Before, a ?hours=12 call could leave only 12 hours cached until the next fetch.

aWATTar. The market-data request it already makes returns the next 24 hours of prices, so aWATTar now uses the base too, with no extra requests: still at most one call per 15 minutes, within aWATTar's 100 a day. Its slots are an hour long; the API doesn't document this, so the module works the length out from the data, and 15-minute slots work too. The current price now comes from the slot covering the current time. It still reports 0 before its first price arrives; that is option 3.

One docs page. docs/modules/Pricing.md covers what the modules share: the values policy can read, cheapest-window charging, what each module reports before it has a price, and the HTTP endpoints. The page notes that Static, aWATTar and Amber currently report 0 for a price they don't know yet. Each module's page keeps only its own setup, forecast horizon and FAQ, and the README now lists the Pricing modules. The page also corrects one claim: an unknown price fails safe only in gt/gte/lt/lte comparisons. With ne, it matches.

Cheapest window kept inside its hours. cheapWindow.endHour is documented as the hour the window must finish by, but the search checked only the hour a window starts in. With {"hours": 5, "startHour": 22, "endHour": 7} it could pick 06:00–11:00, which on Amber's typical day (cheapest mid-morning, with solar) charges outside the night the user set. Every slot of the window must now fall in the range. With startHour alone, only the start is checked, so a window can still run past midnight. This was in the original Amber forecasting code, which is not in a release yet.

Evidence: full unit suite (pytest tests --ignore=tests/API --ignore=tests/integration --ignore=tests/scenarios): 2672 passed. New tests check that Amber's attribute reads never fetch, that its price range is absent before the first forecast, and that aWATTar builds its forecast and finds windows over hourly and 15-minute slots, and that a window ends by endHour. Not run against the live aWATTar or Amber APIs.

Changelog: none (forecasting and the Pricing module type are not in a release yet)

🤖 Generated with Claude Code

MikeBishop and others added 4 commits November 8, 2026 20:37
ForecastingPricing reads a module's config section and cheapWindow, and
provides forecastMinPrice/forecastMaxPrice, cheapWindowStart (latched once
under way), cheapWindowActive, getPriceForecast and getCheapestWindow. A
module supplies its prices, updateForecast() and a non-blocking
_forecast(). Amber and Tesla now use it.

Amber's forecast range is now absent until a forecast arrives, as Tesla's
already was, and its window is worked out when policy reads it rather than
only when a forecast is fetched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pricing.md now covers what the modules share: the values policy can read,
cheapest-window charging, what each module reports before it has a price,
and the HTTP endpoints. Each module's page keeps only its own setup, and
the README lists the Pricing modules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The market-data request aWATTar already makes returns the next 24 hours of
prices; the module kept only the first. It now keeps them all as its
forecast, so it gets cheapWindow, the forecast range and the forecast API
like Amber and Tesla, with no extra requests.

aWATTar's slots are an hour, not half an hour, and the API does not
document their length, so the base now takes the slot length from the
module (slotMinutes, default 30) and aWATTar reads it from the data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cheapWindow.endHour is the hour the window must finish by, but the search
checked only the hour a window starts in. With hours 5, startHour 22 and
endHour 7, it could pick 06:00-11:00, so a module whose cheapest prices
are mid-morning charged outside the night the user configured. Every slot
of the window must now fall in the range; with startHour alone, only the
start is checked, so the window may still run past midnight.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MikeBishop
MikeBishop force-pushed the refactor/pricing-forecast-base branch from 9ccff11 to 334ebea Compare November 9, 2026 03:00
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