Skip to content
2 changes: 2 additions & 0 deletions documentation/api/change_log.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,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.
- Fixed: `/assets/(id)/schedules/trigger <../api/v3_0.html#post--api-v3_0-assets-id-schedules-trigger>`_ (POST) now also accepts the legacy ``force_new_job_creation`` field name (in addition to ``force-new-job-creation``), matching the sensor-level trigger endpoint. Previously, only the sensor-level endpoint accepted both spellings.
- Several query parameters now additionally accept a canonical spelling (hyphenated where applicable), while the legacy underscored spelling keeps working: ``per_page``/``per-page``, ``sort_by``/``sort-by``, ``sort_dir``/``sort-dir`` (all paginated list endpoints), ``account_id``/``account`` and ``asset_id``/``asset`` (``GET /sensors``, ``GET /assets``, ``GET /users``). New clients should prefer the canonical form.

v3.0-31 | 2026-06-01
""""""""""""""""""""
Expand Down
10 changes: 9 additions & 1 deletion flexmeasures/api/common/schemas/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,13 @@ def _serialize(self, values: list[str], attr, obj, **kwargs) -> str:


class AssetAPIQuerySchema(PaginationSchema):
legacy_field_aliases = {
**PaginationSchema.legacy_field_aliases,
"account_id": "account",
}

sort_by = fields.Str(
data_key="sort-by",
required=False,
validate=validate.OneOf(["id", "name", "owner"]),
metadata=dict(
Expand All @@ -55,7 +61,7 @@ class AssetAPIQuerySchema(PaginationSchema):
),
)
account = AccountIdField(
data_key="account_id",
data_key="account",
required=False,
metadata=dict(
description="Select assets from a given account (requires read access to that account). Per default, the user's own account is used.",
Expand Down Expand Up @@ -118,10 +124,12 @@ class PublicAssetAPISchema(Schema):

class AssetPaginationSchema(PaginationSchema):
sort_by = fields.Str(
data_key="sort-by",
required=False,
validate=validate.OneOf(["id", "name", "resolution"]),
)
sort_dir = fields.Str(
data_key="sort-dir",
required=False,
validate=validate.OneOf(["asc", "desc"]),
)
16 changes: 14 additions & 2 deletions flexmeasures/api/common/schemas/generic_schemas.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
from marshmallow import Schema, fields, validate
from flexmeasures.api.common.schemas.search import SearchFilterField
from flexmeasures.api.common.schemas.utils import SupportsLegacyFieldAliases


class PaginationSchema(Schema):
class PaginationSchema(SupportsLegacyFieldAliases, Schema):
legacy_field_aliases = {
"per_page": "per-page",
"sort_by": "sort-by",
"sort_dir": "sort-dir",
}

# note: the absence of this parameter would signal to the API to not paginate (so there is no default set here)
page = fields.Int(required=False, validate=validate.Range(min=1))
per_page = fields.Int(
required=False, validate=validate.Range(min=1), load_default=10
data_key="per-page",
required=False,
validate=validate.Range(min=1),
load_default=10,
)
filter = SearchFilterField(
required=False,
Expand All @@ -15,12 +25,14 @@ class PaginationSchema(Schema):
),
)
sort_by = fields.Str(
data_key="sort-by",
required=False,
metadata=dict(
description="Sort results by this field.",
),
)
sort_dir = fields.Str(
data_key="sort-dir",
required=False,
validate=validate.OneOf(["asc", "desc"]),
metadata=dict(
Expand Down
1 change: 1 addition & 0 deletions flexmeasures/api/common/schemas/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ def _serialize(self, value: User, attr, obj, **kwargs) -> int:

class AccountAPIQuerySchema(PaginationSchema):
sort_by = fields.Str(
data_key="sort-by",
required=False,
validate=validate.OneOf(["id", "name", "assets", "users"]),
)
1 change: 1 addition & 0 deletions flexmeasures/api/common/schemas/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
VariableQuantityField,
VariableQuantityOpenAPISchema,
)
from flexmeasures.data.schemas.utils import SupportsLegacyFieldAliases # noqa: F401


def make_openapi_compatible( # noqa: C901
Expand Down
4 changes: 2 additions & 2 deletions flexmeasures/api/v3_0/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ def index(
This endpoint returns all accounts the current user has access to.
Accessible accounts include the user's own account, accounts for which the user is a consultant, and all accounts if the user has admin privileges.

The endpoint supports pagination of the account list using the `page` and `per_page` query parameters.
The endpoint supports pagination of the account list using the `page` and `per-page` (legacy alias: `per_page`) query parameters.
- If the `page` parameter is not provided, all accounts are returned, without pagination information. The result will be a list of accounts.
- If a `page` parameter is provided, the response will be paginated, showing a specific number of accounts per page as defined by `per_page` (default is 10).
- If a `page` parameter is provided, the response will be paginated, showing a specific number of accounts per page as defined by `per-page` (default is 10).
- If a search 'filter' such as 'solar "ACME corp"' is provided, the response will filter out accounts where each search term is either present in their name.
The response schema for pagination is inspired by [DataTables](https://datatables.net/manual/server-side#Returned-data)

Expand Down
36 changes: 27 additions & 9 deletions flexmeasures/api/v3_0/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
SensorsToShowSchema,
)
from flexmeasures.data.schemas.scheduling import AssetTriggerSchema
from flexmeasures.data.schemas.utils import SupportsLegacyFieldAliases
from flexmeasures.data.services.scheduling import (
create_sequential_scheduling_job,
create_simultaneous_scheduling_job,
Expand Down Expand Up @@ -99,6 +100,22 @@ def sensor_term_filter(term: str):
return or_(*filters)


class AssetTriggerSchemaV3(SupportsLegacyFieldAliases, AssetTriggerSchema):
"""v3_0-only wrapper around the shared `AssetTriggerSchema`.

`AssetTriggerSchema` itself stays canonical (no legacy field-name
aliasing) since it's also used outside the versioned API, e.g. by the
CLI. This subclass adds v3_0's backward compatibility for the legacy
`force_new_job_creation` field name, so it can be deleted in one place,
along with the rest of `flexmeasures/api/v3_0/`, once this API version is
sunset -- without needing to touch the shared domain schema.
"""

legacy_field_aliases = {
"force_new_job_creation": "force-new-job-creation",
}


class AssetTriggerOpenAPISchema(AssetTriggerSchema):

def __init__(self, *args, **kwargs):
Expand Down Expand Up @@ -153,6 +170,7 @@ class AssetChartDataKwargsSchema(Schema):

class AssetAuditLogPaginationSchema(PaginationSchema):
sort_by = fields.Str(
data_key="sort-by",
required=False,
validate=validate.OneOf(["event_datetime"]),
)
Expand Down Expand Up @@ -298,16 +316,16 @@ def index(
description: |
This endpoint returns all assets that are accessible by the user after applying optional filters.

- The `account_id` query parameter can be used to list assets from any account (if the user is allowed to read them). Per default, the user's account is used.
- The `account` query parameter (legacy alias: `account_id`) can be used to list assets from any account (if the user is allowed to read them). Per default, the user's account is used.
- Alternatively, the `all_accessible` query parameter can be used to list assets from all accounts the current_user has read-access to, plus all public assets. Defaults to `false`.
- The `include_public` query parameter can be used to include public assets in the response. Defaults to `false`.
- The `asset_type` query parameter can be used to filter by generic asset type ID.
- The `root` query parameter can be used to list only descendants of a given root asset (including the root itself).
- The `depth` query parameter can be used to search only a max number of descendant generations from the root.

The endpoint supports pagination of the asset list using the `page` and `per_page` query parameters.
The endpoint supports pagination of the asset list using the `page` and `per-page` (legacy alias: `per_page`) query parameters.
- If the `page` parameter is not provided, all assets are returned, without pagination information. The result will be a list of assets.
- If a `page` parameter is provided, the response will be paginated, showing a specific number of assets per page as defined by `per_page` (default is 10).
- If a `page` parameter is provided, the response will be paginated, showing a specific number of assets per page as defined by `per-page` (default is 10).
- If a search 'filter' such as 'solar "ACME corp"' is provided, the response will return only assets where each search term is either present in their name or account name, or is a prefix of their ID.
The response schema for pagination is inspired by [DataTables](https://datatables.net/manual/server-side#Returned-data)

Expand Down Expand Up @@ -455,10 +473,10 @@ def asset_sensors(
description: |
This endpoint returns all sensors under an asset.

The endpoint supports pagination of the sensor list using the `page` and `per_page` query parameters.
The endpoint supports pagination of the sensor list using the `page` and `per-page` (legacy alias: `per_page`) query parameters.

- If the `page` parameter is not provided, all sensors are returned, without pagination information. The result will be a list of sensors.
- If a `page` parameter is provided, the response will be paginated, showing a specific number of sensors per page as defined by `per_page` (default is 10).
- If a `page` parameter is provided, the response will be paginated, showing a specific number of sensors per page as defined by `per-page` (default is 10).
- If a search 'filter' is provided, the response will return only sensors where a search term is either present in their name or is a prefix of their ID.
The response schema for pagination is inspired by https://datatables.net/manual/server-side#Returned-data
security:
Expand Down Expand Up @@ -1125,9 +1143,9 @@ def auditlog(
summary: Get history of asset related actions.
description: |
The endpoint is paginated and supports search filters.
- If the `page` parameter is not provided, all audit logs are returned paginated by `per_page` (default is 10).
- If a `page` parameter is provided, the response will be paginated, showing a specific number of assets per page as defined by `per_page` (default is 10).
- If `sort_by` (field name) and `sort_dir` ("asc" or "desc") are provided, the list will be sorted.
- If the `page` parameter is not provided, all audit logs are returned paginated by `per-page` (legacy alias: `per_page`, default is 10).
- If a `page` parameter is provided, the response will be paginated, showing a specific number of assets per page as defined by `per-page` (default is 10).
- If `sort-by` (field name, legacy alias: `sort_by`) and `sort-dir` ("asc" or "desc", legacy alias: `sort_dir`) are provided, the list will be sorted.
- If a search 'filter' is provided, the response will filter out audit logs where each search term is either present in the event or active user name.
The response schema for pagination is inspired by [DataTables](https://datatables.net/manual/server-side)
security:
Expand Down Expand Up @@ -1427,7 +1445,7 @@ def update_keep_legends_below_graphs(self, **kwargs):
}, 200

@route("/<id>/schedules/trigger", methods=["POST"])
@use_args(AssetTriggerSchema(), location="args_and_json", as_kwargs=True)
@use_args(AssetTriggerSchemaV3(), location="args_and_json", as_kwargs=True)
# Simplification of checking for create-children access on each of the flexible sensors,
# which assumes each of the flexible sensors belongs to the given asset.
@permission_required_for_context("create-children", ctx_arg_name="asset")
Expand Down
45 changes: 27 additions & 18 deletions flexmeasures/api/v3_0/sensors.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from flask_classful import FlaskView, route
from flask_json import as_json
from flask_security import auth_required, current_user
from marshmallow import fields, pre_load, Schema, ValidationError, validates_schema
from marshmallow import fields, Schema, ValidationError, validates_schema
import marshmallow.validate as validate
from rq.job import Job, JobStatus, NoSuchJobError
from webargs.flaskparser import use_args, use_kwargs
Expand All @@ -26,7 +26,10 @@
unprocessable_entity,
fallback_schedule_redirect,
)
from flexmeasures.api.common.schemas.utils import make_openapi_compatible
from flexmeasures.api.common.schemas.utils import (
make_openapi_compatible,
SupportsLegacyFieldAliases,
)
from flexmeasures.api.common.schemas.sensor_data import ( # noqa F401
SensorDataDescriptionSchema,
GetSensorDataSchema,
Expand Down Expand Up @@ -220,14 +223,23 @@ def validate_time_window(self, data, **kwargs):
)


class SensorKwargsSchema(Schema):
account = AccountIdField(data_key="account_id", required=False)
asset = AssetIdField(data_key="asset_id", required=False)
class SensorKwargsSchema(SupportsLegacyFieldAliases, Schema):
legacy_field_aliases = {
"account_id": "account",
"asset_id": "asset",
"per_page": "per-page",
}

account = AccountIdField(data_key="account", required=False)
asset = AssetIdField(data_key="asset", required=False)
include_consultancy_clients = fields.Boolean(required=False, load_default=False)
include_public_assets = fields.Boolean(required=False, load_default=False)
page = fields.Int(required=False, validate=validate.Range(min=1))
per_page = fields.Int(
required=False, validate=validate.Range(min=1), load_default=10
data_key="per-page",
required=False,
validate=validate.Range(min=1),
load_default=10,
)
filter = SearchFilterField(
required=False,
Expand All @@ -238,7 +250,11 @@ class SensorKwargsSchema(Schema):
unit = UnitField(required=False)


class TriggerScheduleKwargsSchema(Schema):
class TriggerScheduleKwargsSchema(SupportsLegacyFieldAliases, Schema):
legacy_field_aliases = {
"force_new_job_creation": "force-new-job-creation",
}

start_of_schedule = AwareDateTimeField(
data_key="start",
format="iso",
Expand Down Expand Up @@ -295,14 +311,6 @@ class TriggerScheduleKwargsSchema(Schema):
),
)

@pre_load
def support_legacy_field_name(self, data, **kwargs):
"""Accept old snake_case input for backwards compatibility."""
if "force_new_job_creation" in data and "force-new-job-creation" not in data:
data["force-new-job-creation"] = data.pop("force_new_job_creation")

return data


class SensorAPI(FlaskView):
route_base = "/sensors"
Expand Down Expand Up @@ -344,13 +352,14 @@ def index(
Finally, you can use the `include_consultancy_clients` parameter to include sensors from accounts for which the current user account is a consultant.
This is only possible if the user has the role of a consultant.

Only admins can use this endpoint to fetch sensors from a different account (by using the `account_id` query parameter).
Only admins can use this endpoint to fetch sensors from a different account (by using the `account` query parameter, legacy alias: `account_id`).

The `filter` parameter allows you to search for sensors by name, account name, asset name, or sensor ID prefix.
The `unit` parameter allows you to filter by unit.

For the pagination of the sensor list, you can use the `page` and `per_page` query parameters, the `page` parameter is used to trigger
pagination, and the `per_page` parameter is used to specify the number of records per page. The default value for `page` is 1 and for `per_page` is 10.
For the pagination of the sensor list, you can use the `page` and `per-page` query parameters (legacy alias: `per_page`). If `page` is omitted,
all accessible sensors are returned, without pagination. If `page` is provided, the response is paginated, using `per-page` (default 10) to
specify the number of records per page.

security:
- ApiKeyAuth: []
Expand Down
Loading
Loading