-
Notifications
You must be signed in to change notification settings - Fork 48
StorageScheduler crashes on multi-asset tree with sensor-less flex_model entries #2084
Description
StorageScheduler._prepare() crashes on multi-asset tree with sensor-less flex_model entries
Summary
StorageScheduler._prepare() raises AttributeError: 'NoneType' object has no attribute 'event_resolution' when scheduling a site whose asset tree includes non-flexible assets that have a flex_model (e.g. power-capacity only) but no sensor key.
FlexMeasures version: 0.31.0
Error
File "flexmeasures/data/models/planning/storage.py", line 1318, in compute
) = self._prepare(skip_validation=skip_validation)
File "flexmeasures/data/models/planning/storage.py", line 904, in _prepare
if sensor_d.event_resolution != timedelta(0):
^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'event_resolution'
Root Cause
MetaStorageScheduler._prepare() (line 904) accesses sensor_d.event_resolution without checking for None. The same method correctly guards against None at lines 674 and 742:
# Lines 674, 742 — correct ✅
if sensor_d is not None and sensor_d.get_attribute(...):
# Line 904 — missing None guard ❌
if sensor_d.event_resolution != timedelta(0):
device_constraints[d]["efficiency"] **= (
resolution / sensor_d.event_resolution
)How sensor_d becomes None
GenericAsset.get_flex_model()recursively collectsflex_modelfrom all descendants in the asset tree — including non-flexible assets that havepower-capacitybut nosensor.Scheduler.collect_flex_config()merges user-provided overrides with all database entries, producing a combined flex_model list._prepare()builds thesensorslist fromflex_model_d.get("sensor")for every entry. Entries without asensorkey yieldNone.- The loop
for d in range(num_flexible_devices)iterates over all entries, andsensor_d = sensors[d]isNonefor non-sensor entries.
Suggested Fix
Add a None guard at line 904 (same pattern as lines 674/742):
- if sensor_d.event_resolution != timedelta(0):
+ if sensor_d is not None and sensor_d.event_resolution != timedelta(0):
device_constraints[d]["efficiency"] **= (
resolution / sensor_d.event_resolution
)Reproduction
Call StorageScheduler.compute() on a site asset that has child assets with flex_model containing only power-capacity (no sensor). This is a normal configuration — non-flexible assets need power-capacity for safe scheduling constraints.
from flexmeasures.data.models.planning.storage import StorageScheduler
from datetime import datetime, timedelta, timezone
scheduler = StorageScheduler(
asset_or_sensor=site_asset, # GenericAsset with multi-level children
start=datetime.now(timezone.utc),
end=datetime.now(timezone.utc) + timedelta(hours=10),
resolution=timedelta(minutes=15),
return_multiple=True,
flex_model=[{"asset": battery_asset_id, "soc-at-start": "2.5 kWh"}],
)
schedule = scheduler.compute() # 💥 AttributeErrorExample Asset Tree
The issue occurs on any site with a mix of sensor-bearing and sensor-less assets. For example:
Site ─ SITE
├── PCC ─ POINT_OF_COMMON_COUPLING
│ ├── Grid ─ GRID
│ └── Distribution Board ─ DISTRIBUTION_BOARD
│ ├── Inverter ─ INVERTER
│ │ ├── PV Array ─ PV ✅ has sensor
│ │ ├── PV Array 2 ─ PV ✅ has sensor
│ │ └── Battery ─ BATTERY ✅ has sensor
│ └── Other assets without sensors
3 assets have sensor in flex_model (Battery, PV Array, PV Array 2) — these are the "flexible devices" being scheduled.
6+ assets have flex_model without sensor (PCC, Grid, Inverter, Distribution Board, etc.) — these only provide power-capacity constraints needed for safe scheduling, but are not themselves scheduled.
flex_model examples
Assets with sensor (scheduled — works fine):
{"sensor": 20, "soc-max": "5.0 kWh", "soc-min": "1.0 kWh", "power-capacity": "5.0 kW", "state-of-charge": {"sensor": 18}}Assets without sensor (not scheduled — triggers crash):
{"power-capacity": "22.0 kW"}All entries end up in the same flex_model list via collect_flex_config(). The _prepare() loop iterates over all of them, and crashes on any entry without a sensor at line 904.