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/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,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``); 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>`_]
* 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>`_]
* 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
2 changes: 2 additions & 0 deletions documentation/features/forecasting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -204,5 +204,7 @@ Each due automation then queues its forecasting jobs.
The jobs record how they were created, which is shown on the asset's status page (UI), where recent jobs are listed.

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>`_.
Account admins and consultants can also create, (de)activate and delete automations right there on the page,
or through the API (`[POST] /assets/(id)/automations`, `[PATCH] /assets/(id)/automations/(automation_id)` and `[DELETE] /assets/(id)/automations/(automation_id)`).

Schedules can be automated in the same way — see :ref:`automating_schedules`.
6 changes: 6 additions & 0 deletions flexmeasures/api/v3_0/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
DefaultAssetViewJSONSchema,
)
from flexmeasures.data.schemas.annotations import AnnotationSchema
from flexmeasures.data.schemas.automations import (
AutomationCreationSchema,
AutomationUpdateSchema,
)
from flexmeasures.data.schemas.generic_assets import GenericAssetSchema as AssetSchema
from flexmeasures.data.schemas.sensors import QuantitySchema, TimeSeriesSchema
from flexmeasures.data.schemas.account import AccountSchema
Expand Down Expand Up @@ -157,6 +161,8 @@ def create_openapi_specs(app: Flask):
("AssetAPIQuerySchema", AssetAPIQuerySchema),
("AssetSchema", AssetSchema),
("AnnotationSchema", AnnotationSchema),
("AutomationCreationSchema", AutomationCreationSchema),
("AutomationUpdateSchema", AutomationUpdateSchema),
("CopyAssetSchema", CopyAssetSchema),
("DefaultAssetViewJSONSchema", DefaultAssetViewJSONSchema),
("AccountSchema", AccountSchema(partial=True)),
Expand Down
237 changes: 236 additions & 1 deletion flexmeasures/api/v3_0/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,17 @@
from flexmeasures.data.models.automations import Automation
from flexmeasures.data.models.user import Account
from flexmeasures.data.models.audit_log import AssetAuditLog
from flexmeasures.data.schemas.automations import AutomationSchema
from flexmeasures.data.schemas.automations import (
AutomationCreationSchema,
AutomationSchema,
AutomationUpdateSchema,
)
from flexmeasures.data.services.automations import (
create_automation,
delete_automation as remove_automation,
describe_cronstr,
get_automation_job_stats,
update_automation,
)
from flexmeasures.data.models.generic_assets import GenericAsset, GenericAssetType
from flexmeasures.data.queries.generic_assets import (
Expand Down Expand Up @@ -1299,6 +1306,234 @@ def get_automation(self, id: int, automation_id: int, asset: GenericAsset):
automation_data["redis_connection_err"] = redis_connection_err
return automation_data, 200

@route("/<id>/automations", methods=["POST"])
@use_kwargs(
{"asset": AssetIdField(data_key="id")},
location="path",
)
# Managing automations requires the same principals that may delete the asset
# (i.e. account admins and consultants), matching the Automation ACL.
@permission_required_for_context("delete", ctx_arg_name="asset")
@as_json
def post_automation(self, id: int, asset: GenericAsset):
"""
.. :quickref: Assets; Create an automation on an asset.

---
post:
summary: Create an automation on an asset.
description: |
Create a recurring task (computing forecasts or schedules) on the asset.
The parameters are validated by the schema matching the automation type:
forecast parameters for type `forecasts`, or a schedule trigger message
(without the asset id) for type `schedules`.
Requires account admin or consultant rights.
security:
- ApiKeyAuth: []
parameters:
- in: path
name: id
required: true
description: ID of the asset to create the automation on.
schema:
type: integer
requestBody:
content:
application/json:
schema: AutomationCreationSchema
examples:
daily_forecasts:
summary: Daily forecasts of sensor 2092
value:
name: Day-ahead PV forecasts
cronstr: "0 6 * * *"
type: forecasts
parameters:
sensor: 2092
responses:
201:
description: CREATED
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:
automation_data = AutomationCreationSchema().load(body)
except ValidationError as e:
return unprocessable_entity(e.messages)
try:
automation, warnings = create_automation(
asset=asset,
name=automation_data["name"],
cronstr=automation_data["cronstr"],
automation_type=automation_data["type"],
active=automation_data["active"],
parameters=automation_data["parameters"],
forecaster_class=automation_data["forecaster"],
config=automation_data["config"],
origin="API",
)
except ValidationError as e:
return unprocessable_entity({"parameters": e.messages})
except ValueError as e:
return unprocessable_entity(str(e))
db.session.commit()
response = automation_schema.dump(automation)
response["recurrence_description"] = describe_cronstr(automation.cronstr)
response["warnings"] = warnings
return response, 201

@route("/<id>/automations/<int:automation_id>", methods=["PATCH"])
@use_kwargs(
{
"asset": AssetIdField(data_key="id"),
"automation_id": fields.Int(),
},
location="path",
)
# Managing automations requires the same principals that may delete the asset
# (i.e. account admins and consultants), matching the Automation ACL.
@permission_required_for_context("delete", ctx_arg_name="asset")
@as_json
def patch_automation(self, id: int, automation_id: int, asset: GenericAsset):
"""
.. :quickref: Assets; Update an automation's name, cron string or activation status.

---
patch:
summary: Update an automation's name, cron string or activation status.
description: |
Any subset of the fields `name`, `cronstr` and `active` can be sent.
Other automation fields cannot be updated; instead, create a new automation.
Requires account admin or consultant rights.
security:
- ApiKeyAuth: []
parameters:
- in: path
name: id
required: true
description: ID of the asset.
schema:
type: integer
- in: path
name: automation_id
required: true
description: ID of the automation.
schema:
type: integer
requestBody:
content:
application/json:
schema: AutomationUpdateSchema
examples:
deactivate:
summary: Deactivate the automation
value:
active: false
responses:
200:
description: PROCESSED
400:
description: INVALID_REQUEST, REQUIRED_INFO_MISSING, UNEXPECTED_PARAMS
401:
description: UNAUTHORIZED
403:
description: INVALID_SENDER
404:
description: NOT_FOUND
422:
description: UNPROCESSABLE_ENTITY
tags:
- Assets
"""
automation = db.session.get(Automation, automation_id)
if automation is None or automation.asset_id != asset.id:
return {
"message": f"Asset {asset.id} has no automation with id {automation_id}."
}, 404
body = request.get_json(silent=True)
if not body:
return unprocessable_entity("No JSON data provided.")
try:
automation_data = AutomationUpdateSchema().load(body)
except ValidationError as e:
return unprocessable_entity(e.messages)
update_automation(automation, origin="API", **automation_data)
db.session.commit()
response = automation_schema.dump(automation)
response["recurrence_description"] = describe_cronstr(automation.cronstr)
return response, 200

@route("/<id>/automations/<int:automation_id>", methods=["DELETE"])
@use_kwargs(
{
"asset": AssetIdField(data_key="id"),
"automation_id": fields.Int(),
},
location="path",
)
# Managing automations requires the same principals that may delete the asset
# (i.e. account admins and consultants), matching the Automation ACL.
@permission_required_for_context("delete", ctx_arg_name="asset")
@as_json
def delete_automation(self, id: int, automation_id: int, asset: GenericAsset):
"""
.. :quickref: Assets; Delete an automation.

---
delete:
summary: Delete an automation.
description: |
Delete the automation. Any jobs it already queued are unaffected.
Requires account admin or consultant rights.
security:
- ApiKeyAuth: []
parameters:
- in: path
name: id
required: true
description: ID of the asset.
schema:
type: integer
- in: path
name: automation_id
required: true
description: ID of the automation.
schema:
type: integer
responses:
204:
description: DELETED
400:
description: INVALID_REQUEST, REQUIRED_INFO_MISSING, UNEXPECTED_PARAMS
401:
description: UNAUTHORIZED
403:
description: INVALID_SENDER
404:
description: NOT_FOUND
tags:
- Assets
"""
automation = db.session.get(Automation, automation_id)
if automation is None or automation.asset_id != asset.id:
return {
"message": f"Asset {asset.id} has no automation with id {automation_id}."
}, 404
remove_automation(automation, origin="API")
db.session.commit()
return {}, 204

@route("/<id>/jobs", methods=["GET"])
@use_kwargs(
{"asset": AssetIdField(data_key="id")},
Expand Down
Loading
Loading