Skip to content

StorageScheduler crashes on multi-asset tree with sensor-less flex_model entries #2084

@saerts-gp

Description

@saerts-gp

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

  1. 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.
  2. Scheduler.collect_flex_config() merges user-provided overrides with all database entries, producing a combined flex_model list.
  3. _prepare() builds the sensors list from flex_model_d.get("sensor") for every entry. Entries without a sensor key yield None.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions