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 collects flex_model from all descendants in the asset tree — including non-flexible assets that have power-capacity but no sensor.
Scheduler.collect_flex_config() merges user-provided overrides with all database entries, producing a combined flex_model list.
_prepare() builds the sensors list from flex_model_d.get("sensor") for every entry. Entries without a sensor key yield None.
- The loop
for d in range(num_flexible_devices) iterates over all entries, and sensor_d = sensors[d] is None for 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() # 💥 AttributeError
Example 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.
StorageScheduler._prepare()crashes on multi-asset tree with sensor-less flex_model entriesSummary
StorageScheduler._prepare()raisesAttributeError: 'NoneType' object has no attribute 'event_resolution'when scheduling a site whose asset tree includes non-flexible assets that have aflex_model(e.g.power-capacityonly) but nosensorkey.FlexMeasures version: 0.31.0
Error
Root Cause
MetaStorageScheduler._prepare()(line 904) accessessensor_d.event_resolutionwithout checking forNone. The same method correctly guards againstNoneat lines 674 and 742:How
sensor_dbecomesNoneGenericAsset.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.for d in range(num_flexible_devices)iterates over all entries, andsensor_d = sensors[d]isNonefor non-sensor entries.Suggested Fix
Add a
Noneguard at line 904 (same pattern as lines 674/742):Reproduction
Call
StorageScheduler.compute()on a site asset that has child assets withflex_modelcontaining onlypower-capacity(nosensor). This is a normal configuration — non-flexible assets needpower-capacityfor safe scheduling constraints.Example Asset Tree
The issue occurs on any site with a mix of sensor-bearing and sensor-less assets. For example:
3 assets have
sensorin 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 providepower-capacityconstraints 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 asensorat line 904.