From ea2b2ca0391ee6648bf74c89b9a172ee07ae78d9 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Fri, 16 Jun 2023 20:23:50 +0200 Subject: [PATCH 1/6] fix: get sensor data timerange in case of a single data point Signed-off-by: F.N. Claessen --- flexmeasures/data/models/generic_assets.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/flexmeasures/data/models/generic_assets.py b/flexmeasures/data/models/generic_assets.py index facdf2c7b..f20f096b9 100644 --- a/flexmeasures/data/models/generic_assets.py +++ b/flexmeasures/data/models/generic_assets.py @@ -539,11 +539,21 @@ def get_timerange(cls, sensors: List["Sensor"]) -> Dict[str, datetime]: # noqa .limit(1) ) results = least_recent_query.union_all(most_recent_query).all() - if not results: - # return now in case there is no data for any of the sensors - now = server_now() - return dict(start=now, end=now) - least_recent, most_recent = results + try: + # try the most common case first (sensor has more than 1 data point) + least_recent, most_recent = results + except ValueError as e: + if not results: + # return now in case there is no data for any of the sensors + now = server_now() + return dict(start=now, end=now) + elif len(results) == 1: + # return the start and end of the only data point found + least_recent = most_recent = results[0] + else: + # reraise this unlikely error + raise e + return dict(start=least_recent.event_start, end=most_recent.event_end) From 2d708584bc5c2ed99e3bc45058446a30ef227c1d Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Fri, 16 Jun 2023 20:38:32 +0200 Subject: [PATCH 2/6] refactor: move to util function Signed-off-by: F.N. Claessen --- flexmeasures/data/models/generic_assets.py | 37 +++------------------ flexmeasures/data/services/timerange.py | 38 ++++++++++++++++++++++ 2 files changed, 42 insertions(+), 33 deletions(-) create mode 100644 flexmeasures/data/services/timerange.py diff --git a/flexmeasures/data/models/generic_assets.py b/flexmeasures/data/models/generic_assets.py index f20f096b9..fe7e792a8 100644 --- a/flexmeasures/data/models/generic_assets.py +++ b/flexmeasures/data/models/generic_assets.py @@ -20,13 +20,11 @@ from flexmeasures.data.models.parsing_utils import parse_source_arg from flexmeasures.data.models.user import User from flexmeasures.data.queries.annotations import query_asset_annotations +from flexmeasures.data.services.timerange import get_timerange from flexmeasures.auth.policy import AuthModelMixin, EVERY_LOGGED_IN_USER from flexmeasures.utils import geo_utils from flexmeasures.utils.coding_utils import flatten_unique -from flexmeasures.utils.time_utils import ( - determine_minimum_resampling_resolution, - server_now, -) +from flexmeasures.utils.time_utils import determine_minimum_resampling_resolution class GenericAssetType(db.Model): @@ -525,36 +523,9 @@ def get_timerange(cls, sensors: List["Sensor"]) -> Dict[str, datetime]: # noqa 'end': datetime.datetime(2020, 12, 3, 14, 30, tzinfo=pytz.utc) } """ - from flexmeasures.data.models.time_series import TimedBelief - sensor_ids = [s.id for s in flatten_unique(sensors)] - least_recent_query = ( - TimedBelief.query.filter(TimedBelief.sensor_id.in_(sensor_ids)) - .order_by(TimedBelief.event_start.asc()) - .limit(1) - ) - most_recent_query = ( - TimedBelief.query.filter(TimedBelief.sensor_id.in_(sensor_ids)) - .order_by(TimedBelief.event_start.desc()) - .limit(1) - ) - results = least_recent_query.union_all(most_recent_query).all() - try: - # try the most common case first (sensor has more than 1 data point) - least_recent, most_recent = results - except ValueError as e: - if not results: - # return now in case there is no data for any of the sensors - now = server_now() - return dict(start=now, end=now) - elif len(results) == 1: - # return the start and end of the only data point found - least_recent = most_recent = results[0] - else: - # reraise this unlikely error - raise e - - return dict(start=least_recent.event_start, end=most_recent.event_end) + start, end = get_timerange(sensor_ids) + return dict(start=start, end=end) def create_generic_asset(generic_asset_type: str, **kwargs) -> GenericAsset: diff --git a/flexmeasures/data/services/timerange.py b/flexmeasures/data/services/timerange.py new file mode 100644 index 000000000..33909011e --- /dev/null +++ b/flexmeasures/data/services/timerange.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +from datetime import datetime + +from flexmeasures.utils import time_utils + + +def get_timerange(sensor_ids: list[int]) -> tuple[datetime, datetime]: + """Get the start and end of the least recent and most recent event, respectively.""" + from flexmeasures.data.models.time_series import TimedBelief + + least_recent_query = ( + TimedBelief.query.filter(TimedBelief.sensor_id.in_(sensor_ids)) + .order_by(TimedBelief.event_start.asc()) + .limit(1) + ) + most_recent_query = ( + TimedBelief.query.filter(TimedBelief.sensor_id.in_(sensor_ids)) + .order_by(TimedBelief.event_start.desc()) + .limit(1) + ) + results = least_recent_query.union_all(most_recent_query).all() + try: + # try the most common case first (sensor has more than 1 data point) + least_recent, most_recent = results + except ValueError as e: + if not results: + # return now in case there is no data for any of the sensors + now = time_utils.server_now() + return now, now + elif len(results) == 1: + # return the start and end of the only data point found + least_recent = most_recent = results[0] + else: + # reraise this unlikely error + raise e + + return least_recent.event_start, most_recent.event_end From e7ede1e851d81ebf6cacb64237294355b2a30bcb Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Fri, 16 Jun 2023 20:40:32 +0200 Subject: [PATCH 3/6] fix: apply fix to sensor timerange, too Signed-off-by: F.N. Claessen --- flexmeasures/data/models/time_series.py | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/flexmeasures/data/models/time_series.py b/flexmeasures/data/models/time_series.py index 222863437..73c0199e1 100644 --- a/flexmeasures/data/models/time_series.py +++ b/flexmeasures/data/models/time_series.py @@ -19,6 +19,7 @@ from flexmeasures.data import db from flexmeasures.data.models.parsing_utils import parse_source_arg from flexmeasures.data.services.annotations import prepare_annotations_for_chart +from flexmeasures.data.services.timerange import get_timerange from flexmeasures.data.queries.utils import ( create_beliefs_query, get_belief_timing_criteria, @@ -480,23 +481,8 @@ def timerange(self) -> dict[str, datetime_type]: 'end': datetime.datetime(2020, 12, 3, 14, 30, tzinfo=pytz.utc) } """ - least_recent_query = ( - TimedBelief.query.filter(TimedBelief.sensor == self) - .order_by(TimedBelief.event_start.asc()) - .limit(1) - ) - most_recent_query = ( - TimedBelief.query.filter(TimedBelief.sensor == self) - .order_by(TimedBelief.event_start.desc()) - .limit(1) - ) - results = least_recent_query.union_all(most_recent_query).all() - if not results: - # return now in case there is no data for the sensor - now = server_now() - return dict(start=now, end=now) - least_recent, most_recent = results - return dict(start=least_recent.event_start, end=most_recent.event_end) + start, end = get_timerange([self.id]) + return dict(start=start, end=end) def __repr__(self) -> str: return f"" From 4093be3baf100b85f9cd71d206b880e7a3a3ca9a Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Fri, 16 Jun 2023 20:58:34 +0200 Subject: [PATCH 4/6] fix: flake8 Signed-off-by: F.N. Claessen --- flexmeasures/data/models/time_series.py | 1 - 1 file changed, 1 deletion(-) diff --git a/flexmeasures/data/models/time_series.py b/flexmeasures/data/models/time_series.py index 73c0199e1..70f1c1f08 100644 --- a/flexmeasures/data/models/time_series.py +++ b/flexmeasures/data/models/time_series.py @@ -44,7 +44,6 @@ from flexmeasures.data.models.generic_assets import GenericAsset from flexmeasures.data.models.validation_utils import check_required_attributes from flexmeasures.data.queries.sensors import query_sensors_by_proximity -from flexmeasures.utils.time_utils import server_now from flexmeasures.utils.geo_utils import parse_lat_lng From 6e5a09959da881ad307ea6b44eceb390c5ff8eba Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 19 Jun 2023 17:17:11 +0200 Subject: [PATCH 5/6] use min and max in query Signed-off-by: F.N. Claessen --- flexmeasures/data/services/timerange.py | 49 +++++++++++-------------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/flexmeasures/data/services/timerange.py b/flexmeasures/data/services/timerange.py index 33909011e..d894ed4bf 100644 --- a/flexmeasures/data/services/timerange.py +++ b/flexmeasures/data/services/timerange.py @@ -2,37 +2,30 @@ from datetime import datetime +from sqlalchemy import func + from flexmeasures.utils import time_utils def get_timerange(sensor_ids: list[int]) -> tuple[datetime, datetime]: - """Get the start and end of the least recent and most recent event, respectively.""" - from flexmeasures.data.models.time_series import TimedBelief + """Get the start and end of the least recent and most recent event, respectively. - least_recent_query = ( - TimedBelief.query.filter(TimedBelief.sensor_id.in_(sensor_ids)) - .order_by(TimedBelief.event_start.asc()) - .limit(1) - ) - most_recent_query = ( - TimedBelief.query.filter(TimedBelief.sensor_id.in_(sensor_ids)) - .order_by(TimedBelief.event_start.desc()) - .limit(1) - ) - results = least_recent_query.union_all(most_recent_query).all() - try: - # try the most common case first (sensor has more than 1 data point) - least_recent, most_recent = results - except ValueError as e: - if not results: - # return now in case there is no data for any of the sensors - now = time_utils.server_now() - return now, now - elif len(results) == 1: - # return the start and end of the only data point found - least_recent = most_recent = results[0] - else: - # reraise this unlikely error - raise e + In case of no data, defaults to (now, now). + """ + from flexmeasures.data.models.time_series import Sensor, TimedBelief - return least_recent.event_start, most_recent.event_end + least_recent_event_start_and_most_recent_event_end = ( + TimedBelief.query.with_entities( + # least recent event start + func.min(TimedBelief.event_start), + # most recent event end + func.max(TimedBelief.event_start + Sensor.event_resolution), + ) + .join(Sensor, TimedBelief.sensor_id == Sensor.id) + .filter(TimedBelief.sensor_id.in_(sensor_ids)) + ).one_or_none() + if least_recent_event_start_and_most_recent_event_end == (None, None): + # return now in case there is no data for any of the sensors + now = time_utils.server_now() + return now, now + return least_recent_event_start_and_most_recent_event_end From be50ae03ec07da8482a5582deba9b16c3acc6ec4 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 19 Jun 2023 17:20:20 +0200 Subject: [PATCH 6/6] docs: changelog entry Signed-off-by: F.N. Claessen --- documentation/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 98260ae8a..107cd2259 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -23,6 +23,7 @@ Bugfixes ----------- * Relax constraint validation of `StorageScheduler` to accommodate violations caused by floating point precision [see `PR #731 `_] +* Fix browser console error when loading asset or sensor page with only a single data point [see `PR #732 `_] v0.14.0 | June 15, 2023