diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 9248c2e26..8deec0cc5 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -9,6 +9,7 @@ New features ------------- * Hit the replay button to replay what happened, available on the sensor and asset pages [see `PR #463 `_] +* The asset page also allows to show sensor data from other assets that belong to the same account [see `PR #500 `_] * Improved import of time series data from CSV file: 1) drop duplicate records with warning, and 2) allow configuring which column contains explicit recording times for each data point (use case: import forecasts) [see `PR #501 `_] Bugfixes diff --git a/flexmeasures/api/v3_0/sensors.py b/flexmeasures/api/v3_0/sensors.py index 0ff900f0a..4449658f8 100644 --- a/flexmeasures/api/v3_0/sensors.py +++ b/flexmeasures/api/v3_0/sensors.py @@ -110,7 +110,7 @@ def index(self, account: Account): :status 403: INVALID_SENDER :status 422: UNPROCESSABLE_ENTITY """ - sensors = get_sensors(account_name=account.name) + sensors = get_sensors(account=account) return sensors_schema.dump(sensors), 200 @route("/data", methods=["POST"]) diff --git a/flexmeasures/data/models/generic_assets.py b/flexmeasures/data/models/generic_assets.py index ae35ed90e..8e62cca92 100644 --- a/flexmeasures/data/models/generic_assets.py +++ b/flexmeasures/data/models/generic_assets.py @@ -431,18 +431,27 @@ def search_beliefs( def sensors_to_show(self) -> List["Sensor"]: # noqa F821 """Sensors to show, as defined by the sensors_to_show attribute. + Sensors to show are defined as a list of sensor ids, which + is set by the "sensors_to_show" field of the asset's "attributes" column. + Valid sensors either belong to the asset itself, to other assets in the same account, + or to public assets. + + Defaults to two of the asset's sensors. """ if not self.has_attribute("sensors_to_show"): return self.sensors[:2] - from flexmeasures.data.services.sensors import get_public_sensors + from flexmeasures.data.services.sensors import get_sensors sensor_ids = self.get_attribute("sensors_to_show") sensor_map = { sensor.id: sensor - for sensor in self.sensors + get_public_sensors(sensor_ids) - if sensor.id in sensor_ids + for sensor in get_sensors( + account=self.owner, + include_public_assets=True, + sensor_id_allowlist=sensor_ids, + ) } # Return sensors in the order given by the sensors_to_show attribute diff --git a/flexmeasures/data/services/sensors.py b/flexmeasures/data/services/sensors.py index e6e4bad65..fd645e760 100644 --- a/flexmeasures/data/services/sensors.py +++ b/flexmeasures/data/services/sensors.py @@ -1,43 +1,43 @@ from __future__ import annotations -from werkzeug.exceptions import NotFound +import sqlalchemy as sa from flexmeasures import Sensor, Account from flexmeasures.data.models.generic_assets import GenericAsset def get_sensors( - account_name: str | None = None, + account: Account | list[Account], + include_public_assets: bool = False, + sensor_id_allowlist: list[int] | None = None, + sensor_name_allowlist: list[str] | None = None, ) -> list[Sensor]: - """Return a list of Sensor objects. + """Return a list of Sensor objects that belong to the given account, and/or public sensors. - :param account_name: optionally, filter by account name. + :param account: select only sensors from this account (or list of accounts) + :param include_public_assets: if True, include sensors that belong to a public asset + :param sensor_id_allowlist: optionally, allow only sensors whose id is in this list + :param sensor_name_allowlist: optionally, allow only sensors whose name is in this list """ sensor_query = Sensor.query - - if account_name is not None: - account = Account.query.filter(Account.name == account_name).one_or_none() - if not account: - raise NotFound(f"There is no account named {account_name}!") - sensor_query = ( - sensor_query.join(GenericAsset) - .filter(Sensor.generic_asset_id == GenericAsset.id) - .filter(GenericAsset.owner == account) - ) - - return sensor_query.all() - - -def get_public_sensors(sensor_ids: list[int] | None = None) -> list[Sensor]: - """Return a list of Sensor objects that belong to a public asset. - - :param sensor_ids: optionally, filter by sensor id. - """ - sensor_query = ( - Sensor.query.join(GenericAsset) - .filter(Sensor.generic_asset_id == GenericAsset.id) - .filter(GenericAsset.account_id.is_(None)) + if isinstance(account, list): + account_ids = [account.id for account in account] + else: + account_ids = [account.id] + sensor_query = sensor_query.join(GenericAsset).filter( + Sensor.generic_asset_id == GenericAsset.id ) - if sensor_ids: - sensor_query = sensor_query.filter(Sensor.id.in_(sensor_ids)) + if include_public_assets: + sensor_query = sensor_query.filter( + sa.or_( + GenericAsset.account_id.in_(account_ids), + GenericAsset.account_id.is_(None), + ) + ) + else: + sensor_query = sensor_query.filter(GenericAsset.account_id.in_(account_ids)) + if sensor_id_allowlist: + sensor_query = sensor_query.filter(Sensor.id.in_(sensor_id_allowlist)) + if sensor_name_allowlist: + sensor_query = sensor_query.filter(Sensor.name.in_(sensor_name_allowlist)) return sensor_query.all()