Skip to content
Merged
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 @@ -15,6 +15,7 @@ v1.0.0 | July XX, 2026
New features
-------------

* ``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>`_]
* The API is now rate-limited, with a generous default limit on all endpoints and a stricter limit on triggering schedules and forecasts (which only counts triggers we accepted). Limits are configurable, and can be set per organisation by putting its account on a plan, which hosts create with ``flexmeasures add plan`` and platform admins assign from the organisation's page in the UI; play servers are exempt, as they run simulations. See :ref:`plans-and-rate-limiting` [see `PR #2306 <https://www.github.com/FlexMeasures/flexmeasures/pull/2306>`_]
Expand Down
1 change: 1 addition & 0 deletions documentation/cli/change_log.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +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.
* ``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``.

since v0.33.0 | June 01, 2026
=================================
Expand Down
52 changes: 50 additions & 2 deletions flexmeasures/cli/data_show.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,10 +421,26 @@ def _format_sensor_plot(plot: dict) -> str:
help="Whether to show the attributes of the DataSource.",
is_flag=True,
)
def list_data_sources(source: DataSource | None = None, show_attributes: bool = False):
@click.option(
"--show-sensors",
"show_sensors",
type=bool,
help="Whether to list the sensors which hold data recorded by the DataSource. Requires --id.",
is_flag=True,
)
def list_data_sources(
source: DataSource | None = None,
show_attributes: bool = False,
show_sensors: bool = False,
):
"""
Show available data sources
"""
if show_sensors and source is None:
# Looking up the sensors of a source queries the (potentially huge) timed_belief table,
# so we only do it for the single source the user asked about.
raise click.UsageError("--show-sensors requires --id.")

if source is None:
sources = db.session.scalars(
select(DataSource)
Expand All @@ -440,7 +456,7 @@ def list_data_sources(source: DataSource | None = None, show_attributes: bool =
click.secho("No data sources created yet.", **MsgStyle.WARN)
raise click.Abort()

headers = ["ID", "Name", "User ID", "Model", "Version"]
headers = ["ID", "Name", "Account ID", "User ID", "Model", "Version"]

if show_attributes:
headers.append("Attributes")
Expand All @@ -451,6 +467,7 @@ def list_data_sources(source: DataSource | None = None, show_attributes: bool =
row = [
source.id,
source.name,
source.account_id,
source.user_id,
source.model,
source.version,
Expand All @@ -469,6 +486,37 @@ def list_data_sources(source: DataSource | None = None, show_attributes: bool =
click.echo(tabulate(row, headers=headers))
click.echo("\n")

if show_sensors:
# At this point we know there is exactly one source, as --show-sensors requires --id.
_list_sensors_of_source(sources[0])


def _list_sensors_of_source(source: DataSource):
"""List the sensors which hold data recorded by the given data source."""
sensors = sorted(source.sensors, key=lambda sensor: sensor.id)
if not sensors:
click.secho(
f"No sensors hold data recorded by data source {source.id}.",
**MsgStyle.WARN,
)
return

click.echo(f"Sensors with data from data source {source.id}:\n")
click.echo(
tabulate(
[
[
sensor.id,
sensor.name,
sensor.unit,
f"{sensor.generic_asset.name} (ID: {sensor.generic_asset.id})",
]
for sensor in sensors
],
headers=["ID", "Name", "Unit", "Asset"],
)
)


@fm_show_data.command("chart")
@with_appcontext
Expand Down
89 changes: 89 additions & 0 deletions flexmeasures/cli/tests/test_data_show.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import pytest
from sqlalchemy import select

from flexmeasures.cli.tests.utils import (
check_command_ran_without_error,
Expand Down Expand Up @@ -93,6 +94,94 @@ def test_list_sources(app, fresh_db, setup_sources_fresh_db):
check_command_ran_without_error(result)


def test_list_sources_shows_account(app, fresh_db, setup_accounts_fresh_db):
"""The account a source belongs to is what tells apart otherwise identical sources."""
from flexmeasures.cli.data_show import list_data_sources
from flexmeasures.data.models.data_sources import DataSource

account = setup_accounts_fresh_db["Prosumer"]
fresh_db.session.add(
DataSource(name="Ada", type="demo script", account_id=account.id)
)
fresh_db.session.commit()

runner = app.test_cli_runner()
result = runner.invoke(list_data_sources)

check_command_ran_without_error(result)
assert "Account ID" in result.output
assert str(account.id) in result.output


def test_list_source_sensors(app, fresh_db, setup_dummy_data):
"""A source which recorded beliefs on two sensors lists both, with their asset."""
from flexmeasures.cli.data_show import list_data_sources
from flexmeasures.data.models.data_sources import DataSource

source = fresh_db.session.execute(
select(DataSource).filter_by(name="source1")
).scalar_one()

runner = app.test_cli_runner()
result = runner.invoke(
list_data_sources, ["--id", str(source.id), "--show-sensors"]
)

check_command_ran_without_error(result)
assert f"Sensors with data from data source {source.id}" in result.output
for sensor_name in ("sensor 1", "sensor 2"):
assert sensor_name in result.output
# The sensors' asset is shown, and sensors without data from this source are not listed
assert "DummyGenericAsset" in result.output
assert "report sensor" not in result.output


def test_list_source_sensors_without_any_data(app, fresh_db, setup_sources_fresh_db):
"""A source which recorded no beliefs at all says so, rather than showing an empty table."""
from flexmeasures.cli.data_show import list_data_sources

fresh_db.session.commit() # get IDs in DB
source = setup_sources_fresh_db["Seita"]

runner = app.test_cli_runner()
result = runner.invoke(
list_data_sources, ["--id", str(source.id), "--show-sensors"]
)

check_command_ran_without_error(result)
assert f"No sensors hold data recorded by data source {source.id}" in result.output


def test_list_source_sensors_requires_a_single_source(app, fresh_db):
"""Looking up sensors scans the timed_belief table, so it is not allowed for a full listing."""
from flexmeasures.cli.data_show import list_data_sources

runner = app.test_cli_runner()
result = runner.invoke(list_data_sources, ["--show-sensors"])

assert result.exit_code != 0
assert "--show-sensors requires --id" in result.output


def test_list_sources_with_deleted_user_and_account(app, fresh_db):
"""The user and account columns have no DB-level FK, so a source can outlive what they point to."""
from flexmeasures.cli.data_show import list_data_sources
from flexmeasures.data.models.data_sources import DataSource

orphaned_source = DataSource(name="Orphan", type="demo script")
orphaned_source.user_id = 999999
orphaned_source.account_id = 999999
fresh_db.session.add(orphaned_source)
fresh_db.session.commit()

runner = app.test_cli_runner()
result = runner.invoke(list_data_sources, ["--id", str(orphaned_source.id)])

check_command_ran_without_error(result)
assert "Orphan" in result.output
assert "999999" in result.output


def test_show_accounts(app, fresh_db, setup_accounts_fresh_db):
from flexmeasures.cli.data_show import show_account

Expand Down
Loading