Skip to main content

☀️ KPower Forecast 📈

PyPI version Python versions CI License: AGPL-3.0 Code style: black

Production-grade solar production and power consumption forecasting.

Built with Facebook Prophet and powered by Open-Meteo. KPower Forecast provides a high-level API for training and predicting energy metrics with physics-informed corrections.


✨ Key Features

  • 🔋 Dual Mode: Specialized logic for both Solar Production and Energy Consumption.
  • 🌓 Night Masking: Physics-informed clamping using solar elevation to eliminate "ghost production" at night.
  • 🌡️ Weather Integration: Automatic fetching and resampling of temperature, cloud cover, and radiation.
  • 🌦️ Adaptive Weather Correction: Learns location/model-specific weather bias as forecast history accumulates.
  • ⚡ Optional Curtailment Limits: Clips delivered solar forecasts to inverter or export limits when configured.
  • 🤖 Prophet Optimized: Pre-configured regressors for maximum accuracy.
  • 💾 Smart Persistence: Automatic serialization of models to skip retraining when possible.
  • ❄️ Heat Pump Mode: Optional temperature correlation for energy consumption models.

🚀 Quick Start

Installation

# Core package
pip install kpower-forecast

# With CLI support (recommended for interactive use)
pip install "kpower-forecast[cli]"

# With the local Nixtla hybrid ML forecasting backend
pip install "kpower-forecast[ml]"

# With NeuralForecast / AI forecasting support
pip install "kpower-forecast[ai]"

🖥️ CLI Usage

KPower Forecast comes with a powerful CLI for interactive forecasting and visualization.

# Forecast solar production using Home Assistant CSV export
# Supports different data categories: instant_energy, cumulative_energy, power
# Supports different units: kWh, Wh, kW, W
# Optional delivered-energy curtailment limits can be supplied in kW
kpower-forecast solar rooftop-1 46.05 14.50 -i history.csv --category power --unit W --inverter-limit 10 --export-limit 7 --horizon 7

# Forecast power consumption
kpower-forecast consumption main-meter 46.05 14.50 -i history.csv --category cumulative_energy --unit kWh --horizon 3 --heatpump

CLI Features:

  • Automatic HA Parsing: Heuristic detection of last_changed and state columns.
  • Smart Data Normalization: Handles meter readings (cumulative), power (kW/W), and instant energy.
  • Heat Pump Mode: Enable --heatpump to correlate consumption with outdoor temperature.
  • Inconsistent Intervals: Robustly handles measurements with non-uniform time gaps.
  • Rich Tables: Beautiful daily summary tables in your terminal.
  • Terminal Graphs: Instant visualization of forecasts and confidence intervals via plotext.

☀️ Solar Production Forecast (API)

from kpower_forecast import KPowerForecast
from kpower_forecast.core import DataCategory, MeasurementUnit
import pandas as pd

# 1. Initialize for your location with specific data types
kp = KPowerForecast(
    model_id="rooftop_solar",
    latitude=46.0569,
    longitude=14.5058,
    forecast_type="solar",
    data_category=DataCategory.POWER,
    unit=MeasurementUnit.W,
    inverter_ac_limit_kw=10.0,
    grid_export_limit_kw=7.0,
)

# 2. Train with your history
# history_df = pd.DataFrame({'ds': [...], 'y': [...]})
# kp.train(history_df)

# 3. Predict the next 7 days
forecast = kp.predict(days=7)
print(forecast[['ds', 'yhat']].head())

🏠 Energy Consumption Forecast

kp_cons = KPowerForecast(
    model_id="house_meter",
    latitude=46.0569,
    longitude=14.5058,
    forecast_type="consumption",
    heat_pump_mode=True # Accounts for heating/cooling loads
)

🧠 Optional ML Forecasting Add-on

The core KPowerForecast API remains optimized for lightweight Prophet-based forecasting. For local classical ML workflows, install the ml extra and use KPowerMLForecast from the optional namespace:

from kpower_forecast.ml import KPowerMLForecast, MLBackendType, MLForecastType

kp_ml = KPowerMLForecast(
    model_id="house_meter_ml",
    latitude=46.0569,
    longitude=14.5058,
    forecast_type=MLForecastType.CONSUMPTION,
    backend=MLBackendType.NIXTLA_HYBRID,
)

# history_df = pd.DataFrame({'ds': [...], 'y': [...]})
# kp_ml.train(history_df, force=True)
forecast = kp_ml.predict(days=3)
print(forecast[["ds", "yhat", "yhat_lower_90", "yhat_upper_90"]].head())

The ML add-on uses Nixtla-compatible backends behind a small project-owned backend interface. NIXTLA_HYBRID combines a statsforecast structural baseline with an mlforecast/LightGBM residual learner and is suitable for local CPU-only controller deployments. NEURALFORECAST is also wired as a selectable Nixtla backend for users who install kpower-forecast[ai] and provide NeuralForecast model objects in backend_params. Future foundation-model adapters can plug into the same backend contract without changing the public API.

For PV forecasts, inverter_ac_limit_kw and grid_export_limit_kw cap the predicted interval energy to account for inverter clipping and static export curtailment. predict(dynamic_export_limits=...) also accepts a dataframe with ds plus export_limit_kw, grid_export_limit_kw, curtailment_limit_kw, or limit_kw for time-varying export controls.


Passive thermal response model

kpower_forecast.thermal is a separate, lightweight heating-regime API. It identifies a stable first-order effective response from pairs of real indoor observations at arbitrary spacing. Every pair carries coverage-gated HVAC electric input; by default a transition needs an hvac_coverage_ratio of at least 0.90 (the EMS adapter reports its least-covered five-minute bucket). train_with_weather obtains historical outdoor temperature from this package's weather client and integrates it across each actual observation interval; indoor targets are never interpolated onto a regular grid. The effective gain is a building response coefficient, not COP.

Training weather combines archive dates with the forecast endpoint's recent past_days window (one day by default, plus today). Future date padding never reaches the archive endpoint. Strict thermal callers preserve missing weather and reject uncovered observation intervals. Recent weather uses the forecast cache expiry rather than the long-lived archive cache; relative forecast requests also refresh at UTC midnight.

from kpower_forecast.thermal import KPowerThermalForecast, ThermalObservedTransition

model = KPowerThermalForecast(
    model_id="thermal_aggregate",
    authority_fingerprint="site-and-zone-configuration-epoch",
    storage_path="./data",
    latitude=46.0569,
    longitude=14.5058,
)
# observations: list[ThermalObservedTransition] from genuine indoor endpoints
# diagnostics = model.train_with_weather(observations)
# model.save()  # only when fitted; load() checks lineage and contract
# forecast = model.predict_with_weather(
#     origin=aligned_utc_origin,
#     initial_temperature_c=fresh_real_indoor_c,
#     hvac_electric_power_w=existing_heating_forecast_w,
#     hvac_drive_source="legacy_heating_forecast",
# )

Fit. Transitions are linked into chains; a gap of up to 30 minutes between two covered transitions (typically one rejected for HVAC coverage) is bridged by filling its inputs from the neighbouring intervals, while the reading that resumes the chain stays the real target. Overlapping 6-24 hour windows starting at real readings are simulated with the RC model, and the loss covers every real reading in each window (output error). The time constant is a grid search with a weak log-normal prior (median 60 h); gain and offset are bounded least squares on the simulated response. The one-step transition fit is also computed (transition_fit). When it is within the hard limits and replays the holdout better at the skill horizons, it is published instead and fit_method reads transition; on the reference site this was the case for one closed-loop TRV zone.

Quality. Acceptance needs the accepted-history gates (72 h, 40 transitions, excitation) and a holdout MAE at +3/+6/+12 h at least 5 % below persistence on two horizons. Sensors quantised to 0.1-0.2 °C make one-step error indistinguishable from persistence, so one-step metrics are reported but do not decide. A fit with a constrained coefficient, a boundary or flat time constant, or less history (at least 24 h) is published as quality="low_identifiability" with 1.5x wider bands, unless it is worse than persistence on the holdout, in which case it is not fitted. Hard limits (2-240 h, gain at most 20 °C/kW, |offset| at most 12 °C) still apply.

Artifacts. load() restores an artifact of the same model ID, lineage fingerprint, contract and config. One written by another package version or weather configuration loads with needs_retrain=True; callers should retrain on the same history rather than discard the fit.

Derived and naive forecasts. predict(..., equilibrium_shift_c=...) and evaluate_with_weather(..., equilibrium_shift_c=...) let a caller derive a zone from the aggregate fit and score that against the zone's own fit. predict_naive holds the current reading while fading a damped recent trend (recent_trend_c_per_hour); it is the model-free last fallback.

Training diagnostics include rejected durations/coverage, excitation, fitted time constant and gain, chronological holdout replay at 1/3/6/12 hours, persistence comparison, and empirical temperature error bands. EMS must additionally validate real future origins before granting any electrical forecast authority. The API never chooses HVAC modes or an HVAC electrical schedule.


Offline identification matrix (calibration only)

With the cli extra installed, run:

python -m kpower_forecast.thermal.identification private-bundle.json private-report.json

MatrixBundle is a strict Pydantic boundary. Its frozen manifest declares the model/source authority epoch, export start, fit cutoff, evaluation start (at least six hours after fitting), final export cutoff, input provenance, target tolerance and documented source confidence. Aware input timestamps normalize to UTC before window arithmetic and serialization. series contains all keys 5, 10, 15, 30, 60, even if empty. Each series is an independently admitted minimum-separation selection of genuine endpoints, with exact elapsed time, covered HP electrical exposure and pre-exported outdoor means. source_authority_id must describe the same selected authority at both endpoints. Data adapters own intermediate source, receipt, quality and exposure checks; this tool cannot reconstruct them from rows. Never rename five-minute rows to manufacture a longer-resolution experiment.

The report includes all 7/14/21-day configurations and an unweighted/runtime baseline versus diagnostic confidence/elapsed weighting. Unknown sensor accuracy uses a named manifest assumption (default 0.1 C standard deviation); documented quantization contributes resolution_c**2/12. The conservative two-endpoint variance bound does not assume independent endpoint errors. Confidence ratios are capped before elapsed-duration weighting. Precision does not establish accuracy.

The solver and physical acceptance limits are shared with the existing RC model. Rejected boundary/sign optima remain visible as diagnostics; tau remains bounded at 240 h. Neighbor comparisons expose coefficient changes and sign agreement, including rejected fits. Complete requested history must be available within the authority/export boundary; short datasets are explicitly unavailable.

All physically accepted candidates replay the same chronological real calibration targets at 30 min, 1 h, 2 h and 4 h, using observed input exposure and a persistence baseline. A source/gap/value discontinuity breaks replay; no indoor interpolation is allowed. Unmatched targets stay unmatched. These are observed-drive response checks, not operational unseen-origin forecast evidence. Internal blocked holdout status is reported separately; it cannot establish empirical reliability.

This first slice excludes calibrated excitation/cycle scoring, disturbance classification, cycle-balanced weighting, final holdout selection and live-manager integration. It never saves an operational model, requests weather, modifies a controller or grants control/electrical authority. Keep private bundles/reports outside Git. Identical CLI reruns are harmless; different existing outputs are refused. Reports are completed and flushed in a private temporary sibling, then atomically published without replacing an existing final report.

Time and history contract

Forecast timestamps and the ds grid remain UTC. ML calendar and holiday features are derived temporarily in the configured IANA timezone. Artifact contract version 3 records that timezone and history policy version 2, so incompatible artifacts are retrained before prediction.

Callers can enable preserve_gaps. Power samples are then converted independently using the fixed interval duration, cumulative energy is differenced only across adjacent valid samples, and missing target intervals remain missing through normalization and feature construction.

Prediction weather for an elapsed prefix combines archive and recent-forecast weather. Only the seam between the two sources, at most one hour (the hourly archive ends at 23:00 while the recent forecast starts at 00:00), is interpolated in time; any other missing weather slot still fails grid alignment.

Telemetry with intermittent dropouts can set max_bridged_gap_intervals (default 0, off; requires preserve_gaps=True, power or instant-energy input and a non-solar target). Interior missing runs up to that many intervals are bridged linearly after normalization so a single dropped row no longer splits the latest structural segment. Longer, leading and trailing gaps stay missing. Bridged rows are excluded from weather-bias fitting and conformal calibration, the persisted baseline history keeps the original gaps, and the manifest records the limit and bridged row count.

Holdout candidate selection

Consumption and HVAC targets can set candidate_selection=True (default off; not for solar). Training then scores three candidates on the calibration holdout, each forecasting it from the same origin with the same historical weather, and serves the one with the lowest RMSE:

  • kpower_ml: the configured ML backend (ties keep it);
  • degree_hour_regression: ridge regression with per-local-hour intercepts, max(0, regression_base_temperature_c − T_out) (default 16 °C) and regression_extra_features (default shortwave radiation);
  • local_slot_weekday_class_median: the leakage-safe slot/weekday median.

RMSE, not MAE, decides because MAE rewards an always-off forecast of an on/off load. Prediction intervals are calibrated on the winner's holdout residuals. With fewer than min_selection_holdout_rows measured holdout rows (default 96) the ML model is kept and selection_reason is holdout_too_short. selected_candidate, candidate_metrics and the regression coefficients are persisted in the manifest. An artifact trained with different selection settings (candidate_selection, regression_base_temperature_c, regression_extra_features) is retrained.


🛠️ Advanced Configuration

Parameter Type Default Description
model_id str required Unique ID for model persistence
latitude float required Location Latitude
longitude float required Location Longitude
interval_minutes `int" 15 Data resolution (15 or 60)
storage_path str "./data" Directory for saved models
heat_pump_mode bool False Enable temperature regressor for consumption
adaptive_weather_correction bool True Learn weather correction from archived forecasts, with historical weather fallback
inverter_ac_limit_kw float | None None Optional inverter AC output limit in kW
grid_export_limit_kw float | None None Optional grid export limit in kW

Adaptive weather correction is conservative on new sites. Initial training works without historical forecast snapshots by falling back to archive weather, then improves as generated forecasts are archived and later matched with actual production.


🔢 Versioning

This project follows a custom Date-Based Versioning scheme: YYYY.MM.Patch (e.g., 2026.2.1)

  • YYYY: Year of release.
  • MM: Month of release (no leading zero, 1-12).
  • Patch: Incremental counter for releases within the same month.

Enforcement

  • CI Validation: Every Pull Request is checked against scripts/validate_version.py to ensure adherence.
  • Consistency: Both pyproject.toml and src/kpower_forecast/__init__.py must match exactly.

🧪 Development & Testing

We use uv for lightning-fast dependency management.

# Clone and setup
git clone https://github.com/akorenc/kpower-forecast
cd kpower-forecast
uv sync --all-extras

# Run tests
uv run pytest

# Linting
uv run ruff check .

📄 License

Distributed under the GNU Affero General Public License v3.0. See LICENSE for more information.


Made with ❤️ for a greener future.

Metadata

Release files for kpower-forecast 2026.10.4

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for kpower-forecast 2026.10.4
File Size Uploaded
kpower_forecast-2026.10.4.tar.gz 273.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for kpower-forecast 2026.10.4
File Interpreter ABI Platform
kpower_forecast-2026.10.4-py3-none-any.whl Python 3 none any Details

Total release size: 383.4 kB

Release files / kpower_forecast-2026.10.4.tar.gz

Download URL kpower_forecast-2026.10.4.tar.gz
Size 273.5 kB
Tags Source
SHA-256 checksum
How to use checksums
6613cc58730550bd57489e1993a9b0c1cecbdfa72e1b5ac320befce6da5c241b
BLAKE2b-256 checksum
How to use checksums
de024bfb03d0514e4451111e0e7383c9204e557fa0f874340601494ad50d9024
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.22 {"installer":{"name":"uv","version":"0.12.22","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / kpower_forecast-2026.10.4-py3-none-any.whl

Download URL kpower_forecast-2026.10.4-py3-none-any.whl
Size 109.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c7457db2ec5e37c10373149862e9a753eaf1e1a158d80be845ff3b9b483bc799
BLAKE2b-256 checksum
How to use checksums
5e74cb909252496dc1f85c8fee805b6caee9f63b7a5714373d4c1db68012d4cd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.22 {"installer":{"name":"uv","version":"0.12.22","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page