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
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