Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0b345b2
feat: record on scheduling jobs how they were created
Flix6x Jul 11, 2026
4c0c5db
feat: schedules as automations
Flix6x Jul 11, 2026
ac4c74a
docs: changelog entry for schedule automations
Flix6x Jul 11, 2026
5e86232
merge: synchronize schedule automations with reviewed parent
BelhsanHmida Aug 5, 2026
bc1fc32
data/models: allow schedule automations without generators
BelhsanHmida Aug 5, 2026
2f4afe2
cli/tests: cover schedule automation validation
BelhsanHmida Aug 5, 2026
d3c3082
data/tests: cover schedule automation dispatch
BelhsanHmida Aug 5, 2026
9505e2b
api/v3_0/tests: cover schedule job provenance
BelhsanHmida Aug 5, 2026
5de1f20
ui/tests: cover automation type tabs
BelhsanHmida Aug 5, 2026
6bf6143
scheduling: harden automation dispatch
BelhsanHmida Aug 5, 2026
03f1b88
cli: reject forecast options for schedule automations
BelhsanHmida Aug 5, 2026
75b2be6
ui/assets: resize automation tables on tab changes
BelhsanHmida Aug 5, 2026
71a7196
docs: clarify schedule automation inputs
BelhsanHmida Aug 5, 2026
d39f60f
cli/tests: cover malformed automation YAML
BelhsanHmida Aug 5, 2026
5b04664
cli: report malformed automation YAML
BelhsanHmida Aug 5, 2026
1ec9324
data/tests: cover stored schedule flex configuration
BelhsanHmida Aug 5, 2026
90025d3
scheduling: load stored flex config for minimal triggers
BelhsanHmida Aug 5, 2026
2f0b09c
docs/scheduling: describe trigger propagation
BelhsanHmida Aug 5, 2026
1e53696
Merge the forecast automation base into the schedule automations
BelhsanHmida Aug 10, 2026
1f68beb
data/migrations: rejoin the two automation migration branches
BelhsanHmida Aug 11, 2026
a7671de
Merge reviewed forecast automation fixes
BelhsanHmida Aug 11, 2026
5af2170
fix(data/services): resolve schedule automation sensors
BelhsanHmida Aug 11, 2026
32c4970
test(data/services): cover schedule sensor resolution
BelhsanHmida Aug 11, 2026
a0a1944
test(api/v3_0): expose schedule dependency details
BelhsanHmida Aug 11, 2026
b8d76a9
cli: refuse forecaster options that were given, not merely ones that …
BelhsanHmida Aug 12, 2026
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/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ New features
* Automations - first roundtrip for forecasts: recurring tasks defined per asset, managed with new CLI commands (``flexmeasures add|edit|delete automation``), run by ``flexmeasures jobs run-automations``, and viewable in a new UI page and API endpoints (``[GET] /assets/(id)/automations``); an automation's details link to the sensors it reads from and writes to, and a sensor's page lists the automations feeding it; jobs now also record whether they were created via the CLI, the API or an automation [see `PR #2290 <https://www.github.com/FlexMeasures/flexmeasures/pull/2290>`_]
* In the UI, the full record of the data source selected on a sensor page can be inspected, backed by a new API endpoint (``[GET] /sources/(id)``) [see `PR #2290 <https://www.github.com/FlexMeasures/flexmeasures/pull/2290>`_]
* Let forecast automations use their own timezone and catch up only the latest missed occurrence after downtime, with skipped daylight-saving times handled once and repeated wall-clock times not duplicated [see `PR #2396 <https://github.com/FlexMeasures/flexmeasures/pull/2396>`_]
* 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>`_]
* ``flexmeasures show data-sources`` now shows which organisation a data source belongs to, and can list the sensors holding data recorded by a given source [see `PR #2401 <https://www.github.com/FlexMeasures/flexmeasures/pull/2401>`_]
* New ``inflexible-consumption`` and ``inflexible-production`` flex-context fields make explicit how the sign of each inflexible device's power data should be read (positive values denote consumption resp. production), accepting sensor references with optional source filters; they replace the now-deprecated ``inflexible-device-sensors`` field (bare sensor IDs, sign read from each sensor's ``consumption_is_positive`` attribute), which remains supported [see `PR #2358 <https://www.github.com/FlexMeasures/flexmeasures/pull/2358>`_]
* An inflexible (unschedulable) device can be modelled as its own asset by giving its flex-model entry a single ``inflexible-consumption`` or ``inflexible-production`` sensor reference; such a device joins a ``group`` like any other member, so its fixed (measured) load counts towards the group's intermediate power constraint [see `PR #2374 <https://www.github.com/FlexMeasures/flexmeasures/pull/2374>`_]
Expand Down
2 changes: 1 addition & 1 deletion documentation/cli/change_log.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ since v1.0.0 | July XX, 2026
* Add ``flexmeasures add plan``, ``flexmeasures show plans`` and ``flexmeasures edit plan``, to manage the rate limits and quotas which apply to the accounts on a plan.
* Add ``flexmeasures edit secret`` to store an encrypted secret on an account or asset.
* Add ``flexmeasures delete secret`` to remove an encrypted secret from an account or asset.
* Add ``flexmeasures add automation``, ``flexmeasures edit automation`` and ``flexmeasures delete automation`` to manage automations (recurring tasks on an asset; for now, computing forecasts).
* Add ``flexmeasures add automation``, ``flexmeasures edit automation`` and ``flexmeasures delete automation`` to manage automations (recurring tasks on an asset, computing forecasts or schedules).
* Add ``flexmeasures jobs run-automations`` to queue jobs for all automations that are due to run this minute from standard five-field cron expressions. Run this command once per minute. It makes at most one queueing attempt per automation per minute, including when an attempt fails after partially queueing jobs.
* Add ``--timezone`` to ``flexmeasures add automation`` and ``flexmeasures edit automation``. ``flexmeasures jobs run-automations`` now persists scheduling progress, catches up only the latest missed forecast occurrence, and handles skipped or repeated daylight-saving-time occurrences once. Failed or partially completed queueing attempts are still not retried automatically.
* ``flexmeasures show data-sources`` now shows the account a data source belongs to, and lists the sensors holding data recorded by a single source with ``--show-sensors``.
Expand Down
2 changes: 1 addition & 1 deletion documentation/cli/commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ of which some are referred to in this documentation.
``flexmeasures add annotation`` Add annotation to accounts, assets and/or sensors.
``flexmeasures add toy-account`` Create a toy account, for tutorials and trying things.
``flexmeasures add report`` Create a report.
``flexmeasures add automation`` Add a forecast automation with its own cron timezone.
``flexmeasures add automation`` Add an automation: a recurring task (computing forecasts or schedules) on an asset, with its own cron timezone.
================================================= =======================================


Expand Down
2 changes: 2 additions & 0 deletions documentation/features/forecasting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -277,3 +277,5 @@ The jobs record how they were created, which is shown on the asset's status page
Automations defined on an asset can be viewed on the asset's *Automations* page in the UI, and listed with the API endpoint `[GET] /assets/(id)/automations <../api/v3_0.html#get--api-v3_0-assets-id-automations>`_.
An automation's details show the sensors it reads from and writes to, linking to each sensor's page.
Conversely, a sensor's page lists the automations that write data to it.

Schedules can be automated in the same way — see :ref:`automating_schedules`.
23 changes: 23 additions & 0 deletions documentation/features/scheduling.rst
Original file line number Diff line number Diff line change
Expand Up @@ -368,3 +368,26 @@ Here are some thoughts on further innovation:
This is ongoing architecture design work, and therefore happens in development settings, until we are happy with the outcomes.
Thoughts welcome :)
- Aggregating flexibility of a group of assets (e.g. a neighborhood) and optimizing its aggregated usage (e.g. for grid congestion support) is also an exciting direction for expansion.


.. _automating_schedules:

Automating schedules
--------------------

Like forecasts, schedules can be computed on a recurring basis by an *automation* defined on the asset (see :ref:`automating_forecasts` for the full introduction, including how to run automations).
The automation's parameters form a schedule trigger message, as accepted by the `[POST] /assets/(id)/schedules/trigger <../api/v3_0.html#post--api-v3_0-assets-id-schedules-trigger>`_ API endpoint (without the asset id).
Use the canonical API field names, including ``flex-model``, ``flex-context`` and ``force-new-job-creation``.

Omit the ``start`` field to calculate it afresh from the server time on each run.
It is floored to the fixed, positive ``resolution`` when given, or otherwise to the minute.
A fixed ``start`` is accepted, but every run then schedules the same period and the CLI warns about this when creating the automation.
The ``duration`` must be positive; ``resolution`` does not accept nominal durations such as a month.
As usual, the flex-context and flex-model can also (partly) live on the asset itself, in which case a minimal trigger message suffices.

For example, this automation queues a scheduling job every hour, each time scheduling the next 12 hours:

.. code-block:: bash

echo 'duration: "PT12H"' > trigger-message.yml
flexmeasures add automation --asset 3 --name "Hourly schedules" --cron "0 * * * *" --type schedules --parameters trigger-message.yml
10 changes: 6 additions & 4 deletions flexmeasures/api/v3_0/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -1389,7 +1389,7 @@ def get_automations(self, id: int, asset: GenericAsset):
get:
summary: Get all automations defined on an asset.
description: |
The response will be a list of automations: recurring tasks (for now, computing forecasts)
The response will be a list of automations: recurring forecasting or scheduling tasks
defined on the asset. Each entry shows the automation's ID, when it was created,
its type, name, activation status, and its recurrence, both as a cron string
and described in natural language. Each entry also shows the IANA timezone in which its cron expression is interpreted and its persistent scheduling cursor.
Expand Down Expand Up @@ -1461,8 +1461,8 @@ def get_automation(self, id: int, automation_id: int, asset: GenericAsset):
summary: Get details of one automation defined on an asset.
description: |
In addition to the fields shown when listing automations, the response shows
the automation's parameters (for forecasts, these are the forecast parameters
used on each run), information about the data generator that runs it,
the automation's parameters (forecast parameters or a schedule trigger message),
information about its data generator (null for schedule automations),
the sensors it reads from and writes to,
and counts of recently created jobs, per job status.
Note that jobs in Redis have a limited TTL, so not all past jobs will be counted.
Expand Down Expand Up @@ -1979,10 +1979,11 @@ def trigger_schedule(
start=start_of_schedule,
end=end_of_schedule,
belief_time=belief_time, # server time if no prior time was sent
resolution=resolution,
flex_model=flex_model,
flex_context=flex_context,
)
if resolution is not None:
scheduler_kwargs["resolution"] = resolution
if sequential:
f = create_sequential_scheduling_job
else:
Expand All @@ -1992,6 +1993,7 @@ def trigger_schedule(
asset=asset,
enqueue=True,
force_new_job_creation=force_new_job_creation,
trigger={"origin": "API"},
**scheduler_kwargs,
)
except ValidationError as err:
Expand Down
1 change: 1 addition & 0 deletions flexmeasures/api/v3_0/sensors.py
Original file line number Diff line number Diff line change
Expand Up @@ -1075,6 +1075,7 @@ def trigger_schedule(
**scheduler_kwargs,
enqueue=True,
force_new_job_creation=force_new_job_creation,
trigger={"origin": "API"},
)
except ValidationError as err:
return unprocessable_entity(err.messages)
Expand Down
4 changes: 4 additions & 0 deletions flexmeasures/api/v3_0/tests/test_asset_schedules_fresh_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ def test_asset_trigger_and_get_schedule(
), "the whole scheduling job is handled as a single job (simultaneous scheduling)"
done_job_id = scheduled_jobs[0].id
scheduling_job = scheduled_jobs[0]
assert all(
job.meta["trigger"] == {"origin": "API"}
for job in [*scheduled_jobs, *deferred_jobs]
)

print(scheduling_job.kwargs)
if sequential:
Expand Down
63 changes: 63 additions & 0 deletions flexmeasures/api/v3_0/tests/test_automations_api_fresh_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,66 @@ def test_details_reject_inaccessible_sensor_metadata(

assert response.status_code == 403
assert hidden_sensor.name not in response.text


@pytest.mark.parametrize(
"requesting_user", ["test_prosumer_user@seita.nl"], indirect=True
)
def test_schedule_details_include_stored_flex_sensors(
client,
fresh_db,
setup_roles_users_fresh_db,
setup_generic_assets_fresh_db,
requesting_user,
):
asset = setup_generic_assets_fresh_db["test_battery"]
power_sensor = Sensor(
name="scheduled power",
unit="MW",
event_resolution=timedelta(minutes=15),
generic_asset=asset,
)
price_sensor = Sensor(
name="schedule price",
unit="EUR/MWh",
event_resolution=timedelta(hours=1),
generic_asset=asset,
)
fresh_db.session.add_all([power_sensor, price_sensor])
fresh_db.session.flush()
asset.flex_model = {
"consumption": {"sensor": power_sensor.id},
"soc-at-start": "2.5 MWh",
"soc-min": "0 MWh",
"soc-max": "5 MWh",
"power-capacity": "2 MW",
}
asset.flex_context = {
"site-power-capacity": "2 MVA",
"consumption-price": {"sensor": price_sensor.id},
}
automation = Automation(
asset=asset,
type="schedules",
name="Minimal schedule details",
cronstr="0 6 * * *",
parameters={"duration": "PT1H"},
)
fresh_db.session.add(automation)
fresh_db.session.commit()

response = client.get(
url_for(
"AssetAPI:get_automation",
id=asset.id,
automation_id=automation.id,
)
)

assert response.status_code == 200
assert response.json["input_sensors"] == [
{"id": price_sensor.id, "name": price_sensor.name}
]
assert response.json["output_sensors"] == [
{"id": power_sensor.id, "name": power_sensor.name}
]
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def test_trigger_and_get_schedule(
len(app.queues["scheduling"]) == 1
) # only 1 schedule should be made for 1 asset
job = app.queues["scheduling"].jobs[0]
assert job.meta["trigger"] == {"origin": "API"}
print(job.kwargs)
assert job.kwargs["asset_or_sensor"]["id"] == sensor.id
assert job.kwargs["start"] == parse_datetime(message["start"])
Expand Down
Loading
Loading