From 08505f76fc534dd863c0fa219f510b00ce37c1a9 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 1 Jun 2026 09:14:14 +0200 Subject: [PATCH 01/10] =?UTF-8?q?fix:=20@validates("storage=5Fefficiency")?= =?UTF-8?q?=20uses=20self.consumption/self.production=20which=20are=20fiel?= =?UTF-8?q?d=20descriptors,=20not=20data=20values=20=E2=80=94=20causing=20?= =?UTF-8?q?an=20AttributeError?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: F.N. Claessen --- .../data/schemas/scheduling/storage.py | 22 +++++++++++++------ .../data/schemas/tests/test_scheduling.py | 15 +++++++++++++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/flexmeasures/data/schemas/scheduling/storage.py b/flexmeasures/data/schemas/scheduling/storage.py index 183565c4b5..c138d9c8dc 100644 --- a/flexmeasures/data/schemas/scheduling/storage.py +++ b/flexmeasures/data/schemas/scheduling/storage.py @@ -509,20 +509,28 @@ def __init__(self, *args, **kwargs): for field in self.declared_fields } - @validates("storage_efficiency") - def validate_storage_efficiency_resolution( - self, unit: Sensor | ur.Quantity, **kwargs - ): + @validates_schema + def validate_storage_efficiency_resolution(self, data: dict, **kwargs): + unit = data.get("storage_efficiency") + consumption = data.get("consumption") + production = data.get("production") + consumption_is_sensor = isinstance(consumption, dict) and isinstance( + consumption.get("sensor"), Sensor + ) + production_is_sensor = isinstance(production, dict) and isinstance( + production.get("sensor"), Sensor + ) if ( isinstance(unit, ur.Quantity) - and not isinstance(self.consumption, Sensor) - and not isinstance(self.production, Sensor) + and not consumption_is_sensor + and not production_is_sensor ): raise ValidationError( "The storage-efficiency cannot be interpreted without a resolution. " "Record the storage-efficiency on a sensor instead (with a non-zero resolution) and then reference that sensor in the flex-model. " "Alternatively, set the consumption or production field in the flex-model to reference a sensor, " - "and the scheduler will assume their resolution is the one to use." + "and the scheduler will assume their resolution is the one to use.", + field_name="storage-efficiency", ) @validates_schema diff --git a/flexmeasures/data/schemas/tests/test_scheduling.py b/flexmeasures/data/schemas/tests/test_scheduling.py index 4fb1bf67af..d3c8e849f9 100644 --- a/flexmeasures/data/schemas/tests/test_scheduling.py +++ b/flexmeasures/data/schemas/tests/test_scheduling.py @@ -798,6 +798,21 @@ def test_get_variable_quantity_unit( {"storage-efficiency": {"sensor": "power-sensor"}}, {"storage-efficiency": "Cannot convert MW to %"}, ), + # plain quantity storage-efficiency without sensor-backed consumption/production should fail + ( + {"storage-efficiency": "90%"}, + { + "storage-efficiency": "The storage-efficiency cannot be interpreted without a resolution." + }, + ), + # plain quantity storage-efficiency is valid when consumption is sensor-backed + ( + { + "storage-efficiency": "90%", + "consumption": {"sensor": "power-sensor"}, + }, + False, + ), ], ) def test_db_flex_model_schema( From 11ee93e5bdfe9bb6946ed948b3928c987d4eb45e Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 1 Jun 2026 09:27:50 +0200 Subject: [PATCH 02/10] fix: move storage_efficiency validation from DBStorageFlexModelSchema to StorageFlexModelSchema; for backwards compatibility, we need to keep support for a db flex-model with storage-efficiency as a fixed quantity, without a consumption or production sensor, as the trigger message may still describe a power sensor to be used, from which the resolution can be inferred Signed-off-by: F.N. Claessen --- .../data/schemas/scheduling/storage.py | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/flexmeasures/data/schemas/scheduling/storage.py b/flexmeasures/data/schemas/scheduling/storage.py index c138d9c8dc..e965e043f8 100644 --- a/flexmeasures/data/schemas/scheduling/storage.py +++ b/flexmeasures/data/schemas/scheduling/storage.py @@ -325,6 +325,30 @@ def validate_asset(self, asset: Asset, **kwargs): if self.sensor is not None and self.sensor.asset != asset: raise ValidationError("Sensor does not belong to asset.") + @validates_schema + def validate_storage_efficiency_resolution(self, data: dict, **kwargs): + unit = data.get("storage_efficiency") + consumption = data.get("consumption") + production = data.get("production") + consumption_is_sensor = isinstance(consumption, dict) and isinstance( + consumption.get("sensor"), Sensor + ) + production_is_sensor = isinstance(production, dict) and isinstance( + production.get("sensor"), Sensor + ) + if ( + isinstance(unit, ur.Quantity) + and not consumption_is_sensor + and not production_is_sensor + ): + raise ValidationError( + "The storage-efficiency cannot be interpreted without a resolution. " + "Record the storage-efficiency on a sensor instead (with a non-zero resolution) and then reference that sensor in the flex-model. " + "Alternatively, set the consumption or production field in the flex-model to reference a sensor, " + "and the scheduler will assume their resolution is the one to use.", + field_name="storage-efficiency", + ) + @validates_schema def check_redundant_efficiencies(self, data: dict, **kwargs): """ @@ -509,30 +533,6 @@ def __init__(self, *args, **kwargs): for field in self.declared_fields } - @validates_schema - def validate_storage_efficiency_resolution(self, data: dict, **kwargs): - unit = data.get("storage_efficiency") - consumption = data.get("consumption") - production = data.get("production") - consumption_is_sensor = isinstance(consumption, dict) and isinstance( - consumption.get("sensor"), Sensor - ) - production_is_sensor = isinstance(production, dict) and isinstance( - production.get("sensor"), Sensor - ) - if ( - isinstance(unit, ur.Quantity) - and not consumption_is_sensor - and not production_is_sensor - ): - raise ValidationError( - "The storage-efficiency cannot be interpreted without a resolution. " - "Record the storage-efficiency on a sensor instead (with a non-zero resolution) and then reference that sensor in the flex-model. " - "Alternatively, set the consumption or production field in the flex-model to reference a sensor, " - "and the scheduler will assume their resolution is the one to use.", - field_name="storage-efficiency", - ) - @validates_schema def forbid_time_series_specs(self, data: dict, **kwargs): """Do not allow time series specs for the flex-model fields saved in the db.""" From d74f4e7f330d259a874964651d6fd332d00d5040 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 1 Jun 2026 09:28:36 +0200 Subject: [PATCH 03/10] fix: pass validation if device flex-model contains a power sensor (see previous commit) Signed-off-by: F.N. Claessen --- flexmeasures/data/schemas/scheduling/storage.py | 1 + 1 file changed, 1 insertion(+) diff --git a/flexmeasures/data/schemas/scheduling/storage.py b/flexmeasures/data/schemas/scheduling/storage.py index e965e043f8..4015efe0e2 100644 --- a/flexmeasures/data/schemas/scheduling/storage.py +++ b/flexmeasures/data/schemas/scheduling/storage.py @@ -338,6 +338,7 @@ def validate_storage_efficiency_resolution(self, data: dict, **kwargs): ) if ( isinstance(unit, ur.Quantity) + and not self.sensor and not consumption_is_sensor and not production_is_sensor ): From 9080e47dc466ba80916ce02eae84d7664d704efb Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 1 Jun 2026 09:30:10 +0200 Subject: [PATCH 04/10] feat: raise explicit error rather than passing silently when the resolution of the storage-efficiency cannot be inferred Signed-off-by: F.N. Claessen --- flexmeasures/data/models/planning/storage.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 4af89ae816..52f9ba0195 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -932,6 +932,13 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 device_constraints[d]["efficiency"] **= ( resolution / production[d].event_resolution ) + else: + raise ValueError( + "The storage-efficiency cannot be interpreted without a resolution. " + "Record the storage-efficiency on a sensor instead (with a non-zero resolution) and then reference that sensor in the flex-model. " + "Alternatively, set the consumption or production field in the flex-model to reference a sensor, " + "and the scheduler will assume their resolution is the one to use.", + ) # check that storage constraints are fulfilled if not skip_validation: From 5007b4163afa31a84ace9815ab1560009ff151ee Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 1 Jun 2026 09:39:10 +0200 Subject: [PATCH 05/10] fix: update tests Signed-off-by: F.N. Claessen --- .../data/schemas/tests/test_scheduling.py | 126 +++++++++++++++++- 1 file changed, 125 insertions(+), 1 deletion(-) diff --git a/flexmeasures/data/schemas/tests/test_scheduling.py b/flexmeasures/data/schemas/tests/test_scheduling.py index d3c8e849f9..c679057012 100644 --- a/flexmeasures/data/schemas/tests/test_scheduling.py +++ b/flexmeasures/data/schemas/tests/test_scheduling.py @@ -749,7 +749,7 @@ def test_get_variable_quantity_unit( ) -# test DBStorageFlexModelSchema +# test StorageFlexModelSchema @pytest.mark.parametrize( ["flex_model", "fails"], [ @@ -815,6 +815,130 @@ def test_get_variable_quantity_unit( ), ], ) +def test_flex_model_schema( + db, app, setup_dummy_sensors, setup_efficiency_sensors, flex_model, fails +): + """Validate StorageFlexModelSchema for accepted and rejected flex-model inputs. + + Input under test: + - ``flex_model`` payloads with fixed quantities, sensor references, and list fields + (for example ``soc-min``, ``soc-minima``, ``soc-gain``, + ``roundtrip-efficiency``, ``storage-efficiency``). + - Sensor placeholders in parametrized payloads are replaced with fixture-backed + sensor IDs before schema loading. + + Expected outcomes: + - When ``fails`` is ``False``, schema loading succeeds. + - When ``fails`` is a field-to-message mapping, schema loading raises + ``ValidationError`` and contains the expected field-specific error message(s). + """ + + schema = StorageFlexModelSchema(start=datetime(2026, 6, 1), sensor=None) + + sensors = { + "energy-sensor": setup_dummy_sensors[0], + "price-sensor": setup_dummy_sensors[1], + "power-sensor": setup_dummy_sensors[3], + "efficiency-sensor": setup_efficiency_sensors, + } + + for field_name, field_value in flex_model.items(): + if isinstance(field_value, dict) and "sensor" in field_value: + # Replace sensor name with sensor ID + flex_model[field_name]["sensor"] = sensors[ + flex_model[field_name]["sensor"] + ].id + if isinstance(field_value, list): + # Replace sensor names in lists with sensor IDs + flex_model[field_name] = [ + {"sensor": sensors[item["sensor"]].id} if "sensor" in item else item + for item in field_value + ] + + if fails: + with pytest.raises(ValidationError) as e_info: + schema.load(flex_model) + for field_name, expected_message in fails.items(): + assert field_name in e_info.value.messages + if field_name in ["soc-gain", "soc-usage"]: + for index, message_list in e_info.value.messages[field_name].items(): + assert message_list[0] == expected_message[index][0] + else: + # Check all messages for the given field for the expected message + assert any( + [ + expected_message in message + for message in e_info.value.messages[field_name] + ] + ) + else: + schema.load(flex_model) + + +# test DBStorageFlexModelSchema +@pytest.mark.parametrize( + ["flex_model", "fails"], + [ + ( + {"soc-min": "450 EUR/MWh"}, + {"soc-min": "Cannot convert value `450 EUR/MWh` to 'MWh'"}, + ), + ( + {"soc-min": "3500 kWh"}, + False, + ), + ( + {"soc-minima": {"sensor": "energy-sensor"}}, + False, + ), + ( + {"soc-minima": {"sensor": "price-sensor"}}, + {"soc-minima": "Cannot convert EUR/MWh to MWh"}, + ), + ( + {"soc-gain": ["450 EUR/MWh", "650 EUR/MWh"]}, + { + "soc-gain": [ + ["Cannot convert value `450 EUR/MWh` to 'MW'"], + ["Cannot convert value `650 EUR/MWh` to 'MW'"], + ] + }, + ), + ( + {"soc-usage": ["3500 kW", {"sensor": "power-sensor"}]}, + False, + ), + ( + {"roundtrip-efficiency": "90%"}, + False, + ), + ( + {"roundtrip-efficiency": "12 MW"}, + {"roundtrip-efficiency": "Cannot convert value `12 MW` to '%'"}, + ), + ( + {"storage-efficiency": {"sensor": "efficiency-sensor"}}, + False, + ), + ( + {"storage-efficiency": {"sensor": "power-sensor"}}, + {"storage-efficiency": "Cannot convert MW to %"}, + ), + # plain quantity storage-efficiency without sensor-backed consumption/production should fail + ( + {"storage-efficiency": "90%"}, + False, + ), + # plain quantity storage-efficiency is valid when consumption is sensor-backed + ( + { + "storage-efficiency": "90%", + "consumption": {"sensor": "power-sensor"}, + }, + False, + ), + ], +) def test_db_flex_model_schema( db, app, setup_dummy_sensors, setup_efficiency_sensors, flex_model, fails ): From 14fba2fcd436113925ebd612693335074de7f67e Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 1 Jun 2026 09:58:34 +0200 Subject: [PATCH 06/10] refactor: combine tests for checking StorageFlexModelSchema and DBStorageFlexModelSchema Signed-off-by: F.N. Claessen --- .../data/schemas/tests/test_scheduling.py | 186 ++++-------------- 1 file changed, 38 insertions(+), 148 deletions(-) diff --git a/flexmeasures/data/schemas/tests/test_scheduling.py b/flexmeasures/data/schemas/tests/test_scheduling.py index c679057012..eb54348f29 100644 --- a/flexmeasures/data/schemas/tests/test_scheduling.py +++ b/flexmeasures/data/schemas/tests/test_scheduling.py @@ -749,7 +749,7 @@ def test_get_variable_quantity_unit( ) -# test StorageFlexModelSchema +# test StorageFlexModelSchema and DBStorageFlexModelSchema @pytest.mark.parametrize( ["flex_model", "fails"], [ @@ -801,133 +801,12 @@ def test_get_variable_quantity_unit( # plain quantity storage-efficiency without sensor-backed consumption/production should fail ( {"storage-efficiency": "90%"}, - { - "storage-efficiency": "The storage-efficiency cannot be interpreted without a resolution." - }, - ), - # plain quantity storage-efficiency is valid when consumption is sensor-backed - ( - { - "storage-efficiency": "90%", - "consumption": {"sensor": "power-sensor"}, - }, - False, - ), - ], -) -def test_flex_model_schema( - db, app, setup_dummy_sensors, setup_efficiency_sensors, flex_model, fails -): - """Validate StorageFlexModelSchema for accepted and rejected flex-model inputs. - - Input under test: - - ``flex_model`` payloads with fixed quantities, sensor references, and list fields - (for example ``soc-min``, ``soc-minima``, ``soc-gain``, - ``roundtrip-efficiency``, ``storage-efficiency``). - - Sensor placeholders in parametrized payloads are replaced with fixture-backed - sensor IDs before schema loading. - - Expected outcomes: - - When ``fails`` is ``False``, schema loading succeeds. - - When ``fails`` is a field-to-message mapping, schema loading raises - ``ValidationError`` and contains the expected field-specific error message(s). - """ - - schema = StorageFlexModelSchema(start=datetime(2026, 6, 1), sensor=None) - - sensors = { - "energy-sensor": setup_dummy_sensors[0], - "price-sensor": setup_dummy_sensors[1], - "power-sensor": setup_dummy_sensors[3], - "efficiency-sensor": setup_efficiency_sensors, - } - - for field_name, field_value in flex_model.items(): - if isinstance(field_value, dict) and "sensor" in field_value: - # Replace sensor name with sensor ID - flex_model[field_name]["sensor"] = sensors[ - flex_model[field_name]["sensor"] - ].id - if isinstance(field_value, list): - # Replace sensor names in lists with sensor IDs - flex_model[field_name] = [ - {"sensor": sensors[item["sensor"]].id} if "sensor" in item else item - for item in field_value - ] - - if fails: - with pytest.raises(ValidationError) as e_info: - schema.load(flex_model) - for field_name, expected_message in fails.items(): - assert field_name in e_info.value.messages - if field_name in ["soc-gain", "soc-usage"]: - for index, message_list in e_info.value.messages[field_name].items(): - assert message_list[0] == expected_message[index][0] - else: - # Check all messages for the given field for the expected message - assert any( - [ - expected_message in message - for message in e_info.value.messages[field_name] - ] - ) - else: - schema.load(flex_model) - - -# test DBStorageFlexModelSchema -@pytest.mark.parametrize( - ["flex_model", "fails"], - [ - ( - {"soc-min": "450 EUR/MWh"}, - {"soc-min": "Cannot convert value `450 EUR/MWh` to 'MWh'"}, - ), - ( - {"soc-min": "3500 kWh"}, - False, - ), - ( - {"soc-minima": {"sensor": "energy-sensor"}}, - False, - ), - ( - {"soc-minima": {"sensor": "price-sensor"}}, - {"soc-minima": "Cannot convert EUR/MWh to MWh"}, - ), - ( - {"soc-gain": ["450 EUR/MWh", "650 EUR/MWh"]}, - { - "soc-gain": [ - ["Cannot convert value `450 EUR/MWh` to 'MW'"], - ["Cannot convert value `650 EUR/MWh` to 'MW'"], - ] - }, - ), - ( - {"soc-usage": ["3500 kW", {"sensor": "power-sensor"}]}, - False, - ), - ( - {"roundtrip-efficiency": "90%"}, - False, - ), - ( - {"roundtrip-efficiency": "12 MW"}, - {"roundtrip-efficiency": "Cannot convert value `12 MW` to '%'"}, - ), - ( - {"storage-efficiency": {"sensor": "efficiency-sensor"}}, - False, - ), - ( - {"storage-efficiency": {"sensor": "power-sensor"}}, - {"storage-efficiency": "Cannot convert MW to %"}, - ), - # plain quantity storage-efficiency without sensor-backed consumption/production should fail - ( - {"storage-efficiency": "90%"}, - False, + [ + { + "storage-efficiency": "The storage-efficiency cannot be interpreted without a resolution." + }, + False, + ], ), # plain quantity storage-efficiency is valid when consumption is sensor-backed ( @@ -939,10 +818,10 @@ def test_flex_model_schema( ), ], ) -def test_db_flex_model_schema( +def test_flex_model_schemas( db, app, setup_dummy_sensors, setup_efficiency_sensors, flex_model, fails ): - """Validate DBStorageFlexModelSchema for accepted and rejected flex-model inputs. + """Validate StorageFlexModelSchema and DBStorageFlexModelSchema for accepted and rejected flex-model inputs. Input under test: - ``flex_model`` payloads with fixed quantities, sensor references, and list fields @@ -955,8 +834,16 @@ def test_db_flex_model_schema( - When ``fails`` is ``False``, schema loading succeeds. - When ``fails`` is a field-to-message mapping, schema loading raises ``ValidationError`` and contains the expected field-specific error message(s). + - When ``fails`` is a list, its first entry represents the expectation for the StorageFlexModelSchema, + and the second entry represents the expectation for the DBStorageFlexModelSchema. """ - schema = DBStorageFlexModelSchema() + schemas = [ + StorageFlexModelSchema(start=datetime(2026, 6, 1), sensor=None), + DBStorageFlexModelSchema(), + ] + if not isinstance(fails, list): + # Then the same expectation holds for both schemas + fails = [fails, fails] sensors = { "energy-sensor": setup_dummy_sensors[0], @@ -978,21 +865,24 @@ def test_db_flex_model_schema( for item in field_value ] - if fails: - with pytest.raises(ValidationError) as e_info: + for schema, fail in zip(schemas, fails): + if fail: + with pytest.raises(ValidationError) as e_info: + schema.load(flex_model) + for field_name, expected_message in fail.items(): + assert field_name in e_info.value.messages + if field_name in ["soc-gain", "soc-usage"]: + for index, message_list in e_info.value.messages[ + field_name + ].items(): + assert message_list[0] == expected_message[index][0] + else: + # Check all messages for the given field for the expected message + assert any( + [ + expected_message in message + for message in e_info.value.messages[field_name] + ] + ) + else: schema.load(flex_model) - for field_name, expected_message in fails.items(): - assert field_name in e_info.value.messages - if field_name in ["soc-gain", "soc-usage"]: - for index, message_list in e_info.value.messages[field_name].items(): - assert message_list[0] == expected_message[index][0] - else: - # Check all messages for the given field for the expected message - assert any( - [ - expected_message in message - for message in e_info.value.messages[field_name] - ] - ) - else: - schema.load(flex_model) From 14330c1f611095fc8bd9bfd878fd5afae167b36a Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 1 Jun 2026 10:10:30 +0200 Subject: [PATCH 07/10] docs: changelog entry Signed-off-by: F.N. Claessen --- documentation/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 0375d293a4..ed391bdb6d 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -23,7 +23,7 @@ New features * Improve UX after deleting a child asset through the UI [see `PR #2119 `_] * Improve source filtering in the sensor data GET endpoint by exposing the documented query parameters in Swagger and allowing filtering by the account linked to data sources [see `PR #2083 `_ and `PR #2151 `_] * Support sensor references for efficiency fields in storage flex-models [see `PR #2142 `_] -* Introduce the ``consumption`` and ``production`` flex-model fields for the ``StorageScheduler`` to save schedules to [see `PR #2190 `_] +* Introduce the ``consumption`` and ``production`` flex-model fields for the ``StorageScheduler`` to save schedules to [see `PR #2190 `_ and `PR #2213 `_] * Added a unified job status endpoint ``GET /api/v3_0/jobs/`` to retrieve the current execution status and result message for any background job [see `PR #2141 `_] * Add ``flexmeasures jobs inspect-job`` CLI command to show job status and metadata information (similar to the job status endpoint in the API) [see `PR #2202 `_] * 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 `_] From 34d0b4e10cfeca5b2be8052dc5b8df0077525cf2 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 1 Jun 2026 10:11:25 +0200 Subject: [PATCH 08/10] docs: streamline punctuation Signed-off-by: F.N. Claessen --- documentation/changelog.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/documentation/changelog.rst b/documentation/changelog.rst index ed391bdb6d..a9a19df3aa 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -212,7 +212,7 @@ New features * Improved the UX for creating sensors, clicking on ``Enter`` now validates and creates a sensor [see `PR #1876 `_] * Show zero values in bar charts even though they have 0 area [see `PR #1932 `_ and `PR #1936 `_] * Added ``root`` and ``depth`` fields to the `[GET] /assets` endpoint for listing assets, to allow selecting descendants of a given root asset up to a given depth [see `PR #1874 `_] -* Support creating schedules with only information known prior to some time, now also via the CLI (the API already supported it) [see `PR #1871 `_]. +* Support creating schedules with only information known prior to some time, now also via the CLI (the API already supported it) [see `PR #1871 `_] * Added capability to update an asset's parent from the UI [`PR #1957 `_] * Add ``fields`` param to the asset-listing endpoints, to save bandwidth in response data [see `PR #1884 `_] @@ -306,7 +306,7 @@ v0.30.0 | December 2, 2025 New features ------------- -* Improve ``flexmeasures add forecasts`` CLI command with clearer success and error messages when creating jobs or beliefs [see `PR #1822 `_]. +* Improve ``flexmeasures add forecasts`` CLI command with clearer success and error messages when creating jobs or beliefs [see `PR #1822 `_] * Major overhaul of ``flexmeasures add forecasts`` (use the ``--help`` option to learn more) [see `PR #1546 `_, `PR #1744 `_ and `PR #1834 `_] * The new ``commitments`` field in the ``flex-context`` can be used to specify previous commitments (e.g. market positions) that the scheduler needs to take into account [see `PR #1754 `_ and `PR #1793 `_] * Many more field descriptions in Swagger, including flex-model and flex-context fields [see `PR #1777 `_ and `PR #1841 `_] From c630eb200a4aadcb3e6f8e2a348584dd7f2dcedd Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 1 Jun 2026 10:26:42 +0200 Subject: [PATCH 09/10] fix: pass in case of trivial storage-efficiency Signed-off-by: F.N. Claessen --- flexmeasures/data/models/planning/storage.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 52f9ba0195..9e9c7bd960 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -932,7 +932,13 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 device_constraints[d]["efficiency"] **= ( resolution / production[d].event_resolution ) - else: + elif ( + ~( + device_constraints[d]["efficiency"].isna() + | device_constraints[d]["efficiency"].eq(1) + ) + ).any(): + # Only trivial storage efficiency (missing or 1), which needs no resampling raise ValueError( "The storage-efficiency cannot be interpreted without a resolution. " "Record the storage-efficiency on a sensor instead (with a non-zero resolution) and then reference that sensor in the flex-model. " From 6f29baf4f49ed35dd5837edb1f588b8390f71bdf Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 1 Jun 2026 10:32:57 +0200 Subject: [PATCH 10/10] refactor: move most common case to the front Signed-off-by: F.N. Claessen --- flexmeasures/data/models/planning/storage.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 9e9c7bd960..58284ca278 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -908,7 +908,10 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 device_constraints[d]["efficiency"] = storage_efficiency[d] # Convert efficiency from sensor resolution to scheduling resolution - if isinstance(storage_efficiency[d], Sensor): + if device_constraints[d]["efficiency"].dropna().eq(1).all(): + # Only missing or unit efficiency; no resampling needed. + pass + elif isinstance(storage_efficiency[d], Sensor): # Resample from the resolution of the storage-efficiency sensor device_constraints[d]["efficiency"] **= ( resolution / storage_efficiency[d].event_resolution @@ -932,13 +935,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 device_constraints[d]["efficiency"] **= ( resolution / production[d].event_resolution ) - elif ( - ~( - device_constraints[d]["efficiency"].isna() - | device_constraints[d]["efficiency"].eq(1) - ) - ).any(): - # Only trivial storage efficiency (missing or 1), which needs no resampling + else: raise ValueError( "The storage-efficiency cannot be interpreted without a resolution. " "Record the storage-efficiency on a sensor instead (with a non-zero resolution) and then reference that sensor in the flex-model. "