Skip to content

feat(forecast): Open-Meteo + Forecast.Solar weather providers - #87

Merged
frahlg merged 1 commit into
masterfrom
feat/forecast-solar-and-open-meteo
Apr 17, 2026
Merged

frahlg merged 1 commit into
masterfrom
feat/forecast-solar-and-open-meteo

Conversation

@frahlg

@frahlg frahlg commented Apr 17, 2026

Copy link
Copy Markdown
Member

Direct fix for the operator report on homelab-rpi today: met.no says 49 % cloud, naive PV formula predicts 2.1 kW, reality is 8 kW. The RLS twin needs ~50 samples of matching features to self-correct — meanwhile the MPC undercommits. Using a forecaster that returns radiation or site-calibrated PV output sidesteps the cloud-fraction conversion entirely.

What's new

  • open_meteo — free, no key, ECMWF-driven. Returns hourly shortwave_radiation (W/m²), cloud_cover, temperature_2m. 16-day horizon. Radiation is a far better PV proxy than cloud fraction.
  • forecast_solar — purpose-built for residential PV. Free tier, 12 req/hour. Takes site geometry (lat/lon/tilt/azimuth/kWp) and returns site-calibrated watts per timestamp via EU PVGIS + weather. fetchAndStore uses the provider's PVWEstimated directly when present.

Signal precedence in fetchAndStore:

PVWEstimated (Forecast.Solar) > SolarWm2 × rated/1000 (Open-Meteo) > cloud-derated (met.no, openweather)

No default changed — sites stay on met.no until explicitly migrated via config.

Config

weather:
  provider: forecast_solar      # new option, or: open_meteo
  latitude: 56.7045
  longitude: 16.3353
  pv_rated_w: 10000
  pv_tilt_deg: 35               # forecast_solar needs geometry; open_meteo ignores
  pv_azimuth_deg: 180

Tests

  • Open-Meteo: happy-path parsing, null-field handling, non-200 error surfacing.
  • Forecast.Solar: bucket-to-hour aggregation with max-in-hour, URL geometry check, 429 rate-limit surfacing, zero-kWp rejection, sortByHour stability.
  • Existing forecast / e2e suite still green.

Test plan

  • go test ./... green (unit + e2e).
  • Deploy to homelab-rpi after merge, switch weather.provider: forecast_solar, observe pv_w_predicted track reality across multi-hour slots.

Follow-ups (not in this PR)

  • pvmodel should drop the (1 - cloud)^1.5 prior when radiation or direct-PV signal is present — the RLS is then a thin calibration on top.
  • Delete EstimatePVW once no sites use cloud-only providers.

🤖 Generated with Claude Code

Two new weather providers that give materially better PV-prediction
input than the cloud-cover-only sources (met.no, openweather):

- open_meteo: free, no key, ECMWF-driven. Returns hourly
  shortwave_radiation in W/m² alongside cloud cover and temperature.
  Radiation is a far better PV proxy than cloud fraction — "49 %
  cloud in the sky" can mean anything from full sun to overcast
  depending on which part of the sky the Sun is behind. fetchAndStore
  now derives pv_w_estimated from SolarWm2 × rated/1000 when it's
  populated, instead of the brittle (1 - cloud)^1.5 curve.

- forecast_solar: purpose-built for residential PV. Free tier is
  12 req/hour (rolling window) — ample for our 3-hour refresh cadence.
  Takes site geometry (lat/lon/tilt/azimuth/kWp) and returns
  site-calibrated watts per timestamp using the EU PVGIS clear-sky
  model + weather forecast. fetchAndStore uses PVWEstimated directly
  when present — the RLS twin becomes a thin calibration layer on top
  of an already-good prediction, not the sole calibration mechanism.

Signal selection in fetchAndStore is a simple precedence:
  PVWEstimated (direct)  >  SolarWm2 × rated/1000  >  cloud-derated prior

Config (new fields on Weather):
  - provider: open_meteo | forecast_solar (plus existing met_no, openweather)
  - pv_tilt_deg, pv_azimuth_deg: panel geometry for forecast_solar

Tests (8 new):
- Open-Meteo: happy-path radiation/cloud/temp parsing, null-field
  handling, non-200 error surfacing.
- Forecast.Solar: bucket-to-hour with max-in-hour aggregation, URL
  geometry verification, 429 rate-limit surfacing, zero-kWp rejection,
  sortByHour stability.

Motivation: operator report 2026-04-17 on homelab-rpi — met.no says
49 % cloud, naive formula predicts 2.1 kW, reality is 8 kW. The twin
can only self-correct once it has ~50 samples of the right features;
until then, the plan undercommits. Using radiation or direct PV output
sidesteps the problem: the conversion is either (a) already done by
Forecast.Solar with proper site parameters, or (b) a single calibration
factor on top of physics-measured radiation instead of a seven-feature
RLS over cloud fraction.

Rollout: the switch is per-site via config. No default changed; existing
sites stay on met.no until explicitly migrated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@frahlg
frahlg merged commit fdf461d into master Apr 17, 2026
1 check passed
@frahlg
frahlg deleted the feat/forecast-solar-and-open-meteo branch April 17, 2026 12:00

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2a2c6a7e19

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +124 to +125
out := make([]RawForecast, 0, len(buckets))
for hourMs, watts := range buckets {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Emit contiguous hourly rows for Forecast.Solar output

This provider currently returns only the hours that appear in doc.Result.Watts, but Forecast.Solar timestamps are irregular and can omit long spans (commonly overnight). In go/internal/mpc/service.go, lookupPV forward-fills with the previous row between forecast timestamps, so these gaps can carry the last daytime value into night slots and produce materially wrong charge/discharge plans. Please materialize a continuous hourly series (using 0 W for missing hours) before returning rows.

Useful? React with 👍 / 👎.

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