Skip to content
Open
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
3 changes: 2 additions & 1 deletion documentation/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ v1.0.0 | July XX, 2026

New features
-------------

* 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>`_]
* Support for creating new assets by using another asset as a template from the UI. [see `PR #2195 <https://www.github.com/FlexMeasures/flexmeasures/pull/2195>`_ and `PR #2268 <https://www.github.com/FlexMeasures/flexmeasures/pull/2268>`_
* 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>`_]
* Floor off-clock API datetimes to a non-instantaneous sensor's resolution by default when ingesting sensor data, uploading sensor data, and handling scheduler flex-model timed events; configurable with the ``floor_datetimes_to_resolution`` sensor attribute [see `PR #2146 <https://www.github.com/FlexMeasures/flexmeasures/pull/2146>`_]
* Sensor references in flex-model and flex-context support various ways of filtering by source [see `PR #2209 <https://www.github.com/FlexMeasures/flexmeasures/pull/2209>`_]
Expand Down Expand Up @@ -81,7 +83,6 @@ New features
* New ``GET /api/v3_0/sources`` endpoint to list accessible data sources and defined types, with ``only_latest=true`` by default to return only the most recent version per source [see `PR #2126 <https://www.github.com/FlexMeasures/flexmeasures/pull/2126>`_]
* Add support for filtering sensor data GET requests by ``source-type`` on ``/api/v3_0/sensors/<id>/data`` [see `PR #2127 <https://www.github.com/FlexMeasures/flexmeasures/pull/2127>`_]
* Making monitoring alerts more flexible: allow ``flexmeasures monitor`` alerts to target one or more user IDs or email addresses with ``--recipient``; ``flexmeasures monitor last-seen`` can now narrow monitored users to one or more accounts with ``--account`` or to client accounts with ``--consultancy`` [see `PR #2158 <https://www.github.com/FlexMeasures/flexmeasures/pull/2158>`_]
* Support for creating new assets by using another asset as a template from the UI. [see `PR #2195 <https://www.github.com/FlexMeasures/flexmeasures/pull/2195>`_]
* Improve LightGBM daily seasonal lag handling for sub-hourly forecasting sensors [see `PR #2157 <https://www.github.com/FlexMeasures/flexmeasures/pull/2157>`_]

Infrastructure / Support
Expand Down
15 changes: 15 additions & 0 deletions documentation/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,21 @@ When ``FLEXMEASURES_MODE=demo``\ , this can hold login credentials (demo user em

Default: ``None``

FLEXMEASURES_CREATE_TEMPLATE_ASSETS_ON_STARTUP
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Whether FlexMeasures should create its built-in starter template assets when the
application starts.

If ``True``, FlexMeasures provisions a small set of public starter templates,
such as ``Battery Template``, ``EV Charger Template`` and ``Heat Pump Template``,
if they do not exist yet. Together with the asset copy workflow, new users will

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So they get recreated if the host deletes them without setting this config setting to false?

find FlexMeasures to be more useful out of the box.

If ``False``, no template assets are created automatically at startup.

Default: ``True``

.. _sunset-config:

Sunset
Expand Down
29 changes: 25 additions & 4 deletions documentation/getting-started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Below, we added helpful pointers to start reading.
:local:
:depth: 2

|

.. _start_using_flexmeasures_in_your_organization:

Expand All @@ -20,11 +21,31 @@ We make FlexMeasures, so that your software developers are as productive with en

Your journey, from dipping your toes in the water towards being a productive energy optimization company, could look like this:

1. Quickstart ― Find an optimized schedule for your flexible asset, like a battery, with standard FlexMeasures tooling. This is basically what we show in :ref:`tut_toy_schedule`. All you need are 10 minutes and a CSV file with prices to optimize against.
2. Automate ― get the prices from an open API, for instance `ENTSO-E <https://transparency.entsoe.eu/>`_ (using a plugin like `flexmeasures-entsoe <https://github.com/SeitaBV/flexmeasures-entsoe>`_), and run the scheduler regularly in a cron job.
3. Integrate ― Load the schedules via FlexMeasures' API, so you can directly control your assets and/or show them within your own frontend.
4. Customize ― Load other data (e.g. add weather forecasts via `flexmeasures-weather <https://github.com/flexmeasures/flexmeasure-weather/>`_, which can help you better forecast local production or consumption). Adapt the data dashboards to your liking. Play with flex-context and flex-model settings. In your own plugin, add your custom pages or endpoints or even adapt the algorithms. For example, do your own forecasting or tweak the standard scheduling algorithm so it optimizes what you care about. Or write a plugin for accessing a new kind of market. The opportunities are endless!
1. Quickstart
^^^^^^^^^^^^^^^^^^^

Find an optimized schedule for your flexible asset, like a battery, with standard FlexMeasures tooling. This is basically what we show in :ref:`tut_toy_schedule`. All you need are 10 minutes and a CSV file with prices to optimize against.

2. Automate
^^^^^^^^^^^^^^^^^^^

Get the prices from an open API, for instance `ENTSO-E <https://transparency.entsoe.eu/>`_ (using a plugin like `flexmeasures-entsoe <https://github.com/SeitaBV/flexmeasures-entsoe>`_), and run the scheduler regularly in a cron job. Do more, like automating forecasts, too. And for scale, copy your successfully-tested asset programmatically (or in the UI), to set up new customers quickly, e.g. as you are servicing the same kind of site often.

3. Integrate
^^^^^^^^^^^^^^^^^^^

Download the schedules via FlexMeasures' API, so you can directly control your assets within your IoT/gateway.
You can also download forecasts, schedules and reports to show them within your own frontend.

4. Customize
^^^^^^^^^^^^^^^^^^^

The opportunities are endless!

- Load other data (e.g. add weather forecasts via `flexmeasures-weather <https://github.com/flexmeasures/flexmeasure-weather/>`_, which can help you better forecast local production or consumption).
- Adapt the data dashboards to your liking. Play with flex-context and flex-model settings.
- In your own plugin, write your own KPI reporter, add your custom pages or endpoints or even adapt the algorithms. For example, do your own forecasting or tweak the standard scheduling algorithm so it optimizes what you care about.
- Or write a plugin for accessing a new kind of market.



Expand Down
11 changes: 11 additions & 0 deletions documentation/plugin/customisation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,17 @@ In the same way, you can customize the siblings that are shown as drop-down for
For this, the attribute is named "breadcrumb_siblings" and follows the same syntax. One use case might be to set it to empty (``[]``).


Creating your own template assets (for scaling setups)
-----------------------------------------------------------

In FlexMeasures, you can create new assets by copying existing ones. This can be great for repeating a successful setup,
e.g. as you are servicing the same kind of site often.

- You can manually set up a well-working asset, with flex-config, sensors, child assets etc and then copy it in the UI.
- You can also copy it via the API endpoint for asset copying, and make that part of your workflow.
- Finally, you could script the template. Your plugin can make sure on startup that custom template assets exist, programmatically. For FlexMeasures' own initial templates, which are scripted on startups, you might find the code in ``flexmeasures/data/scripts/data_gen.py::_ensure_public_root_asset()`` useful.


Validating arguments in your CLI commands with marshmallow
-----------------------------------------------------------

Expand Down
29 changes: 29 additions & 0 deletions flexmeasures/api/common/utils/api_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,34 @@ def convert_asset_json_fields(asset_kwargs):
return asset_kwargs


def _remove_template_copy_guidance(description: str | None) -> str | None:
"""Strip template-specific copy instructions from an asset description."""
if not description:
return description

cleaned_description = re.sub(
r"\s*Copy this(?: asset)?\b.*?(?:\.\s*|$)",
"",
description,
flags=re.IGNORECASE,
).strip()
return cleaned_description or None


def _sanitize_copied_asset_kwargs(asset_kwargs: dict) -> dict:
"""Turn a copied template into a regular asset payload."""
attributes = asset_kwargs.get("attributes")
if isinstance(attributes, dict) and "template" in attributes:
sanitized_attributes: dict = deepcopy(attributes)
sanitized_attributes.pop("template", None)
asset_kwargs["attributes"] = sanitized_attributes
asset_kwargs["description"] = _remove_template_copy_guidance(
asset_kwargs.get("description")
)

return asset_kwargs


def _copy_direct_sensors(
source_asset: GenericAsset, copied_asset: GenericAsset
) -> dict[int, int]:
Expand Down Expand Up @@ -465,6 +493,7 @@ def _copy_asset_subtree(
# set external_id to None to avoid conflicts with unique constraint on (account_id, external_id)
asset_kwargs["external_id"] = None
asset_kwargs = convert_asset_json_fields(asset_kwargs)
asset_kwargs = _sanitize_copied_asset_kwargs(asset_kwargs)

copied_asset = GenericAsset(**asset_kwargs)
db.session.add(copied_asset)
Expand Down
44 changes: 44 additions & 0 deletions flexmeasures/api/v3_0/tests/test_assets_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -907,6 +907,50 @@ def test_copy_asset_increments_name_under_same_parent(
assert third_copy.name == f"{battery.name} (Copy 3)"


def test_copy_template_asset_drops_template_metadata(
setup_api_test_data, setup_accounts, db
):
"""Copying a template should yield a regular asset in the target account."""
prosumer_account = setup_accounts["Prosumer"]
battery = db.session.scalars(
select(GenericAsset).filter_by(
account_id=prosumer_account.id,
name="Test grid connected battery storage",
)
).first()
assert battery is not None

template_asset = GenericAsset(
name="Battery Template",
generic_asset_type_id=battery.generic_asset_type_id,
account_id=None,
description=(
"Single battery asset with example power and state-of-charge sensors, "
"plus a basic storage flex-model. Copy this to start modeling a battery."
),
attributes={"template": {"key": "battery-template", "has_scenarios": False}},
)
db.session.add(template_asset)
db.session.flush()

assert template_asset.attributes["template"]["key"] == "battery-template"
assert "Copy this" in template_asset.description

copied_asset = copy_asset(template_asset, account=prosumer_account)

assert copied_asset.account_id == prosumer_account.id
assert copied_asset.parent_asset_id is None
assert copied_asset.attributes == {}
assert copied_asset.description == (
"Single battery asset with example power and state-of-charge sensors, "
"plus a basic storage flex-model."
)

# The source template stays available as a template for future copies.
assert template_asset.attributes["template"]["key"] == "battery-template"
assert "Copy this" in template_asset.description


def test_copy_asset_to_another_account_preserves_config(
setup_api_test_data, setup_accounts, setup_markets, setup_generic_asset_types, db
):
Expand Down
5 changes: 4 additions & 1 deletion flexmeasures/api/v3_0/tests/test_assets_api_fresh_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,14 @@ def test_post_an_asset_as_admin(client, setup_api_fresh_test_data, requesting_us
print("Server responded with:\n%s" % post_assets_response.json)
assert post_assets_response.status_code == 201
assert post_assets_response.json["latitude"] == 30.1
assert post_assets_response.json["description"] == post_data["description"]

asset: GenericAsset = db.session.execute(
select(GenericAsset).filter_by(name=post_data["name"])
).scalar_one_or_none()
assert asset is not None
assert asset.latitude == 30.1
assert asset.description == post_data["description"]


@pytest.mark.parametrize("requesting_user", ["test_admin_user@seita.nl"], indirect=True)
Expand Down Expand Up @@ -106,7 +108,7 @@ def test_edit_an_asset(client, setup_api_fresh_test_data, requesting_user, db):
with AccountContext("Test Supplier Account") as supplier:
existing_asset = supplier.generic_assets[0]

post_data = dict(latitude=10)
post_data = dict(latitude=10, description="Updated description")
edit_asset_response = client.patch(
url_for("AssetAPI:patch", id=existing_asset.id),
json=post_data,
Expand All @@ -118,6 +120,7 @@ def test_edit_an_asset(client, setup_api_fresh_test_data, requesting_user, db):
assert updated_asset.latitude == 10 # changed value
assert updated_asset.longitude == existing_asset.longitude
assert updated_asset.name == existing_asset.name
assert updated_asset.description == "Updated description"


@pytest.mark.parametrize("requesting_user", ["test_admin_user@seita.nl"], indirect=True)
Expand Down
1 change: 1 addition & 0 deletions flexmeasures/api/v3_0/tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ def make_sensor_data_request_for_gas_sensor(
def get_asset_post_data(account_id: int = 1, asset_type_id: int = 1) -> dict:
post_data = {
"name": "Test battery 2",
"description": "A test battery asset.",
"latitude": 30.1,
"longitude": 100.42,
"generic_asset_type_id": asset_type_id,
Expand Down
19 changes: 19 additions & 0 deletions flexmeasures/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def create( # noqa C901
)
from flexmeasures.utils.error_utils import add_basic_error_handlers
from flexmeasures.utils.secrets_utils import set_secret_key, set_totp_secrets
from sqlalchemy.exc import OperationalError, ProgrammingError

configure_logging() # do this first, see https://flask.palletsprojects.com/en/2.0.x/logging
cfg_location = find_flexmeasures_cfg() # Find flexmeasures.cfg location
Expand Down Expand Up @@ -209,6 +210,24 @@ def create( # noqa C901

register_ui_at(app)

if (
app.config.get("FLEXMEASURES_CREATE_TEMPLATE_ASSETS_ON_STARTUP", False)
and not app.testing
and app.config.get("FLEXMEASURES_ENV") != "documentation"
):
from flexmeasures.data import db
from flexmeasures.data.scripts.data_gen import (
provision_default_template_assets,
)

try:
with app.app_context():
provision_default_template_assets(db)
except (OperationalError, ProgrammingError) as exc:
app.logger.warning(
f"Skipping startup template provisioning due to an error: {exc}"
)

# Global template variables for both our own templates and external templates
@app.context_processor
def set_global_template_variables():
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""add description to generic asset

Revision ID: 4b0f2e9c1a6d
Revises: 55d8936a55f9
Create Date: 2026-07-02 17:00:00.000000

"""

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "4b0f2e9c1a6d"
down_revision = "55d8936a55f9"
branch_labels = None
depends_on = None


def upgrade():
with op.batch_alter_table("generic_asset", schema=None) as batch_op:
batch_op.add_column(sa.Column("description", sa.Text(), nullable=True))


def downgrade():
with op.batch_alter_table("generic_asset", schema=None) as batch_op:
batch_op.drop_column("description")
1 change: 1 addition & 0 deletions flexmeasures/data/models/generic_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ class GenericAsset(db.Model, AuthModelMixin):
# No relationship
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), default="")
description = db.Column(db.Text, nullable=True)
latitude = db.Column(db.Float, nullable=True)
longitude = db.Column(db.Float, nullable=True)
attributes = db.Column(MutableDict.as_mutable(JSONB), nullable=False, default={})
Expand Down
1 change: 1 addition & 0 deletions flexmeasures/data/schemas/generic_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ class GenericAssetSchema(ma.SQLAlchemySchema):

id = ma.auto_field(dump_only=True)
name = fields.Str(required=True)
description = fields.Str(required=False, allow_none=True)
account_id = ma.auto_field()
owner = ma.Nested("AccountSchema", dump_only=True, only=("id", "name"))
latitude = LatitudeField(allow_none=True)
Expand Down
Loading
Loading