Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,23 @@ This can be used to retrieve the schedule, using:
The client will re-try until the schedule is available or the ``MAX_POLLING_STEPS`` of ``10`` is reached.


Forecasting
===========

Trigger a forecast for a sensor and wait for the result:

.. code-block:: python

forecast = await client.trigger_and_get_forecast(
sensor_id=<sensor_id>, # int
duration="PT24H", # ISO duration – how far ahead to forecast
)
# Returns e.g. {"values": [1.2, 1.5, ...], "start": "...", "duration": "PT24H", "unit": "kW"}

The client polls until the forecasting job is complete. For more advanced options
(training window, regressors, forecast frequency, etc.) see :doc:`forecasting`.


Development
==============

Expand Down
147 changes: 147 additions & 0 deletions docs/forecasting.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
.. _forecasting:

Forecasting
===========

The FlexMeasures Client supports the forecasting API endpoints introduced in
FlexMeasures v0.31.0:

- ``POST /sensors/<id>/forecasts/trigger`` — queue a forecasting job
- ``GET /sensors/<id>/forecasts/<uuid>`` — poll for results

These are exposed through three client methods:

- :meth:`trigger_forecast` — trigger and return the job UUID
- :meth:`get_forecast` — poll until results are ready
- :meth:`trigger_and_get_forecast` — convenience wrapper for both

.. note::

These endpoints require a FlexMeasures server of version **0.31.0** or above.


Basic example
-------------

Forecast the next 24 hours for a sensor, using server-side defaults for the
training window:

.. code-block:: python

import asyncio
from flexmeasures_client import FlexMeasuresClient

async def main():
client = FlexMeasuresClient(
host="localhost:5000",
ssl=False,
email="user@example.com",
password="password",
)

forecast = await client.trigger_and_get_forecast(
sensor_id=1,
duration="PT24H",
)
print(forecast)
# e.g. {"values": [1.2, 1.5, 1.8, ...], "start": "...", "duration": "PT24H", "unit": "kW"}

await client.close()

asyncio.run(main())


Specifying a forecast window
-----------------------------

Use ``start`` and ``end`` (or ``start`` and ``duration``) to define the exact
period to forecast:

.. code-block:: python

forecast = await client.trigger_and_get_forecast(
sensor_id=1,
start="2025-01-15T00:00:00+01:00",
end="2025-01-17T00:00:00+01:00",
)


Controlling the training window
---------------------------------

Pass training parameters inside a nested structure via the ``train_start``,
``train_period``, and ``retrain_frequency`` keyword arguments:

.. code-block:: python

forecast = await client.trigger_and_get_forecast(
sensor_id=1,
start="2025-01-15T00:00:00+01:00",
duration="PT48H",
# Training configuration
train_start="2025-01-01T00:00:00+01:00", # historical data start
train_period="P14D", # use 14 days of history
retrain_frequency="PT24H", # retrain every 24 h
)


Using regressors
----------------

You can improve forecast accuracy by supplying regressor sensor IDs:

.. code-block:: python

forecast = await client.trigger_and_get_forecast(
sensor_id=1,
duration="PT24H",
# Sensors whose *forecasts* matter (e.g. weather forecasts)
future_regressors=[10, 11],
# Sensors whose *measurements* matter (e.g. price history)
past_regressors=[20],
)


Step-by-step usage
-------------------

Trigger and retrieve separately to handle the job UUID yourself:

.. code-block:: python

# Step 1 – enqueue the forecasting job
forecast_id = await client.trigger_forecast(
sensor_id=1,
start="2025-01-15T00:00:00+01:00",
end="2025-01-17T00:00:00+01:00",
)
print(f"Job queued: {forecast_id}")

# Step 2 – poll until the job finishes
forecast = await client.get_forecast(
sensor_id=1,
forecast_id=forecast_id,
)
print(forecast)


Polling behaviour
-----------------

``get_forecast`` polls the server with a ``GET`` request and returns when the
server responds with HTTP 200. The polling respects the same client-level
settings as scheduling:

- ``polling_interval`` (default 10 s) — time between retries
- ``polling_timeout`` (default 200 s) — maximum total wait time
- ``max_polling_steps`` (default 10) — maximum number of poll attempts

Override them at client construction time:

.. code-block:: python

client = FlexMeasuresClient(
...,
polling_interval=5.0, # check every 5 seconds
polling_timeout=300.0, # wait up to 5 minutes
)
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Contents
:maxdepth: 2

Overview <readme>
Forecasting <forecasting>
Contributions & Help <contributing>
License <license>
Authors <authors>
Expand Down
71 changes: 20 additions & 51 deletions examples/HEMS/forecasting.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import subprocess

from const import (
FORECAST_HORIZON_HOURS,
FORECASTING_START,
Expand All @@ -22,17 +20,9 @@ async def generate_sensor_forecasts(
community_name: str,
regressors: list[tuple[str, str]] | None = None,
):
"""Generate forecasts using FlexMeasures CLI for the second week."""
"""Generate forecasts for the second week and wait until the job finishes."""
print(f"Generating {sensor_name} forecasts for {asset_name}...")

# Check if flexmeasures CLI is available
check_cmd = ["which", "flexmeasures"]
check_result = subprocess.run(check_cmd, capture_output=True, text=True)

if check_result.returncode != 0:
print("FlexMeasures CLI not found. Skipping forecast generation.")
return False

# Find sensors
sensor_mappings = [
# (key, sensor name, asset name)
Expand All @@ -58,48 +48,27 @@ async def generate_sensor_forecasts(
print("Could not find required sensors for forecasting")
return False

# Run CLI command
# NOTE: This uses the CLI because there is no public API yet.
# An API endpoint is coming soon, so this can later be done via the client.
# Requires FlexMeasures PR #1546.
cmd = [
"flexmeasures",
"add",
"forecasts",
"--sensor",
str(target_sensor["id"]),
"--train-start",
TUTORIAL_START_DATE,
"--from-date",
FORECASTING_START,
"--to-date",
SCHEDULING_END,
"--max-forecast-horizon",
f"PT{FORECAST_HORIZON_HOURS}H",
"--forecast-frequency",
f"PT{SIMULATION_STEP_HOURS}H",
"--ensure-positive",
"--model-save-dir",
"forecaster_models",
]

if regressor_sensors:
cmd.extend(
[
"--past-regressors",
",".join([str(sensor["id"]) for sensor in regressor_sensors]),
]
) # TODO: to be changed to --regressors when the sensor has irradiance forecasts

print(f"Running: {' '.join(cmd)}")
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
forecast_id = await client.trigger_forecast(
sensor_id=target_sensor["id"],
train_start=TUTORIAL_START_DATE,
start=FORECASTING_START,
end=SCHEDULING_END,
max_forecast_horizon=f"PT{FORECAST_HORIZON_HOURS}H",
forecast_frequency=f"PT{SIMULATION_STEP_HOURS}H",
past_regressors=[sensor["id"] for sensor in regressor_sensors]
if regressor_sensors
else None,
)
if forecast_id is not None:
print(f"Forecast triggered with ID: {forecast_id}")
await client.get_forecast(
sensor_id=target_sensor["id"],
forecast_id=forecast_id,
)
print(f"Forecast job completed for {sensor_name} on {asset_name}")

if result.returncode == 0:
print(f"{sensor_name} forecasts for {asset_name} generated successfully")
return True
else:
print(f"{sensor_name} forecasts for {asset_name} failed: {result.stderr}")
return False
return forecast_id


async def generate_forecasts(
Expand Down
Loading