Skip to content
Open
2 changes: 2 additions & 0 deletions documentation/api/change_log.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ v3.0-32 | July XX, 2026
""""""""""""""""""""""""

- 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.
- Added a ``group`` field to the storage flex-model, accepted by the `/assets/(id)/schedules/trigger <../api/v3_0.html#post--api-v3_0-assets-id-schedules-trigger>`_ (POST) endpoint, referencing a power sensor representing a group of devices (e.g. a shared inverter or feeder). The group's ``power-capacity`` is enforced as a hard constraint on the group's aggregate power, while its ``consumption-capacity``/``production-capacity`` are enforced as soft constraints with default breach prices; the group's scheduled aggregate power is saved to the group sensor.
- The ``group`` field also accepts a ``{"asset": <id>}`` reference (besides ``{"sensor": <id>}``), pointing at an asset whose own (DB-stored) flex-model defines the group's constraints. Such a group defines no power sensor of its own; its aggregate schedule is instead saved via its ``consumption``/``production`` output sensor references, following the same conventions as any other asset-only flex-model entry. This lets the entire flex-model for a device tree (including groups) live in the DB, with ``flex-model`` omitted or empty on the trigger request.

v3.0-31 | 2026-06-01
""""""""""""""""""""
Expand Down
2 changes: 2 additions & 0 deletions documentation/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ New features
* CLI support for adding/editing account attributes [see `PR #2242 <https://www.github.com/FlexMeasures/flexmeasures/pull/2242>`_]
* Improve chart axis domain for event values not around zero, with a per-sub-chart ``y-axis`` option in ``sensors_to_show`` (default ``zero``, which pads the axis out to include zero) that can be set to ``data`` to fit a sub-chart's y-axis to the values shown, to an explicit ``[min, max]`` domain that the axis will cover at least (expanding to fit data beyond it), or to a strict ``{"min": min, "max": max}`` domain that the axis will never exceed (clamping data beyond it, with a warning when that happens), editable from the graph editor [see `PR #2244 <https://www.github.com/FlexMeasures/flexmeasures/pull/2244>`_]
* Extended ``GET /api/v3_0/jobs/<uuid>`` with a ``result`` field containing ``unresolved`` and ``resolved`` soft state-of-charge constraint analysis (``soc-minima``/``soc-maxima`` violations or satisfied constraints, keyed by asset ID) for scheduling jobs; both arrays are empty when no SoC constraints were defined [see `PR #2072 <https://www.github.com/FlexMeasures/flexmeasures/pull/2072>`_]
* Add support for intermediate power constraints on groups of devices, via a new ``group`` field in the storage flex-model [see `PR #2276 <https://www.github.com/FlexMeasures/flexmeasures/pull/2276>`_ and `issue #2092 <https://github.com/FlexMeasures/flexmeasures/issues/2092>`_]
* The ``group`` field now also accepts a ``{"asset": <id>}`` reference (in addition to ``{"sensor": <id>}``), allowing intermediate power constraints to be defined entirely from flex-models stored on the asset tree, with results saved via the group's ``consumption``/``production`` output sensors, without needing any flex-model in the scheduling trigger [see `issue #2092 <https://github.com/FlexMeasures/flexmeasures/issues/2092>`_]
* Extended the scheduling job ``result`` field with a ``num-beliefs`` field reporting the total number of beliefs (scheduled values) saved to the database [see `PR #2280 <https://www.github.com/FlexMeasures/flexmeasures/pull/2280>`_]
* Migrate the asset tree in the UI's Structure tab from Vega to ECharts, adding interactive pan/zoom navigation and refreshed node styling [see `PR #2025 <https://www.github.com/FlexMeasures/flexmeasures/pull/2025>`_]

Expand Down
7 changes: 2 additions & 5 deletions documentation/concepts/commitments.rst
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ It is possible to define what group of schedule values is expected to not deviat

The ``'device'`` attribute already lets us reduce from the values for all devices to just one.
The ``' _type'``attribute offers more powerful grouping options.
For now, this extra grouping can happen across different definitions of time slots, soon also per groups of devices.
For now, this extra grouping can happen across different definitions of time slots, and, for some cases, across groups of devices (see below).

- ``_type == 'each'``: penalise deviations per time slot (this is the default for time series).
- ``_type == 'any'``: treat the whole commitment horizon as one group (useful
Expand All @@ -124,10 +124,7 @@ For now, this extra grouping can happen across different definitions of time slo

.. note::

Near-term feature: support for **grouping over devices** is planned and
will be documented here. When enabled, grouping over devices lets you express
soft constraints that aggregate deviations across a set of devices,
for example, an intermediate capacity constraint from a feeder shared by a group of devices (via **flow commitments**), or multiple power-to-heat devices that feed a shared thermal buffer (via **stock commitments**).
Grouping over devices is now partly supported. An intermediate power constraint from a feeder or shared inverter can be modelled by adding a ``group`` field to the relevant devices' storage flex-model entries, referencing a power sensor for the group; the group's ``power-capacity`` is enforced as a hard constraint, while its ``consumption-capacity``/``production-capacity`` are enforced as soft constraints with default breach prices (via **flow commitments**). See :ref:`scheduling` for details. Multiple devices feeding a shared storage (e.g. power-to-heat devices feeding a shared thermal buffer) are also supported, via multiple feeder sensors on one storage flex-model. Fuller, more general support for grouping deviations across arbitrary sets of devices in commitments (including custom breach prices per group, and stock commitments over device groups) remains planned and will be documented here as it lands.


How flex-context fields are converted into commitments
Expand Down
39 changes: 39 additions & 0 deletions documentation/features/scheduling.rst
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,9 @@ For more details on the possible formats for field values, see :ref:`variable_qu
* - ``production-capacity``
- |PRODUCTION_CAPACITY.example| (only consumption)
- .. include:: ../_autodoc/PRODUCTION_CAPACITY.rst
* - ``group``
- |GROUP.example|
- .. include:: ../_autodoc/GROUP.rst

.. [#quantity_field] Can only be set as a fixed quantity.

Expand All @@ -268,6 +271,42 @@ For more details on the possible formats for field values, see :ref:`variable_qu

For more details on the possible formats for field values, see :ref:`variable_quantities`.


Intermediate power constraints
"""""""""""""""""""""""""""""""

In a multi-device flex-model list, a device entry may declare a ``group`` field referencing a group of devices, for example a hybrid inverter shared by a battery and PV installation, or a feeder shared by several devices. This lets you model an intermediate power constraint that sits between the individual devices and the site as a whole. The ``group`` field accepts exactly one of two references:

- ``{"sensor": <power sensor id>}``: the group is identified by a power sensor, which itself gets its own flex-model entry (typically passed alongside the device entries; mainly useful for API-passed flex-models).
- ``{"asset": <asset id>}``: the group is identified by the flex-model entry stored on that asset (typically a sub-EMS/asset in the asset tree, such as the inverter in the example below). Such a group entry defines no power sensor of its own; instead, like any other asset-only entry, it may define ``consumption`` and/or ``production`` output sensor references (see below) on which the group's aggregate power gets saved.

Either way, the group reference's target (sensor or asset) gets its own flex-model entry, defining constraints on the group's aggregate (summed) power:

- ``power-capacity`` on the group is a **hard** constraint (applied in both directions).
- ``consumption-capacity`` and ``production-capacity`` on the group are **soft** constraints, enforced with the same default breach prices used at the site level (10000 currency/kW); users cannot configure custom breach prices for groups.

The group's scheduled aggregate power is saved as a schedule output, following the same conventions used for any device's schedule output:

- If the group's flex-model entry has a ``sensor`` field, the aggregate power is saved directly to that sensor.
- Otherwise (an asset-only entry), the aggregate power is saved via its ``consumption`` and/or ``production`` output sensor references: with only ``consumption`` set, the full profile is saved consumption-positive; with only ``production`` set, the full profile is saved production-positive (i.e. sign-flipped before saving); with both set, the profile is split into its non-negative part (saved to ``consumption``) and its non-positive part (saved, as a positive magnitude, to ``production``).

Groups can be nested (a group entry may itself reference a parent group), but cyclic references are rejected. Groups require a multi-device flex-model; they are rejected when scheduling a single sensor.

Example, for a 2.5 kW hybrid inverter (sensor 5) shared by a battery (sensor 1) and PV installation (sensor 2), taken from `issue #2092 <https://github.com/FlexMeasures/flexmeasures/issues/2092>`_:

.. code-block:: json

[
{"sensor": 1, "power-capacity": "2 kW", "group": {"sensor": 5}},
{"sensor": 2, "production-capacity": "2 kW", "consumption-capacity": "0 kW", "group": {"sensor": 5}},
{"sensor": 5, "power-capacity": "2.5 kW"}
]

Here, the battery and PV installation may each individually schedule up to 2 kW, but their combined power flowing through the shared inverter is hard-limited to 2.5 kW.

The ``{"asset": <id>}`` variant lets you define the entire flex-model on the asset tree in the DB, with no flex-model needed in the scheduling trigger at all: each device asset carries its own (partial) flex-model, including a ``group`` field pointing at the parent asset that represents the shared equipment, and that parent asset's own flex-model defines the group's constraints and output sensor(s). Triggering a schedule for the top-level site asset with an empty (or omitted) ``flex-model`` then collects the full configuration from the tree. For a hands-on walkthrough (including how to store flex-models on assets, and where the resulting schedules end up), see :ref:`tut_toy_schedule_group_constraints`.


Usually, not the whole flexibility model is needed.
FlexMeasures can infer missing values in the flex model, and even get them (as default) from the sensor's attributes.

Expand Down
1 change: 1 addition & 0 deletions documentation/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ In :ref:`getting_started`, we have some helpful tips how to dive into this docum
tut/toy-example-from-scratch
tut/toy-example-expanded
tut/toy-example-multiasset-curtailment
tut/toy-example-group-constraints
tut/flex-model-v2g
tut/multi-feed-storage
tut/multi-commodity
Expand Down
158 changes: 158 additions & 0 deletions documentation/tut/toy-example-group-constraints.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
.. _tut_toy_schedule_group_constraints:


Toy example IV: Intermediate power constraints (groups)
================================================================

So far, our flexible devices (the battery and the PV inverter) have only ever been constrained directly by the building's own grid connection capacity.
But in reality, several devices are often physically wired together behind a shared piece of equipment before they reach the site's connection, and that piece of equipment has its own power limit.

The classic example is a **hybrid inverter**: a battery and a PV installation share one inverter, and while each device could individually push a lot of power, the inverter itself caps their *combined* power flow.
This is what the ``group`` field in the storage flex-model is for (see :ref:`storage_device_scheduler` for the general explanation). This tutorial shows a fully DB-driven setup, where the entire flex-model lives on the asset tree, and you trigger a schedule for the site with an empty flex-model.

We'll build the following little asset tree:

.. code-block:: text

site (building)
└── inverter (hybrid inverter, hard power-capacity 2.5 kW)
├── battery (device, group member)
└── PV (device, group member)

Setting up the asset tree
---------------------------------------

We create the site, the inverter (the group) and the two devices as assets, with the inverter and devices being children of the site.
Each device also needs output sensors to record its schedule (since these devices won't have a dedicated power sensor of their own — they are "asset-only" flex-model entries), and the inverter needs an output sensor for the group's aggregate schedule.

.. code-block:: bash

$ flexmeasures add asset --name "toy site" --asset-type-id 5 --account-id 1
Successfully created asset with ID 10.

$ flexmeasures add asset --name "hybrid inverter" --asset-type-id 5 --account-id 1 --parent-asset 10
Successfully created asset with ID 11.

$ flexmeasures add asset --name "toy battery" --asset-type-id 5 --account-id 1 --parent-asset 10
Successfully created asset with ID 12.

$ flexmeasures add asset --name "toy PV" --asset-type-id 5 --account-id 1 --parent-asset 10
Successfully created asset with ID 13.

$ flexmeasures add sensor --name "inverter aggregate power" --unit MW --event-resolution PT15M --asset-id 11
Successfully created sensor with ID 21.

$ flexmeasures add sensor --name "battery consumption" --unit MW --event-resolution PT15M --asset-id 12
Successfully created sensor with ID 22.
$ flexmeasures add sensor --name "battery production" --unit MW --event-resolution PT15M --asset-id 12
Successfully created sensor with ID 23.

$ flexmeasures add sensor --name "PV production" --unit MW --event-resolution PT15M --asset-id 13
Successfully created sensor with ID 24.

.. note:: Asset type IDs and IDs returned above will differ in your own setup — substitute your own.

Storing the flex-models on the assets
---------------------------------------

Rather than sending a flex-model with the trigger request, we store each asset's (partial) flex-model directly on the asset. FlexMeasures walks the asset tree and collects these into one combined flex-model when scheduling the site.

You can set an asset's flex-model with ``PATCH /api/v3_0/assets/<id>``, sending a ``flex_model`` field with the JSON below. (The FlexMeasures UI's flex-model editor on the asset's properties page supports this too, and even suggests the parent asset as a candidate for the ``group`` field.)

The battery is a device with both a consumption and production output sensor (it can charge and discharge), belonging to the inverter's group:

.. code-block:: json

{
"flex_model": {
"power-capacity": "2 kW",
"consumption-capacity": "2 kW",
"production-capacity": "2 kW",
"group": {"asset": 11},
"consumption": {"sensor": 22},
"production": {"sensor": 23}
}
}

Sent as ``PATCH /api/v3_0/assets/12``.

The PV installation only produces, so it only needs a production output sensor:

.. code-block:: json

{
"flex_model": {
"power-capacity": "2 kW",
"consumption-capacity": "0 kW",
"production-capacity": "2 kW",
"group": {"asset": 11},
"production": {"sensor": 24}
}
}

Sent as ``PATCH /api/v3_0/assets/13``.

Finally, the inverter's own flex-model defines the group's hard power-capacity and where to save the group's aggregate schedule (as it has no power sensor of its own, either):

.. code-block:: json

{
"flex_model": {
"power-capacity": "2.5 kW",
"consumption": {"sensor": 21}
}
}

Sent as ``PATCH /api/v3_0/assets/11``.

Note that neither the battery, the PV installation, nor the inverter reference a ``sensor`` field of their own for scheduling purposes — this is what makes them "asset-only" entries. Instead, results are always saved via their ``consumption``/``production`` output sensor references.

Triggering the schedule
---------------------------------------

We now trigger a schedule for the site (asset 10) with an empty (or omitted) flex-model. Everything the scheduler needs is picked up from the DB-stored flex-models on the asset tree.

.. tabs::

.. tab:: CLI

.. code-block:: bash

$ flexmeasures add schedule \
--asset 10 \
--start ${TOMORROW}T00:00+01:00 --duration PT4H \
--flex-model '[]'
New schedule is stored.

.. tab:: API

Example call: `[POST] http://localhost:5000/api/v3_0/assets/10/schedules/trigger <../api/v3_0.html#post--api-v3_0-assets-id-schedules-trigger>`_ (update the start date to tomorrow):

.. code-block:: json

{
"start": "2026-07-11T00:00+01:00",
"duration": "PT4H",
"flex-model": []
}

Inspecting the results
---------------------------------------

Once the job has finished, three schedules were computed and saved:

- The battery's schedule, split (as it can both charge and discharge) between sensor 22 (``battery consumption``, holding the non-negative, consumption-positive part) and sensor 23 (``battery production``, holding the non-positive part, stored as a positive magnitude).
- The PV installation's schedule, saved entirely to sensor 24 (``PV production``), sign-flipped to be stored as a positive magnitude (since PV only produces).
- The inverter group's aggregate schedule, saved to sensor 21 (``inverter aggregate power``), equal to the (consumption-positive) sum of the battery's and PV's schedules.

You can inspect any of these with:

.. code-block:: bash

$ flexmeasures show beliefs --sensor 21 --start ${TOMORROW}T00:00:00+01:00 --duration PT4H

The group's aggregate power never exceeds 2.5 kW in either direction — even though the battery and PV could individually reach 2 kW each (4 kW combined) — because the hybrid inverter's hard ``power-capacity`` caps their sum. Without the group, the scheduler could plan the battery to charge at full power during peak PV production, which the inverter physically cannot deliver.

.. note:: If a device only ever consumes or only ever produces, you only need to define the corresponding single output sensor (as we did for the PV installation above). Only devices (or groups) that can go both ways need both a ``consumption`` and a ``production`` output sensor.

This concludes our tour of intermediate power constraints. For the full field reference, see :ref:`storage_device_scheduler` and the "Intermediate power constraints" section of :ref:`scheduling`.
Loading
Loading