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
2 changes: 2 additions & 0 deletions documentation/cli/change_log.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ since v1.0.0 | July XX, 2026
* Add ``flexmeasures add automation``, ``flexmeasures edit automation`` and ``flexmeasures delete automation`` to manage automations (recurring tasks on an asset, computing forecasts, schedules or reports).
* Add an ``--as-job`` flag to ``flexmeasures add report``, to queue a reporting job (processed by workers of the new ``reporting`` queue) instead of computing directly.
* Add ``flexmeasures jobs run-automations`` to queue jobs for all automations that are due to run this minute (run this once per minute, e.g. via cron).
* Add ``flexmeasures show report-templates`` to list prepared report templates, or print one in full (with ``--name``).
* Add a ``--template`` option to ``flexmeasures add report`` and ``flexmeasures add automation`` (type ``reports``), to start from a prepared report template (any ``--config``/``--parameters`` files and other options override it).

since v0.33.0 | June 01, 2026
=================================
Expand Down
1 change: 1 addition & 0 deletions documentation/cli/commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ of which some are referred to in this documentation.
``flexmeasures show data-sources`` List available data sources.
``flexmeasures show beliefs`` Plot time series data.
``flexmeasures show reporters`` List available reporters.
``flexmeasures show report-templates`` List prepared report templates, or print one in full.
``flexmeasures show schedulers`` List available schedulers.
``flexmeasures show chart`` Export charts to PNG or SVG.
``flexmeasures show forecasters`` List available forecasters.
Expand Down
48 changes: 48 additions & 0 deletions documentation/features/reporting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,51 @@ For example, this automation computes a report over each past day, every morning

flexmeasures add automation --asset 3 --name "Daily aggregation report" --cron "0 1 * * *" --type reports \
--reporter PandasReporter --config reporter-config.yml --parameters report-parameters.yml


.. _report_templates:

Report templates
--------------------

FlexMeasures ships with prepared report templates: ready-made report definitions (a reporter class, a complete reporter config and a parameters skeleton),
so you don't have to author a report definition from scratch. List them with:

.. code-block:: bash

$ flexmeasures show report-templates

Name Reporter Description
---------------- -------------------- ---------------------------------------------------------------------------------------------------------------
energy-costs ProfitOrLossReporter Energy costs over the reporting window, from a power/energy sensor and a consumption price sensor (costs are positive).
self-consumption PandasReporter Share of produced energy consumed on-site, from a production and a consumption sensor.

Print a template in full (e.g. to pipe it to a file and edit it):

.. code-block:: bash

$ flexmeasures show report-templates --name self-consumption > self-consumption.yml

In a template's parameters skeleton, you fill in your own sensors by replacing the ``FILL_IN`` placeholders
(a clear validation error points out any placeholders you left unfilled).
The templates also recommend a rolling reporting window (``start-offset``/``end-offset`` fields, reporting on the previous day), for recurring use.

You can pass a template directly to ``flexmeasures add report`` or ``flexmeasures add automation --type reports`` with the ``--template`` option.
The template then acts as defaults: an explicitly given ``--reporter`` and any top-level keys in your ``--config``/``--parameters`` files override it,
and if you provide any timing fields yourself (``start``/``end``/offsets, in the parameters or as CLI options), the template's recommended timing fields are dropped.
For example, this sets up a daily self-consumption report in which only the sensors needed to be filled in:

.. code-block:: bash

$ echo "
input:
- name: production
sensor: 1
- name: consumption
sensor: 2
output:
- name: self-consumption
sensor: 3
" > parameters.yml
$ flexmeasures add automation --asset 3 --name "Daily self-consumption report" \
--cron "0 1 * * *" --type reports --template self-consumption --parameters parameters.yml
121 changes: 116 additions & 5 deletions flexmeasures/cli/data_add.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@
add_default_asset_types,
)
from flexmeasures.data.services.automations import create_automation
from flexmeasures.data.services.report_templates import (
PLACEHOLDER,
find_placeholders,
get_report_template,
list_report_templates,
merge_template_parameters,
)
from flexmeasures.data.services.data_sources import (
get_or_create_source,
get_data_generator,
Expand Down Expand Up @@ -1055,6 +1062,52 @@ def _assemble_forecaster_config_and_parameters(
return config, parameters


def _apply_report_template(
template_name: str,
reporter_class: str | None,
config: dict,
parameters: dict,
user_provided_timing: bool = False,
) -> tuple[str | None, dict, dict]:
"""Use a prepared report template as defaults for the reporter class, config and parameters.

Anything the user provided wins: an explicitly given reporter class is kept,
and top-level config/parameters keys override the template's.
"""
template = get_report_template(template_name)
if template is None:
available_templates = ", ".join(t["name"] for t in list_report_templates())
click.secho(
f"Unknown report template '{template_name}'."
f" Available report templates: {available_templates}.",
**MsgStyle.ERROR,
)
raise click.Abort()
reporter_class = reporter_class or template.get("reporter")
config = {**(template.get("config") or {}), **config}
parameters = merge_template_parameters(
template.get("parameters") or {},
parameters,
user_provided_timing=user_provided_timing,
)
return reporter_class, config, parameters


def _abort_on_unfilled_placeholders(config: dict, parameters: dict):
"""Abort with a clear validation error if template placeholders were left unfilled."""
placeholders = [f"config: {path}" for path in find_placeholders(config)] + [
f"parameters: {path}" for path in find_placeholders(parameters)
]
if placeholders:
click.secho(
f"Invalid report definition: replace the '{PLACEHOLDER}' template placeholders"
" with your own values (usually sensor IDs) in the following fields:\n- "
+ "\n- ".join(placeholders),
**MsgStyle.ERROR,
)
raise click.Abort()


@fm_add_data.command("forecasts")
@click.option(
"--resolution",
Expand Down Expand Up @@ -1260,6 +1313,15 @@ def add_forecast( # noqa: C901
help="Reporter class registered in flexmeasures.data.models.reporting or in an available flexmeasures plugin (only used for --type reports)."
" Use the command `flexmeasures show reporters` to list all the available reporters.",
)
@click.option(
"--template",
"template_name",
required=False,
type=click.STRING,
help="Name of a prepared report template to use as defaults for the reporter, config and parameters"
" (only used for --type reports). Any --config/--parameters files and other options override it."
" Use the command `flexmeasures show report-templates` to list all the available templates.",
)
@click.option(
"--source",
"source",
Expand Down Expand Up @@ -1296,6 +1358,7 @@ def add_automation(
inactive: bool = False,
forecaster_class: str = "TrainPredictPipeline",
reporter_class: str | None = None,
template_name: str | None = None,
source: DataSource | None = None,
config_file: TextIOBase | None = None,
parameters_file: TextIOBase | None = None,
Expand All @@ -1311,8 +1374,8 @@ def add_automation(
flexmeasures add automation --asset 3 --name "Hourly schedules"
--cron "0 * * * *" --type schedules --parameters trigger-message.yml
flexmeasures add automation --asset 3 --name "Daily self-consumption report"
--cron "0 1 * * *" --type reports --reporter PandasReporter
--config reporter-config.yml --parameters report-parameters.yml
--cron "0 1 * * *" --type reports --template self-consumption
--parameters report-parameters.yml

For forecasts and reports, the data generator configuration is stored on a
data source, and the parameters are validated and stored on the automation itself.
Expand All @@ -1328,6 +1391,19 @@ def add_automation(
kwargs, config_file, parameters_file
)

if template_name is not None:
if automation_type != "reports":
click.secho(
"The --template option is only supported for report automations (--type reports).",
**MsgStyle.ERROR,
)
raise click.Abort()
reporter_class, config, parameters = _apply_report_template(
template_name, reporter_class, config, parameters
)
if automation_type == "reports":
_abort_on_unfilled_placeholders(config, parameters)

# The service validates the parameters by automation type (we store them serialized)
try:
automation, warnings = create_automation(
Expand Down Expand Up @@ -1568,11 +1644,21 @@ def add_schedule( # noqa C901
@click.option(
"--reporter",
"reporter_class",
default="PandasReporter",
default=None,
type=click.STRING,
help="Reporter class registered in flexmeasures.data.models.reporting or in an available flexmeasures plugin."
help="Reporter class registered in flexmeasures.data.models.reporting or in an available flexmeasures plugin"
" (defaults to PandasReporter, or to the template's reporter if --template is given)."
" Use the command `flexmeasures show reporters` to list all the available reporters.",
)
@click.option(
"--template",
"template_name",
required=False,
type=click.STRING,
help="Name of a prepared report template to use as defaults for the reporter, config and parameters."
" Any --config/--parameters files and other options override it."
" Use the command `flexmeasures show report-templates` to list all the available templates.",
)
@click.option(
"--start",
"start",
Expand Down Expand Up @@ -1656,7 +1742,8 @@ def add_schedule( # noqa C901
"To process the job, run a worker (on any computer, but configured to the same databases) to process the 'reporting' queue. Defaults to False.",
)
def add_report( # noqa: C901
reporter_class: str,
reporter_class: str | None = None,
template_name: str | None = None,
source: DataSource | None = None,
config_file: TextIOBase | None = None,
parameters_file: TextIOBase | None = None,
Expand Down Expand Up @@ -1708,6 +1795,30 @@ def add_report( # noqa: C901
if edit_parameters:
parameters = launch_editor("/tmp/parameters.yml")

if template_name is not None:
reporter_class, config, parameters = _apply_report_template(
template_name,
reporter_class,
config,
parameters,
user_provided_timing=any(
timing_option is not None
for timing_option in (start, end, start_offset, end_offset)
),
)
if reporter_class is None:
reporter_class = "PandasReporter"

# Offset fields in the parameters (e.g. from a template) serve as fallbacks for the CLI options
start_offset = (
start_offset if start_offset is not None else parameters.get("start-offset")
)
end_offset = end_offset if end_offset is not None else parameters.get("end-offset")
parameters.pop("start-offset", None)
parameters.pop("end-offset", None)

_abort_on_unfilled_placeholders(config, parameters)

# check if sensor is not provided in the `parameters` description
if "output" not in parameters or len(parameters["output"]) == 0:
click.secho(
Expand Down
50 changes: 50 additions & 0 deletions flexmeasures/cli/data_show.py
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,56 @@ def list_reporters():
list_data_generators("reporter")


@fm_show_data.command("report-templates")
@with_appcontext
@click.option(
"--name",
"name",
required=False,
type=click.STRING,
help="Print the full YAML of the template with this name (e.g. to pipe it to a file).",
)
def show_report_templates(name: str | None = None):
"""
Show prepared report templates: ready-made report definitions to start from.

Fill in your own sensors (replacing the FILL_IN placeholders) and pass a template
to `flexmeasures add report` or `flexmeasures add automation --type reports`
using the --template option.
"""
from flexmeasures.data.services.report_templates import (
get_report_template_text,
list_report_templates,
)

if name is not None:
template_text = get_report_template_text(name)
if template_text is None:
click.secho(
f"Unknown report template '{name}'."
" Use `flexmeasures show report-templates` to list the available templates.",
**MsgStyle.ERROR,
)
raise click.Abort()
click.echo(template_text)
return

click.echo("Prepared report templates:\n")
click.echo(
tabulate(
[
(template["name"], template["reporter"], template["description"])
for template in list_report_templates()
],
headers=["Name", "Reporter", "Description"],
)
)
click.echo(
"\nUse `flexmeasures show report-templates --name <name>` to print a template in full,"
"\nor pass `--template <name>` to `flexmeasures add report` or `flexmeasures add automation --type reports`."
)


@fm_show_data.command("schedulers")
@with_appcontext
def list_schedulers():
Expand Down
Loading
Loading