From f08065508ae75d655d1596de72f444743fe79919 Mon Sep 17 00:00:00 2001 From: Ahmad-Wahid Date: Fri, 27 Feb 2026 02:22:29 +0100 Subject: [PATCH 1/5] fix: drop a dictionary from flexmodel that has no sensor Signed-off-by: Ahmad-Wahid --- flexmeasures/data/models/planning/storage.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 4d17ee4596..60e05d67d0 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -89,6 +89,11 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 if not self.config_deserialized: self.deserialize_config() + # todo: look for the reason why flex_model has an object(dict) without a sensor, and fix the root cause if possible, instead of filtering it out here + self.flex_model = [ + model for model in self.flex_model if model["sensor"] is not None + ] + start = self.start end = self.end resolution = self.resolution From 56d9a96aee2fed5792b3cc04ca04d1fa57a73dca Mon Sep 17 00:00:00 2001 From: Ahmad-Wahid Date: Fri, 27 Feb 2026 02:26:15 +0100 Subject: [PATCH 2/5] feat: return inflexible device sensors data along with schedules Signed-off-by: Ahmad-Wahid --- flexmeasures/data/models/planning/storage.py | 54 +++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 60e05d67d0..dc891edadf 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -940,6 +940,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 device_constraints, ems_constraints, commitments, + inflexible_device_sensors, ) def convert_to_commitments( @@ -1252,6 +1253,7 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType: device_constraints, ems_constraints, commitments, + inflexible_device_sensors, ) = self._prepare(skip_validation=skip_validation) # Fallback policy if the problem was unsolvable @@ -1320,6 +1322,7 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType: device_constraints, ems_constraints, commitments, + inflexible_device_sensors, ) = self._prepare(skip_validation=skip_validation) ems_schedule, expected_costs, scheduler_results, model = device_scheduler( @@ -1347,6 +1350,16 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType: elif sensor is not None and sensor in storage_schedule: storage_schedule[sensor] += ems_schedule[d] + # Obtain the inflexible device schedules + num_flexible_devices = len(sensors) + inflexible_schedules = dict() + for i, inflexible_sensor in enumerate(inflexible_device_sensors): + device_index = num_flexible_devices + i + if inflexible_sensor not in inflexible_schedules: + inflexible_schedules[inflexible_sensor] = ems_schedule[device_index] + else: + inflexible_schedules[inflexible_sensor] += ems_schedule[device_index] + # Obtain the aggregate power schedule, too, if the flex-context states the associated sensor. Fill with the sum of schedules made here. aggregate_power_sensor = self.flex_context.get("aggregate_power", None) if isinstance(aggregate_power_sensor, Sensor): @@ -1366,6 +1379,18 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType: if sensor is not None } + # Convert each inflexible device schedule to the unit of the device's power sensor + inflexible_schedules = { + sensor: convert_units( + inflexible_schedules[sensor], + "MW", + sensor.unit, + event_resolution=sensor.event_resolution, + ) + for sensor in inflexible_schedules.keys() + if sensor is not None + } + flex_model = self.flex_model.copy() if not isinstance(self.flex_model, list): @@ -1402,6 +1427,13 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType: for sensor in storage_schedule.keys() if sensor is not None } + inflexible_schedules = { + sensor: inflexible_schedules[sensor] + .resample(sensor.event_resolution) + .mean() + for sensor in inflexible_schedules.keys() + if sensor is not None + } # Round schedule if self.round_to_decimals: @@ -1410,6 +1442,11 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType: for sensor in storage_schedule.keys() if sensor is not None } + inflexible_schedules = { + sensor: inflexible_schedules[sensor].round(self.round_to_decimals) + for sensor in inflexible_schedules.keys() + if sensor is not None + } soc_schedule = { sensor: soc_schedule[sensor].round(self.round_to_decimals) for sensor in soc_schedule.keys() @@ -1426,6 +1463,16 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType: for sensor in storage_schedule.keys() if sensor is not None ] + inflexible_device_schedules = [ + { + "name": "inflexible_device_schedule", + "sensor": sensor, + "data": inflexible_schedules[sensor], + "unit": sensor.unit, + } + for sensor in inflexible_schedules.keys() + if sensor is not None + ] commitment_costs = [ { "name": "commitment_costs", @@ -1447,7 +1494,12 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType: } for sensor, soc in soc_schedule.items() ] - return storage_schedules + commitment_costs + soc_schedules + return ( + storage_schedules + + inflexible_device_schedules + + commitment_costs + + soc_schedules + ) else: return storage_schedule[sensors[0]] From 54b79b8fd9316d5e4db61afa271d43f1ceb0bfac Mon Sep 17 00:00:00 2001 From: Ahmad-Wahid Date: Mon, 2 Mar 2026 19:02:37 +0100 Subject: [PATCH 3/5] fix: support a single flex model which is not in a list Signed-off-by: Ahmad-Wahid --- flexmeasures/data/models/planning/storage.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 4cf102ac3b..38a89c5c59 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -90,9 +90,10 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 self.deserialize_config() # todo: look for the reason why flex_model has an object(dict) without a sensor, and fix the root cause if possible, instead of filtering it out here - self.flex_model = [ - model for model in self.flex_model if model["sensor"] is not None - ] + if isinstance(self.flex_model, list): + self.flex_model = [ + model for model in self.flex_model if model["sensor"] is not None + ] start = self.start end = self.end From 25a381577f52c23a7aac6cb3cbfc946151a9e543 Mon Sep 17 00:00:00 2001 From: Ahmad-Wahid Date: Mon, 2 Mar 2026 20:12:16 +0100 Subject: [PATCH 4/5] fix: unpack all available returns Signed-off-by: Ahmad-Wahid --- flexmeasures/data/models/planning/tests/test_solver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/flexmeasures/data/models/planning/tests/test_solver.py b/flexmeasures/data/models/planning/tests/test_solver.py index 919936d315..233a44b417 100644 --- a/flexmeasures/data/models/planning/tests/test_solver.py +++ b/flexmeasures/data/models/planning/tests/test_solver.py @@ -1370,6 +1370,7 @@ def set_if_not_none(dictionary, key, value): device_constraints, ems_constraints, commitments, + inflexible_device_sensors, ) = scheduler._prepare(skip_validation=True) assert all(device_constraints[0]["derivative min"] == -expected_capacity) From cf566426cadd824e0922dbf02ee0fb6b7bacfdd7 Mon Sep 17 00:00:00 2001 From: Ahmad-Wahid Date: Mon, 2 Mar 2026 20:30:56 +0100 Subject: [PATCH 5/5] fix: unpack all available returns for all cases Signed-off-by: Ahmad-Wahid --- flexmeasures/data/models/planning/tests/test_solver.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flexmeasures/data/models/planning/tests/test_solver.py b/flexmeasures/data/models/planning/tests/test_solver.py index 233a44b417..ad279a63cf 100644 --- a/flexmeasures/data/models/planning/tests/test_solver.py +++ b/flexmeasures/data/models/planning/tests/test_solver.py @@ -259,6 +259,7 @@ def run_test_charge_discharge_sign( device_constraints, ems_constraints, commitments, + inflexible_device_sensors, ) = scheduler._prepare(skip_validation=True) planned_power_per_device, planned_costs, results, model = device_scheduler( @@ -1188,6 +1189,7 @@ def test_numerical_errors(app_with_each_solver, setup_planning_test_data, db): device_constraints, ems_constraints, commitments, + inflexible_device_sensors, ) = scheduler._prepare(skip_validation=True) _, _, results, model = device_scheduler(