Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion documentation/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ New features
* Support forecasting from a given time in the past, by allowing to specify a ``prior`` belief time in the forecasting API endpoint (as already possible with CLI command) [see `PR #1978 <https://www.github.com/FlexMeasures/flexmeasures/pull/1978>`_]
* UI support for editing JSON attributes on sensors, assets and accounts [see `PR #2093 <https://www.github.com/FlexMeasures/flexmeasures/pull/2093>`_]
* Show sensor attributes on sensor page, if not empty [see `PR #2015 <https://www.github.com/FlexMeasures/flexmeasures/pull/2015>`_]
* Separate the ``StorageScheduler``'s tie-breaking preference for a full :abbr:`SoC (state of charge)` from its reported energy costs [see `PR #2023 <https://www.github.com/FlexMeasures/flexmeasures/pull/2023>`_]
* Separate the ``StorageScheduler``'s tie-breaking preference for a full :abbr:`SoC (state of charge)` from its reported energy costs [see `PR #2023 <https://www.github.com/FlexMeasures/flexmeasures/pull/2023>`_ and `PR #2108 <https://www.github.com/FlexMeasures/flexmeasures/pull/2108>`_]
* Improve asset graph hover interaction with a vertical ruler across subcharts, while keeping hover dots for easier visual tracking [see `PR #2079 <https://www.github.com/FlexMeasures/flexmeasures/pull/2079>`_]
* Improve asset audit log messages for JSON field edits (especially ``sensors_to_show`` and nested flex-config values) [see `PR #2055 <https://www.github.com/FlexMeasures/flexmeasures/pull/2055>`_]
* Added a form on the UI for deleting sensor data sources [see `PR #2095 <https://www.github.com/FlexMeasures/flexmeasures/pull/2095>`_]
Expand Down
8 changes: 7 additions & 1 deletion flexmeasures/data/models/planning/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,13 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901
for d, (prefer_charging_sooner_d, prefer_curtailing_later_d) in enumerate(
zip(prefer_charging_sooner, prefer_curtailing_later)
):
if prefer_charging_sooner_d:
# Mixed-device schedules can include non-storage devices such as PV.
# These do not have a state of charge, so there is nothing to "prefer full".
if (
prefer_charging_sooner_d
and soc_max[d] is not None
and soc_at_start[d] is not None
):
tiny_price_slope = (
add_tiny_price_slope(
up_deviation_prices, "event_value", order="desc"
Expand Down
3 changes: 1 addition & 2 deletions flexmeasures/data/models/planning/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,7 @@ def building(db, setup_accounts, setup_markets) -> GenericAsset:
select(GenericAssetType).filter_by(name="building")
).scalar_one_or_none()
if not building_type:
# create_test_battery_assets might have created it already
building_type = GenericAssetType(name="battery")
building_type = GenericAssetType(name="building")
db.session.add(building_type)
building = GenericAsset(
name="building",
Expand Down
68 changes: 68 additions & 0 deletions flexmeasures/data/models/planning/tests/test_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -2891,6 +2891,74 @@ def initialize_combined_commitments(num_devices: int):
), "Individual costs mismatch: Costs for one or more devices are not calculated as expected."


def test_prefer_full_storage_skips_non_storage_devices(db, building):
"""Do not apply SoC-based storage preferences to non-storage devices such as PV."""

battery = Sensor(
name="mixed battery power sensor",
generic_asset=building,
event_resolution=timedelta(hours=1),
unit="MW",
)
pv = Sensor(
name="mixed pv power sensor",
generic_asset=building,
event_resolution=timedelta(hours=1),
unit="MW",
attributes={"is_strictly_non_positive": True},
)
db.session.add_all([battery, pv])
db.session.commit()

start = pd.Timestamp("2020-01-01T00:00:00", tz="Europe/Amsterdam")
end = start + timedelta(hours=4)
resolution = timedelta(hours=1)

scheduler = StorageScheduler(
asset_or_sensor=building,
start=start,
end=end,
resolution=resolution,
flex_model=[
{
"sensor": battery,
"soc_at_start": 1.0,
"soc_min": 0.0,
"soc_max": 2.0,
"power_capacity_in_mw": ur.Quantity("1 MW"),
"consumption_capacity": ur.Quantity("1 MW"),
"production_capacity": ur.Quantity("1 MW"),
"prefer_charging_sooner": True,
"prefer_curtailing_later": True,
},
{
"sensor": pv,
"power_capacity_in_mw": ur.Quantity("1 MW"),
"consumption_capacity": ur.Quantity("0 MW"),
"production_capacity": ur.Quantity("1 MW"),
"prefer_charging_sooner": True,
"prefer_curtailing_later": True,
},
],
flex_context={
"consumption_price": ur.Quantity("100 EUR/MWh"),
"production_price": ur.Quantity("100 EUR/MWh"),
"shared_currency_unit": "EUR",
"ems_power_capacity_in_mw": ur.Quantity("2 MW"),
},
return_multiple=True,
)
scheduler.config_deserialized = True

schedule = scheduler.compute()

assert isinstance(schedule, list)
assert any(
result.get("name") == "storage_schedule" and result.get("sensor") == battery
for result in schedule
)


def test_multiple_devices_sequential_scheduler():
start = pd.Timestamp("2023-01-01T00:00:00")
end = pd.Timestamp("2023-01-02T00:00:00")
Expand Down
Loading