Skip to content
Merged
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 @@ -44,6 +44,7 @@ v3.0-34 | November XX, 2026

- ``GET /api/v3_0/assets/<id>/jobs`` now accepts ``page`` and ``per-page`` to return jobs newest first, with ``num-records`` and ``filtered-records`` counts. Paginated results can be sorted by enqueue time or queue with ``sort-by`` and ``sort-dir``. Requests without ``page`` retain the full list and previous response shape.
- ``GET /api/v3_0/assets/<id>/jobs`` also lists the jobs of the assets below the asset, at any depth, as far as the caller may read them; pass ``include-child-assets=false`` to list only the asset's own jobs. Its entries include ``created_via`` provenance, and the automation that created a job only where the caller may read that automation.
- ``GET /api/v3_0/jobs/<uuid>`` reports, for a finished reporting or scheduling job, how many beliefs it computed and how many it recorded, per output sensor. A reporting job's ``result`` is an object holding an ``outputs`` list, and a scheduling job's ``result`` gains the same ``outputs`` list, next to its unchanged ``num-beliefs`` (the total of ``num-computed-beliefs`` across ``outputs``). Each entry holds the ``sensor`` (its ID), ``num-computed-beliefs`` and ``num-recorded-beliefs``. The recorded count leaves out beliefs that were already on record (and NaN values), so a rerun that only repeats what is on record records 0 of the beliefs it computed.
- ``GET /api/v3_0/assets/<id>/jobs`` renames its ``redis_connection_err`` field, which shipped in v1.0.0, to ``redis-connection-err``, as the automation endpoints spell it. It is read by FlexMeasures' own UI rather than by the FlexMeasures client.
- ``GET /api/v3_0/assets`` now scopes a listing the way its parameters say. ``num-records`` respects the ``root`` and ``depth`` constraints, where it used to count every asset in the account and asset-type scope. And an explicit ``include_public=false`` is now honoured, also next to ``all_accessible`` or ``root``; leaving the parameter out keeps the behaviour it had, where ``all_accessible`` and ``root`` include public assets and a listing of a single account does not.
- ``GET /api/v3_0/sensors/<id>/status`` now accepts an optional ``asset_id`` query parameter, naming the asset whose status page the sensor is reported on. It only affects the reported ``relation``, which used to claim that the sensor belongs to the asset shown, also for a sensor that is only listed because the asset's flex-context or graphs page refers to it. Reading the status of a sensor in the context of an asset requires read permission on that asset, too.
Expand Down
2 changes: 1 addition & 1 deletion documentation/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ Bugfixes

* An ``AggregatorReporter`` input that holds more than one belief about an event, such as one read with ``most-recent-beliefs-only: false``, is now refused with a message naming the sensor and the event, where it used to fail with a ``KeyError`` [see `PR #2741 <https://www.github.com/FlexMeasures/flexmeasures/pull/2741>`_]
* A ``PandasReporter`` with ``droplevels`` no longer fails with ``BeliefDataframe has more than one row per event`` when several data sources reported the same input event, as happens for a while when a job reporting on a rolling window gets a new data source; it keeps one belief per event, unless an input sets ``one_deterministic_belief_per_event`` to ``false`` [see `PR #2595 <https://www.github.com/FlexMeasures/flexmeasures/pull/2595>`_]
* The result of a reporting or scheduling job now counts the beliefs it saved rather than those it computed, so that a report's ``n_rows`` and a schedule's ``num-beliefs`` read 0 when a rerun only repeats what is on record [see `PR #2689 <https://www.github.com/FlexMeasures/flexmeasures/pull/2689>`_ and `PR #2571 <https://www.github.com/FlexMeasures/flexmeasures/pull/2571>`_]
* The results of reporting and scheduling jobs now list, per output sensor, the beliefs computed and those recorded, so that a rerun which only repeats what is on record reads as having recorded nothing, rather than as having computed nothing; a schedule's ``num-beliefs`` still totals the beliefs computed, now also on a CLI dry run [see `PR #2689 <https://www.github.com/FlexMeasures/flexmeasures/pull/2689>`_, `PR #2571 <https://www.github.com/FlexMeasures/flexmeasures/pull/2571>`_ and `PR #2822 <https://www.github.com/FlexMeasures/flexmeasures/pull/2822>`_]
* A forecaster registered by a plugin no longer needs to save its forecasts itself, but can return them instead: ``flexmeasures add forecasts`` now saves them, where it used to drop them while reporting them as created [see `PR #2689 <https://www.github.com/FlexMeasures/flexmeasures/pull/2689>`_ and `PR #2571 <https://www.github.com/FlexMeasures/flexmeasures/pull/2571>`_]
* Running the same forecast twice from the same start no longer fails on the beliefs it already recorded [see `PR #2688 <https://www.github.com/FlexMeasures/flexmeasures/pull/2688>`_]
* Whether a forecaster takes a regressor's belief as realized now follows the knowledge horizon of the regressor's sensor, as in timely-beliefs, so that the past readings of an instantaneous regressor (such as a thermometer read on the hour) and day-ahead prices (beliefs about future periods, which count as realized once the auction has cleared) are no longer kept from the model [see `PR #2692 <https://www.github.com/FlexMeasures/flexmeasures/pull/2692>`_]
Expand Down
3 changes: 2 additions & 1 deletion documentation/cli/change_log.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@ since v1.1.0 | November XX, 2026
* Add ``flexmeasures add report --as-job`` and the ``reporting`` worker queue for asynchronous one-off reports.
* Add ``flexmeasures add forecasts --dry-run`` to compute a forecast and see what it would record, without saving anything to the database. It cannot be combined with ``--as-job``.
* ``flexmeasures add report`` saves the reporter's config on its data source by default. As a report then records under a new source, anything referring to its old source by ID should be pointed at the new one after the first run. Pass ``--no-save-config`` to leave the config out and keep recording under the old source; ``--save-config`` is still accepted.
* ``flexmeasures add report`` saves all of a report's outputs or none of them, where each output used to be committed on its own, and says for each output how many beliefs it saved.
* ``flexmeasures add report`` saves all of a report's outputs or none of them, where each output used to be committed on its own, and says for each output how many beliefs it computed and how many of those it recorded.
* ``flexmeasures add forecasts`` saves the forecasts of a forecaster that a plugin registers, which used to be dropped while the command reported them as created.
* ``flexmeasures add forecasts`` no longer fails when it runs again from the same ``--from-date``, and says how many of the forecast beliefs it computed were new and saved.
* ``flexmeasures add forecasts`` no longer keeps every trained model as a file; pass ``--model-save-dir`` to keep them.

**Scheduling**

* ``flexmeasures add schedule`` says how many beliefs it computed and how many of those it recorded, so that a rerun which only repeats what is on record no longer reads as storing a new schedule.
* Fix ``flexmeasures add schedule --dry-run`` saving a schedule when combined with ``--as-job``: the combination is now rejected, and a dry run reports the beliefs and events it would have saved.

**Assets**
Expand Down
61 changes: 31 additions & 30 deletions flexmeasures/api/v3_0/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,30 +75,28 @@ def get_job_status(self, job_id: str, **kwargs):
get:
summary: Get the status of a background job
description: |
Look up a background job by its UUID and see whether it is
queued, running, finished, or failed.
Look up a background job by its UUID and see whether it is queued, running, finished, or failed.

The response includes a status message plus job metadata such
as the queue name, function name, timestamps, and the job
result when available.
The response includes a status message plus job metadata,
such as the queue name, function name, timestamps, and the job result when available.

Failed jobs also include traceback information when the worker
stored it with the job result.
Failed jobs also include traceback information when the worker stored it with the job result.

For a finished scheduling job, ``result`` is an object. For a
``StorageScheduler`` job it holds soft state-of-charge constraint
analysis: ``unresolved`` lists constraints the scheduler could not
satisfy, and ``resolved`` lists constraints that were satisfied
with some margin. Each device entry's ``soc-minima``/``soc-maxima``
value is a list, holding one entry per violated slot (for
``unresolved``) or per met slot with its margin (for ``resolved``),
ordered chronologically. Both arrays are empty when the flex model
defines no ``soc-minima``/``soc-maxima``, or when a scheduler other
than ``StorageScheduler`` was used. The ``num-beliefs`` field holds
the total number of beliefs (scheduled values) saved to the database.
This is the only place constraint analysis is available — the sensor
schedule endpoint (``GET /api/v3_0/sensors/<id>/schedules/<uuid>``)
returns power values only.
For a finished scheduling job, ``result`` is an object.
For a ``StorageScheduler`` job, it holds soft state-of-charge constraint analysis:
``unresolved`` lists constraints the scheduler could not satisfy,
and ``resolved`` lists constraints that were satisfied with some margin.
Each device entry's ``soc-minima``/``soc-maxima`` value is a list,
holding one entry per violated slot (for ``unresolved``) or per met slot with its margin (for ``resolved``), ordered chronologically.
Both arrays are empty when the flex model defines no ``soc-minima``/``soc-maxima``, or when a scheduler other than ``StorageScheduler`` was used.
This is the only place constraint analysis is available:
the sensor schedule endpoint (``GET /api/v3_0/sensors/<id>/schedules/<uuid>``) returns power values only.
The ``outputs`` field lists one entry per sensor the schedule records on:
the ``sensor`` (its ID), the number of beliefs (scheduled values) the scheduler computed for it (``num-computed-beliefs``),
and the number recorded in the database (``num-recorded-beliefs``), which leaves out beliefs that were already on record or had no value.
The ``num-beliefs`` field holds the total of ``num-computed-beliefs`` across ``outputs``.

For a finished reporting job, ``result`` is an object holding the same ``outputs`` field, with one entry per output sensor of the report.
security:
- ApiKeyAuth: []
parameters:
Expand Down Expand Up @@ -134,15 +132,14 @@ def get_job_status(self, job_id: str, **kwargs):
description: Human-readable description of the job status.
result:
description: >
Return value of the job function, or null when not yet
available. For a finished scheduling job, this is an
object; a ``StorageScheduler`` job populates it with
``unresolved``/``resolved`` soft state-of-charge
constraint analysis (empty arrays when the flex model
defines no ``soc-minima``/``soc-maxima``, or when a
scheduler other than ``StorageScheduler`` was used).
The ``num-beliefs`` field holds the total number of
beliefs (scheduled values) saved to the database.
Return value of the job function, or null when not yet available.
For a finished scheduling job, this is an object;
a ``StorageScheduler`` job populates it with ``unresolved``/``resolved`` soft state-of-charge constraint analysis
(empty arrays when the flex model defines no ``soc-minima``/``soc-maxima``, or when a scheduler other than ``StorageScheduler`` was used).
Its ``outputs`` field lists, per sensor the schedule records on, the ``sensor`` (its ID),
``num-computed-beliefs`` and ``num-recorded-beliefs``,
and its ``num-beliefs`` field holds the total of ``num-computed-beliefs`` across ``outputs``.
For a finished reporting job, this is an object holding the same ``outputs`` field.
nullable: true
func-name:
type: string
Expand Down Expand Up @@ -223,6 +220,10 @@ def get_job_status(self, job_id: str, **kwargs):
- datetime: "2024-01-01T10:15:00+00:00"
violation: "180.0 kWh"
resolved: []
outputs:
- sensor: 5
num-computed-beliefs: 96
num-recorded-beliefs: 96
num-beliefs: 96
func-name: "flexmeasures.data.services.scheduling.create_schedule"
origin: scheduling
Expand Down
14 changes: 13 additions & 1 deletion flexmeasures/api/v3_0/tests/test_jobs_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,10 @@ def test_get_job_status_finished(
result = data["result"]
assert result["unresolved"] == []
assert result["resolved"] == []
assert set(result.keys()) == {"unresolved", "resolved", "outputs", "num-beliefs"}
assert result["outputs"] == [
{"sensor": sensor.id, "num-computed-beliefs": 96, "num-recorded-beliefs": 96}
]
assert result["num-beliefs"] == 96
assert data["exc-info"] is None
assert_legacy_job_status_fields(data)
Expand Down Expand Up @@ -315,9 +319,17 @@ def test_get_job_status_finished_with_unresolved_soc_minima(

result = data["result"]
assert isinstance(result, dict)
assert set(result.keys()) == {"unresolved", "resolved", "num-beliefs"}
assert set(result.keys()) == {
"unresolved",
"resolved",
"outputs",
"num-beliefs",
}
assert isinstance(result["num-beliefs"], int)
assert result["num-beliefs"] > 0
assert result["num-beliefs"] == sum(
output["num-computed-beliefs"] for output in result["outputs"]
)

asset_id = add_battery_assets["Test battery"].id
unresolved_entry = next(
Expand Down
34 changes: 27 additions & 7 deletions flexmeasures/api/v3_0/tests/test_report_trigger_api_fresh_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,22 @@ def test_trigger_report_worker_stores_report_data(
# Adding the two hourly 0-9 input series yields 0, 2, ..., 18; two-hour mean resampling yields 1, 5, 9, 13, 17.
assert stored_report.values.T.tolist() == [[1, 5, 9, 13, 17]]

# The job's result, as the API hands it out, lists the report's output with what it computed and what it saved.
with app.test_client() as client:
job_response = client.get(
url_for("JobAPI:get_job_status", uuid=response.json["job"])
)
assert job_response.status_code == 200
result = job_response.json["result"]
assert set(result.keys()) == {"outputs"}
assert result["outputs"] == [
{
"sensor": sensors["output"].id,
"num-computed-beliefs": 5,
"num-recorded-beliefs": 5,
}
]


@pytest.mark.parametrize(
"requesting_user", ["test_prosumer_user@seita.nl"], indirect=True
Expand Down Expand Up @@ -126,13 +142,17 @@ def test_trigger_report_worker_reports_nothing_saved_for_misaligned_inputs(

job = app.queues["reporting"].fetch_job(response.json["job"])
assert job.get_status() == "finished"
# The count must exclude computed rows that persistence drops as NaN.
assert job.return_value() == [
{
"sensor_id": hourly_output.id,
"n_rows": 0,
}
]
# The saved count must exclude computed rows that persistence drops as NaN, while the computed count includes them:
# the two inputs do not align on source, so adding them gives a NaN for each of their 2 x 10 rows.
assert job.return_value() == {
"outputs": [
{
"sensor": hourly_output.id,
"num-computed-beliefs": 20,
"num-recorded-beliefs": 0,
}
]
}
assert any(
"produced no persistable values" in record.message for record in caplog.records
), "an empty report should be warned about"
Expand Down
38 changes: 30 additions & 8 deletions flexmeasures/cli/data_add.py
Original file line number Diff line number Diff line change
Expand Up @@ -2199,16 +2199,31 @@ def add_schedule( # noqa C901
**MsgStyle.SUCCESS,
)
else:
make_schedule(
scheduling_result = make_schedule(
asset_or_sensor=get_asset_or_sensor_ref(asset_or_sensor),
dry_run=dry_run,
**scheduling_kwargs,
)
if not dry_run:
click.secho("New schedule is stored.", **MsgStyle.SUCCESS)
n_computed = sum(
output["num-computed-beliefs"] for output in scheduling_result["outputs"]
)
n_recorded = sum(
output["num-recorded-beliefs"] for output in scheduling_result["outputs"]
)
if n_computed == 0:
click.secho(
"The scheduler computed no beliefs, so no new schedule was stored.",
**MsgStyle.WARN,
)
else:
if dry_run:
reason = ", as this was a dry run"
elif n_recorded < n_computed:
reason = ", as the rest was already on record"
else:
reason = ""
click.secho(
"The schedule above was computed but not stored (because of --dry-run).",
f"Scheduling done: computed {n_computed}, recorded {n_recorded}{reason}.",
**MsgStyle.SUCCESS,
)

Expand Down Expand Up @@ -2504,19 +2519,26 @@ def add_report( # noqa: C901
# The report was saved above, unless running in dry mode, so say what that save stored.
if not dry_run:
n_saved = saved[i]["n_rows"]
if n_saved:
n_computed = saved[i]["n_computed"]
if n_saved and n_saved == n_computed:
click.secho(
f"Success. Recorded {pluralize('belief', n_saved, include_count=True)} of the report for sensor `{sensor}` in the database.",
**MsgStyle.SUCCESS,
)
elif n_saved:
click.secho(
f"Success. Saved {pluralize('belief', n_saved, include_count=True)} of the report for sensor `{sensor}` to the database.",
f"Success. Recorded {n_saved} of the {pluralize('belief', n_computed, include_count=True)} computed for the report for sensor `{sensor}` in the database:"
" the rest was already on record or had no value.",
**MsgStyle.SUCCESS,
)
elif count_persistable_values(data):
click.secho(
f"The report for sensor `{sensor}` repeats beliefs already on record, so nothing new was saved.",
f"The report for sensor `{sensor}` repeats beliefs already on record, so nothing new was recorded (computed {n_computed}, recorded 0).",
**MsgStyle.SUCCESS,
)
else:
click.secho(
f"Nothing was saved for sensor `{sensor}`, as the report holds no values to save."
f"Nothing was recorded for sensor `{sensor}`, as the report holds no values to record (computed {n_computed}, recorded 0)."
" This can happen when its inputs do not align on source and belief time.",
**MsgStyle.WARN,
)
Expand Down
Loading
Loading