Skip to content

Fix asset-level scheduling crash for a single-entry flex-model list without a top-level sensor - #2361

Draft
Flix6x wants to merge 2 commits into
mainfrom
fix/single-entry-nested-sensor-flex-model
Draft

Fix asset-level scheduling crash for a single-entry flex-model list without a top-level sensor#2361
Flix6x wants to merge 2 commits into
mainfrom
fix/single-entry-nested-sensor-flex-model

Conversation

@Flix6x

@Flix6x Flix6x commented Jul 27, 2026

Copy link
Copy Markdown
Member

Description

An asset-level scheduling trigger whose flex-model list contains a single device entry without a top-level "sensor" key — e.g. one battery entry shaped

[
  {
    "consumption": {"sensor": N},
    "state-of-charge": {"sensor": M},
    "soc-at-start": "0.2 MWh",
    "power-capacity": "1 MW"
  }
]

crashed before scheduling (at job creation, during deserialize_config) with:

flexmeasures/data/models/planning/storage.py:1742: in _deserialize_flex_model
    if self.sensor.generic_asset.asset_type.name in storage_asset_types:
E   AttributeError: 'NoneType' object has no attribute 'generic_asset'

The same device entry in a multi-entry list scheduled fine — which is why this went unnoticed, and why PR #2360's regression test is multi-device. Discovered during the work on #2360 and deliberately left out of its scope.

Root cause

collect_flex_config unwraps ("delistifies") any single-entry flex-model list whose entry lacks a top-level "sensor" key into a bare dict. A bare dict flex-model is deserialized downstream in single-sensor mode, which resolves the device against self.sensor — but for asset-level scheduling there is no sensor (self.sensor is None), so it crashed. Only multi-device (asset) mode knows how to resolve a device's power sensor from nested references such as {"consumption": {"sensor": N}} (via _resolve_power_sensor, since #2321).

The fix (smallest, least surprising change)

  • collect_flex_config now only reverts the flex-model listification when scheduling a specific sensor (self.sensor is not None). That unwrap exists to revert the listification this method itself applies to bare-dict (sensor-level) flex-models; for asset-level scheduling the flex-model stays a list, keeping multi-device mode, where nested sensor references already resolve correctly. The alternative — teaching single-sensor mode to resolve nested references — would have duplicated multi-device logic in a mode that is defined by resolving against one given sensor, and would have changed a lot more code.
  • Fail-safe (same spirit as Raise clear error when flex-model sensor is missing #2343): if single-sensor (dict) flex-model deserialization is ever reached without a sensor (e.g. through a direct Scheduler construction), MetaStorageScheduler._deserialize_flex_model now raises a clear ValueError naming the flex-model entry, instead of the AttributeError. The crash is now impossible through any code path.

Tests

  • test_scheduling_single_device_with_nested_output_sensor_only (mirrors Fix scheduling jobs failing on a timed_belief UniqueViolation for devices with nested-output-only power sensors #2360's regression test, but with a single-entry flex-model list): asserts the asset-level job schedules successfully and the nested consumption output sensor receives exactly one series of beliefs (96 quarter-hours). Verified to fail on main with the exact AttributeError above, and to pass with this fix.
  • test_collect_flex_config_keeps_single_entry_list_for_asset: unit test that asset-level scheduling keeps a single-entry list as a list, while sensor-level scheduling still unwraps to single-sensor (dict) mode.
  • test_deserialize_dict_flex_model_without_sensor_raises: unit test for the fail-safe ValueError.

Interaction with #2360 (not a dependency)

This PR is based on origin/main and does not overlap with #2360's diff (no merge conflicts expected; #2360 touches the scheduler's output assembly, this PR touches flex-config collection/deserialization). One behavioral interaction to be aware of: with this fix, a single nested-output-only device reaches the output assembly, where — without #2360 — its power sensor is still emitted both as a storage_schedule and as a consumption_schedule output. The regression test here stays green on plain main because its flat prices make both outputs identical (so save_to_db skips the duplicate as an unchanged belief), but non-trivial schedules for such devices still need #2360's fix to save. Merging both PRs (in either order) fully resolves the field scenario in which the crash was found.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WtuVTVfL4fQ9QSqbLXmAGD

Flix6x and others added 2 commits July 27, 2026 17:16
…duling

An asset-level scheduling trigger whose flex-model list contains a single
device entry without a top-level "sensor" key (e.g. a battery referencing
its power sensor only via a nested output reference, like
{"consumption": {"sensor": N}, "state-of-charge": {"sensor": M}})
crashed before scheduling with:

    AttributeError: 'NoneType' object has no attribute 'generic_asset'

collect_flex_config unwrapped any single-entry flex-model list without a
top-level "sensor" key into a bare dict, which downstream is deserialized
in single-sensor mode - resolving the device against self.sensor, which is
None for asset-level scheduling. The same entry in a multi-entry list
scheduled fine, because multi-device (asset) mode resolves nested output
sensor references (see _resolve_power_sensor).

Only unwrap when scheduling a specific sensor (self.sensor is not None);
for asset-level scheduling, keep the list form (multi-device mode) even
for a single device entry. As a fail-safe, single-sensor (dict) flex-model
deserialization without a sensor now raises a clear ValueError naming the
flex-model entry, so the AttributeError cannot recur through any code path.

Discovered during PR #2360 (whose regression test is multi-device for
exactly this reason).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WtuVTVfL4fQ9QSqbLXmAGD
Signed-off-by: F.N. Claessen <claessen@seita.nl>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WtuVTVfL4fQ9QSqbLXmAGD
Signed-off-by: F.N. Claessen <claessen@seita.nl>
@read-the-docs-community

Copy link
Copy Markdown

Documentation build overview

📚 flexmeasures | 🛠️ Build #33780455 | 📁 Comparing 8b569cf against latest (a3fb811)

  🔍 Preview build  

2 files changed
± changelog.html
± api/v3_0.html

Flix6x added a commit that referenced this pull request Jul 27, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WtuVTVfL4fQ9QSqbLXmAGD
Signed-off-by: F.N. Claessen <claessen@seita.nl>

Bugfixes
-----------
* Fix asset-level scheduling crashing with ``AttributeError: 'NoneType' object has no attribute 'generic_asset'`` when the flex-model list contains a single device entry without a top-level ``sensor`` key (e.g. a device referencing its power sensor only via a nested output reference, like ``{"consumption": {"sensor": ...}}``); such a flex-model now stays in multi-device mode, and a clear ``ValueError`` is raised if a device's power sensor genuinely cannot be resolved [see `PR #2361 <https://www.github.com/FlexMeasures/flexmeasures/pull/2361>`_]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this bug is unreleased. Append the PR reference to the changelog entry for the new consumption field instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant