Summary
set_attribute only updates an attribute that is already there. Setting one that does not exist yet silently does nothing:
# flexmeasures/data/models/generic_assets.py
def has_attribute(self, attribute: str) -> bool:
return attribute in self.attributes
def set_attribute(self, attribute: str, value):
if self.has_attribute(attribute):
self.attributes[attribute] = value
Sensor.set_attribute (flexmeasures/data/models/time_series.py:286) is identical. No exception, no log, no return value: a caller cannot tell the difference between a write that happened and one that did not.
Why this matters
It has already caught someone out. flexmeasures/data/services/scheduling.py:868 carries a workaround with the reason spelled out:
# Direct attribute assignment works for both new and existing attributes.
# set_attribute() is intentionally not used here because it silently
# no-ops when the attribute does not yet exist.
result_sensor.attributes["consumption_is_positive"] = intended
And the remaining production call sites look exposed to exactly that. StorageScheduler._set_soc_attributes (flexmeasures/data/models/planning/storage.py:1920) writes soc_datetime and soc_in_mwh through set_attribute, so on an asset that does not already carry those two attributes the write is dropped. That path is marked deprecated, but it is live until it goes.
It also makes a natural-looking line in a test or a script a no-op, which is how I ran into it: asset.set_attribute("timezone", "Europe/Amsterdam") followed by a commit leaves has_attribute("timezone") False.
Options
- Make it set.
self.attributes[attribute] = value unconditionally — what the name promises, and what the scheduling.py workaround does by hand. Then audit whether any caller was relying on the guard to avoid creating an attribute (none obviously is; the only non-test callers are the two in storage.py).
- Make it refuse. Raise on an unknown attribute, so a dropped write becomes loud. Safer for whatever relied on the current behaviour, but noisier, and the callers above would then need
attributes[...] = ... anyway.
- Leave it, rename it to something that says what it does (
update_existing_attribute), and give callers a separate way to create one.
Option 1 seems right: the guard reads as a leftover rather than a deliberate rule, nothing documents it, and the one place that noticed worked around it rather than depending on it.
Whichever way it goes, Sensor.set_attribute should change with it, and the scheduling.py workaround and its comment can then be dropped.
Acceptance criteria
- Setting an attribute that does not exist yet either takes effect or raises, with no third silent outcome.
StorageScheduler._set_soc_attributes records soc_datetime and soc_in_mwh on an asset that had neither.
GenericAsset and Sensor behave the same way.
Summary
set_attributeonly updates an attribute that is already there. Setting one that does not exist yet silently does nothing:Sensor.set_attribute(flexmeasures/data/models/time_series.py:286) is identical. No exception, no log, no return value: a caller cannot tell the difference between a write that happened and one that did not.Why this matters
It has already caught someone out.
flexmeasures/data/services/scheduling.py:868carries a workaround with the reason spelled out:And the remaining production call sites look exposed to exactly that.
StorageScheduler._set_soc_attributes(flexmeasures/data/models/planning/storage.py:1920) writessoc_datetimeandsoc_in_mwhthroughset_attribute, so on an asset that does not already carry those two attributes the write is dropped. That path is marked deprecated, but it is live until it goes.It also makes a natural-looking line in a test or a script a no-op, which is how I ran into it:
asset.set_attribute("timezone", "Europe/Amsterdam")followed by a commit leaveshas_attribute("timezone")False.Options
self.attributes[attribute] = valueunconditionally — what the name promises, and what thescheduling.pyworkaround does by hand. Then audit whether any caller was relying on the guard to avoid creating an attribute (none obviously is; the only non-test callers are the two instorage.py).attributes[...] = ...anyway.update_existing_attribute), and give callers a separate way to create one.Option 1 seems right: the guard reads as a leftover rather than a deliberate rule, nothing documents it, and the one place that noticed worked around it rather than depending on it.
Whichever way it goes,
Sensor.set_attributeshould change with it, and thescheduling.pyworkaround and its comment can then be dropped.Acceptance criteria
StorageScheduler._set_soc_attributesrecordssoc_datetimeandsoc_in_mwhon an asset that had neither.GenericAssetandSensorbehave the same way.