Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move over deprecated soc-targets field #566

Merged
merged 10 commits into from Dec 30, 2022
3 changes: 2 additions & 1 deletion documentation/changelog.rst
@@ -1,3 +1,4 @@

**********************
FlexMeasures Changelog
**********************
Expand Down Expand Up @@ -40,7 +41,7 @@ Infrastructure / Support
* Remove bokeh dependency and obsolete UI views [see `PR #476 <http://www.github.com/FlexMeasures/flexmeasures/pull/476>`_]
* Fix ``flexmeasures db-ops dump`` and ``flexmeasures db-ops restore`` not working in docker containers [see `PR #530 <http://www.github.com/FlexMeasures/flexmeasures/pull/530>`_] and incorrectly reporting a success when `pg_dump` and `pg_restore` are not installed [see `PR #526 <http://www.github.com/FlexMeasures/flexmeasures/pull/526>`_]
* Plugins can save BeliefsSeries, too, instead of just BeliefsDataFrames [see `PR #523 <http://www.github.com/FlexMeasures/flexmeasures/pull/523>`_]
* Improve documentation and code w.r.t. storage flexibility modelling ― prepare for handling other schedulers & merge battery and car charging schedulers [see `PR #511 <http://www.github.com/FlexMeasures/flexmeasures/pull/511>`_ and `PR #537 <http://www.github.com/FlexMeasures/flexmeasures/pull/537>`_]
* Improve documentation and code w.r.t. storage flexibility modelling ― prepare for handling other schedulers & merge battery and car charging schedulers [see `PR #511 <http://www.github.com/FlexMeasures/flexmeasures/pull/511>`_, `PR #537 <http://www.github.com/FlexMeasures/flexmeasures/pull/537>`_ and `PR #566 <http://www.github.com/FlexMeasures/flexmeasures/pull/566>`_]
* Revised strategy for removing unchanged beliefs when saving data: retain the oldest measurement (ex-post belief), too [see `PR #518 <http://www.github.com/FlexMeasures/flexmeasures/pull/518>`_]
* Scheduling test for maximizing self-consumption, and improved time series db queries for fixed tariffs (and other long-term constants) [see `PR #532 <http://www.github.com/FlexMeasures/flexmeasures/pull/532>`_]
* Clean up table formatting for ``flexmeasures show`` CLI commands [see `PR #540 <http://www.github.com/FlexMeasures/flexmeasures/pull/540>`_]
Expand Down
6 changes: 2 additions & 4 deletions flexmeasures/api/v3_0/sensors.py
Expand Up @@ -36,7 +36,6 @@
from flexmeasures.data.schemas.sensors import SensorSchema, SensorIdField
from flexmeasures.data.schemas.times import AwareDateTimeField
from flexmeasures.data.schemas.units import QuantityField
from flexmeasures.data.schemas.scheduling.storage import SOCTargetSchema
from flexmeasures.data.schemas.scheduling import FlexContextSchema
from flexmeasures.data.services.sensors import get_sensors
from flexmeasures.data.services.scheduling import (
Expand Down Expand Up @@ -240,9 +239,7 @@ def get_data(self, response: dict):
]
),
), # todo: allow unit to be set per field, using QuantityField("%", validate=validate.Range(min=0, max=1))
"targets": fields.List(
fields.Nested(SOCTargetSchema), data_key="soc-targets"
),
"targets": fields.List(fields.Dict, data_key="soc-targets"),
"prefer_charging_sooner": fields.Bool(
data_key="prefer-charging-sooner", required=False
),
Expand Down Expand Up @@ -401,6 +398,7 @@ def trigger_schedule( # noqa: C901
(soc_min, "soc-min"),
(soc_max, "soc-max"),
(unit, "soc-unit"),
(kwargs.get("targets"), "soc-targets"),
(roundtrip_efficiency, "roundtrip-efficiency"),
(
prefer_charging_sooner,
Expand Down
33 changes: 22 additions & 11 deletions flexmeasures/api/v3_0/tests/test_sensor_schedules.py
Expand Up @@ -89,9 +89,6 @@ def test_trigger_and_get_schedule(
asset_name,
):
# trigger a schedule through the /sensors/<id>/schedules/trigger [POST] api endpoint
message["roundtrip-efficiency"] = 0.98
message["soc-min"] = 0
message["soc-max"] = 4
assert len(app.queues["scheduling"]) == 0

sensor = Sensor.query.filter(Sensor.name == asset_name).one_or_none()
Expand All @@ -106,11 +103,12 @@ def test_trigger_and_get_schedule(
headers={"Authorization": auth_token},
)
print("Server responded with:\n%s" % trigger_schedule_response.json)
check_deprecation(trigger_schedule_response)
if "flex-model" not in message:
check_deprecation(trigger_schedule_response)
assert (
"soc-min" in trigger_schedule_response.json["message"]
) # deprecation warning
assert trigger_schedule_response.status_code == 200
assert (
"soc-min" in trigger_schedule_response.json["message"]
) # deprecation warning
job_id = trigger_schedule_response.json["schedule"]

# look for scheduling jobs in queue
Expand Down Expand Up @@ -152,18 +150,28 @@ def test_trigger_and_get_schedule(
== app.config.get("FLEXMEASURES_PLANNING_HORIZON") / resolution
)

# check targets, if applicable
if "targets" in message:
if "flex-model" not in message:
start_soc = message["soc-at-start"] / 1000 # in MWh
roundtrip_efficiency = message["roundtrip-efficiency"]
soc_targets = message.get("soc-targets")
else:
start_soc = message["flex-model"]["soc-at-start"] / 1000 # in MWh
roundtrip_efficiency = message["flex-model"]["roundtrip-efficiency"]
soc_targets = message["flex-model"].get("soc-targets")

# check targets, if applicable
if soc_targets:
soc_schedule = integrate_time_series(
consumption_schedule,
start_soc,
up_efficiency=roundtrip_efficiency**0.5,
down_efficiency=roundtrip_efficiency**0.5,
decimal_precision=6,
)
print(consumption_schedule)
print(soc_schedule)
for target in message["targets"]:
assert soc_schedule[target["datetime"]] == target["soc-target"] / 1000
for target in soc_targets:
assert soc_schedule[target["datetime"]] == target["value"] / 1000

# try to retrieve the schedule through the /sensors/<id>/schedules/<job_id> [GET] api endpoint
get_schedule_message = message_for_get_device_message(
Expand Down Expand Up @@ -204,3 +212,6 @@ def test_trigger_and_get_schedule(
get_schedule_response_long.json["values"][0:192]
== get_schedule_response.json["values"]
)

# Check whether the soc-at-start was persisted as an asset attribute
assert sensor.generic_asset.get_attribute("soc_in_mwh") == start_soc
20 changes: 13 additions & 7 deletions flexmeasures/api/v3_0/tests/utils.py
Expand Up @@ -54,18 +54,24 @@ def message_for_trigger_schedule(
"start"
] = "2040-01-01T00:00:00+01:00" # We have no beliefs in our test database about 2040 prices

flex_model = {
"soc-at-start": 12.1, # in kWh, according to soc-unit
"soc-min": 0, # in kWh, according to soc-unit
"soc-max": 40, # in kWh, according to soc-unit
"soc-unit": "kWh",
"roundtrip-efficiency": 0.98,
}
if deprecated_format_pre012:
message["soc-at-start"] = 12.1
message["soc-unit"] = "kWh"
# unpack flex model directly as message fields
message = dict(**message, **flex_model)
else:
message["flex-model"] = {}
message["flex-model"]["soc-at-start"] = 12.1
message["flex-model"]["soc-unit"] = "kWh"
message["flex-model"] = flex_model
if with_targets:
if realistic_targets:
targets = [{"value": 3500, "datetime": "2015-01-02T23:00:00+01:00"}]
# this target (in kWh, according to soc-unit) is well below the soc_max_in_mwh on the battery's sensor attributes
targets = [{"value": 25, "datetime": "2015-01-02T23:00:00+01:00"}]
else:
# this target is actually higher than soc_max_in_mwh on the battery's sensor attributes
# this target (in kWh, according to soc-unit) is actually higher than soc_max_in_mwh on the battery's sensor attributes
targets = [{"value": 25000, "datetime": "2015-01-02T23:00:00+01:00"}]
if deprecated_format_pre012:
message["soc-targets"] = targets
Expand Down
7 changes: 5 additions & 2 deletions flexmeasures/data/models/planning/storage.py
Expand Up @@ -188,14 +188,17 @@ def compute_schedule(
def persist_flex_model(self):
"""Store new soc info as GenericAsset attributes"""
self.sensor.generic_asset.set_attribute("soc_datetime", self.start.isoformat())
if self.flex_model.get("soc_unit") == "kWh":
soc_unit = self.flex_model.get("soc_unit")
if soc_unit == "kWh":
self.sensor.generic_asset.set_attribute(
"soc_in_mwh", self.flex_model["soc_at_start"] / 1000
)
else:
elif soc_unit == "MWh":
self.sensor.generic_asset.set_attribute(
"soc_in_mwh", self.flex_model["soc_at_start"]
)
else:
raise NotImplementedError(f"Unsupported SoC unit '{soc_unit}'.")

def deserialize_flex_config(self):
"""
Expand Down
2 changes: 1 addition & 1 deletion flexmeasures/data/schemas/scheduling/storage.py
Expand Up @@ -56,7 +56,7 @@ def post_load_sequence(self, data: dict, **kwargs) -> dict:
if data.get("soc_targets"):
for target in data["soc_targets"]:
target["value"] /= 1000.0
data["soc_unit"] == "MWh"
data["soc_unit"] = "MWh"

# Convert round-trip efficiency to dimensionless (to the (0,1] range)
if data.get("roundtrip_efficiency") is not None:
Expand Down