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
1 change: 1 addition & 0 deletions documentation/api/change_log.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ API change log
v3.0-32 | July XX, 2026
""""""""""""""""""""""""

- Added ``POST /api/v3_0/assets/<id>/reports/trigger`` to queue a one-off reporting job (to be picked up by a worker processing the ``reporting`` queue), mirroring how forecasts and schedules are triggered. The job status can be polled via ``GET /api/v3_0/jobs/<uuid>``.
- Extended ``GET /api/v3_0/jobs/<uuid>`` with a ``result`` field containing ``unresolved`` and ``resolved`` arrays, each keyed by asset ID. For scheduling jobs, this surfaces soft state-of-charge constraint analysis: ``soc-minima`` and ``soc-maxima`` violations (with a ``violation`` magnitude) or satisfied constraints (with a ``margin`` headroom). Both arrays are empty when no SoC constraints were defined.

v3.0-31 | 2026-06-01
Expand Down
1 change: 1 addition & 0 deletions documentation/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ New features
* Automations can also compute schedules on a recurring basis (``flexmeasures add automation --type schedules``), with the schedule start defaulting to each run's time [see `PR #2293 <https://www.github.com/FlexMeasures/flexmeasures/pull/2293>`_]
* Automations can be created, edited and deleted in the UI and through new API endpoints (``[POST|PATCH|DELETE] /assets/(id)/automations``), by account admins and consultants [see `PR #2294 <https://www.github.com/FlexMeasures/flexmeasures/pull/2294>`_]
* Reports can run as background jobs (``flexmeasures add report --as-job``, processed by workers of the new ``reporting`` queue) and be computed on a recurring basis by automations, with a rolling report window expressed as Pandas offsets or defaulting to the last cron period [see `PR #2297 <https://www.github.com/FlexMeasures/flexmeasures/pull/2297>`_]
* One-off reporting jobs can be triggered via the API (``POST /assets/(id)/reports/trigger``), mirroring how forecasts and schedules are triggered [see `PR #2298 <https://www.github.com/FlexMeasures/flexmeasures/pull/2298>`_]
* Breaking behaviour change: the top-level flex-context's ``relax-constraints`` field now defaults to ``True`` (matching the default already used within each ``commodities`` entry), so constraint violations are softly penalized by default instead of being hard constraints, unless explicitly set to ``False`` [see `PR #2172 <https://www.github.com/FlexMeasures/flexmeasures/pull/2172>`_]
* In the UI, asset and sensor lists can be filtered by ID prefix through API-backed search fields [see `PR #2231 <https://www.github.com/FlexMeasures/flexmeasures/pull/2231>`_]
* Support configurable lower and upper bounds and snapping for forecast post-processing [see `PR #2273 <https://www.github.com/FlexMeasures/flexmeasures/pull/2273>`_]
Expand Down
25 changes: 25 additions & 0 deletions documentation/features/reporting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,31 @@ Automating reports
Reports can be queued as background jobs (add ``--as-job`` to ``flexmeasures add report``, and let a worker process the ``reporting`` queue, see :ref:`redis-queue`),
and computed on a recurring basis by an *automation* defined on the asset (see :ref:`automations` for the full concept, including how to manage and run automations).

One-off reporting jobs can also be triggered via the API, mirroring how forecasts and schedules are triggered.
``POST /api/v3_0/assets/<id>/reports/trigger`` takes the reporter class, its configuration and the report parameters (which must reference at least one output sensor, assumed to belong to the asset or one of its (grand)children):

.. code-block:: json

{
"reporter": "PandasReporter",
"config": {
"required_input": [{"name": "sensor_1"}, {"name": "sensor_2"}],
"required_output": [{"name": "df_agg"}],
"transformations": [
{"df_input": "sensor_1", "method": "add", "args": ["@sensor_2"], "df_output": "df_agg"},
{"method": "resample_events", "args": ["2h"]}
]
},
"parameters": {
"input": [{"name": "sensor_1", "sensor": 1}, {"name": "sensor_2", "sensor": 2}],
"output": [{"name": "df_agg", "sensor": 3}],
"start": "2023-04-10T00:00:00+00:00",
"end": "2023-04-10T10:00:00+00:00"
}
}

The response contains the id of the queued reporting job, whose status can be polled via the generic job status endpoint (``GET /api/v3_0/jobs/<uuid>``).

The reporter and its configuration are stored on a data source (steady across runs, so all report results attribute to the same source),
while the report parameters are stored on the automation itself and their timing is resolved freshly on each run:

Expand Down
2 changes: 2 additions & 0 deletions flexmeasures/api/v3_0/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
AutomationUpdateSchema,
)
from flexmeasures.data.schemas.generic_assets import GenericAssetSchema as AssetSchema
from flexmeasures.data.schemas.reporting import ReportTriggerSchema
from flexmeasures.data.schemas.sensors import QuantitySchema, TimeSeriesSchema
from flexmeasures.data.schemas.account import AccountSchema
from flexmeasures.api.v3_0.accounts import AccountAPIQuerySchema
Expand Down Expand Up @@ -163,6 +164,7 @@ def create_openapi_specs(app: Flask):
("AnnotationSchema", AnnotationSchema),
("AutomationCreationSchema", AutomationCreationSchema),
("AutomationUpdateSchema", AutomationUpdateSchema),
("ReportTriggerSchema", ReportTriggerSchema),
("CopyAssetSchema", CopyAssetSchema),
("DefaultAssetViewJSONSchema", DefaultAssetViewJSONSchema),
("AccountSchema", AccountSchema(partial=True)),
Expand Down
158 changes: 158 additions & 0 deletions flexmeasures/api/v3_0/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@
update_automation,
)
from flexmeasures.data.models.generic_assets import GenericAsset, GenericAssetType
from flexmeasures.data.models.reporting import Reporter
from flexmeasures.data.schemas.reporting import (
ReporterParametersSchema,
ReportTriggerSchema,
)
from flexmeasures.data.services.data_sources import get_data_generator
from flexmeasures.data.queries.generic_assets import (
filter_assets_under_root,
query_assets_by_search_terms,
Expand Down Expand Up @@ -2050,6 +2056,158 @@ def trigger_schedule(
d, s = request_processed()
return dict(**response, **d), s

@route("/<id>/reports/trigger", methods=["POST"])
@use_kwargs(
{"asset": AssetIdField(data_key="id")},
location="path",
)
# Simplification of checking for create-children access on each of the output sensors,
# which assumes each of the output sensors belongs to the given asset.
@permission_required_for_context("create-children", ctx_arg_name="asset")
@as_json
def trigger_report(self, id: int, asset: GenericAsset):
"""
.. :quickref: Assets; Trigger a one-off reporting job for this asset.

---
post:
summary: Trigger a one-off reporting job for this asset.
description: |
Trigger FlexMeasures to compute a report as a background job
(to be picked up by a worker processing the `reporting` queue),
mirroring how forecasts and schedules are triggered.

In this request, you can describe:

- the reporter class to use (see the CLI command `flexmeasures show reporters` for the available reporters)
- the reporter's configuration (static parameters, stored on the reporter's data source, e.g. the transformations of a `PandasReporter`)
- the report parameters (dynamic parameters, such as which `input` data to use, which `output` sensors to save the report to, and the `start` and `end` of the reporting window)

The asset in the URL path scopes permissions: triggering reports requires
`create-children` access to the asset. The report parameters must reference
at least one output sensor, which is assumed to belong to the given asset
(either directly or indirectly, by being assigned to one of the asset's (grand)children).

The response contains the id of the queued reporting job,
whose status can be polled via [GET /api/v3_0/jobs/(uuid)](#/Jobs/get_api_v3_0_jobs__uuid_).
Once the job finishes, the report is saved to the database
and can be retrieved from the output sensor(s).
security:
- ApiKeyAuth: []
parameters:
- in: path
name: id
required: true
$ref: '#/components/parameters/AssetIdPath'
requestBody:
content:
application/json:
schema: ReportTriggerSchema
examples:
aggregation_report:
summary: Sum two sensors into a report sensor
description: |
This message triggers a `PandasReporter` report that adds up the data
of sensors 1 and 2 and saves the result to sensor 3.
value:
reporter: PandasReporter
config:
required_input:
- name: sensor_1
- name: sensor_2
required_output:
- name: df_agg
transformations:
- df_input: sensor_1
method: add
args: ["@sensor_2"]
df_output: df_agg
parameters:
input:
- name: sensor_1
sensor: 1
- name: sensor_2
sensor: 2
output:
- name: df_agg
sensor: 3
start: "2023-04-10T00:00:00+00:00"
end: "2023-04-10T10:00:00+00:00"
responses:
200:
description: PROCESSED
content:
application/json:
schema:
type: object
examples:
successful_response:
description: |
This message indicates that the reporting request has been processed without any error.
A reporting job has been created with some Universally Unique Identifier (UUID),
which will be picked up by a worker.
The given UUID may be used to poll the job status: see [GET /api/v3_0/jobs/(uuid)](#/Jobs/get_api_v3_0_jobs__uuid_).
value:
status: PROCESSED
report: "364bfd06-c1fa-430b-8d25-8f5a547651fb"
message: "Request has been processed."
400:
description: INVALID_REQUEST, REQUIRED_INFO_MISSING, UNEXPECTED_PARAMS
401:
description: UNAUTHORIZED
403:
description: INVALID_SENDER
422:
description: UNPROCESSABLE_ENTITY
tags:
- Assets
"""
body = request.get_json(silent=True)
if not body:
return unprocessable_entity("No JSON data provided.")
try:
report_data = ReportTriggerSchema().load(body)
except ValidationError as e:
return unprocessable_entity(e.messages)
parameters = report_data["parameters"]
try:
ReporterParametersSchema().load(parameters)
except ValidationError as e:
return unprocessable_entity({"parameters": e.messages})
if not parameters.get("output"):
return unprocessable_entity(
{"parameters": {"output": ["At least one output sensor is required."]}}
)
# Guard against referencing sensors outside the caller's reach
# (raises Forbidden, answered with 403)
_check_automation_sensor_access("reports", parameters, {})
try:
reporter = get_data_generator(
source=None,
model=report_data["reporter"],
config=report_data["config"],
save_config=True,
data_generator_type=Reporter,
)
except ValidationError as e:
return unprocessable_entity({"config": e.messages})
if reporter is None:
return unprocessable_entity(
f"Reporter class `{report_data['reporter']}` not available."
" Use the CLI command `flexmeasures show reporters` to list all the available reporters."
)
reporter.set_job_trigger("API")
try:
returns = reporter.compute(as_job=True, parameters=parameters)
except ValidationError as e:
return unprocessable_entity({"parameters": e.messages})
except ValueError as e:
return unprocessable_entity(str(e))

response = dict(report=returns["job_id"])
d, s = request_processed()
return dict(**response, **d), s

@route("/<id>/kpis", methods=["GET"])
@use_kwargs(
{
Expand Down
Loading
Loading