def test_charging_station_solver_day_2(target_soc, charging_station_name):
    """Starting with a state of charge 1 kWh, within 2 hours we should be able to reach
    any state of charge in the range [1, 5] kWh for a unidirectional station,
    or [0, 5] for a bidirectional station."""
    soc_at_start = 1
    duration_until_target = timedelta(hours=2)

    epex_da = Market.query.filter(Market.name == "epex_da").one_or_none()
    charging_station = Asset.query.filter(
        Asset.name == charging_station_name).one_or_none()
    start = as_server_time(datetime(2015, 1, 2))
    end = as_server_time(datetime(2015, 1, 3))
    resolution = timedelta(minutes=15)
    target_soc_datetime = start + duration_until_target
    soc_targets = pd.Series(np.nan,
                            index=pd.date_range(start,
                                                end,
                                                freq=resolution,
                                                closed="right"))
    soc_targets.loc[target_soc_datetime] = target_soc
    consumption_schedule = schedule_charging_station(charging_station, epex_da,
                                                     start, end, resolution,
                                                     soc_at_start, soc_targets)
    soc_schedule = integrate_time_series(consumption_schedule,
                                         soc_at_start,
                                         decimal_precision=6)

    # Check if constraints were met
    assert min(
        consumption_schedule.values) >= charging_station.capacity_in_mw * -1
    assert max(consumption_schedule.values) <= charging_station.capacity_in_mw
    print(consumption_schedule.head(12))
    print(soc_schedule.head(12))
    assert abs(soc_schedule.loc[target_soc_datetime] - target_soc) < 0.00001
def test_battery_solver_day_2():
    epex_da = Market.query.filter(Market.name == "epex_da").one_or_none()
    battery = Asset.query.filter(Asset.name == "Test battery").one_or_none()
    start = as_server_time(datetime(2015, 1, 2))
    end = as_server_time(datetime(2015, 1, 3))
    resolution = timedelta(minutes=15)
    soc_at_start = battery.soc_in_mwh
    schedule = schedule_battery(battery, epex_da, start, end, resolution,
                                soc_at_start)
    soc_schedule = integrate_time_series(schedule,
                                         soc_at_start,
                                         decimal_precision=6)

    with pd.option_context("display.max_rows", None, "display.max_columns", 3):
        print(soc_schedule)

    # Check if constraints were met
    assert min(schedule.values) >= battery.capacity_in_mw * -1
    assert max(schedule.values) <= battery.capacity_in_mw
    for soc in soc_schedule.values:
        assert soc >= battery.min_soc_in_mwh
        assert soc <= battery.max_soc_in_mwh

    # Check whether the resulting soc schedule follows our expectations for 8 expensive, 8 cheap and 8 expensive hours
    assert (soc_schedule.iloc[-1] == battery.min_soc_in_mwh
            )  # Battery sold out at the end of its planning horizon
    assert (soc_schedule.loc[start +
                             timedelta(hours=8)] == battery.min_soc_in_mwh
            )  # Sell what you begin with
    assert (soc_schedule.loc[start +
                             timedelta(hours=16)] == battery.max_soc_in_mwh
            )  # Buy what you can to sell later
Beispiel #3
0
def test_fallback_to_unsolvable_problem(target_soc, charging_station_name):
    """Starting with a state of charge 10 kWh, within 2 hours we should be able to reach
    any state of charge in the range [10, 14] kWh for a unidirectional station,
    or [6, 14] for a bidirectional station, given a charging capacity of 2 kW.
    Here we test target states of charge outside that range, ones that we should be able
    to get as close to as 1 kWh difference.
    We want our scheduler to handle unsolvable problems like these with a sensible fallback policy.
    """
    soc_at_start = 10
    duration_until_target = timedelta(hours=2)
    expected_gap = 1

    epex_da = Sensor.query.filter(Sensor.name == "epex_da").one_or_none()
    charging_station = Sensor.query.filter(
        Sensor.name == charging_station_name).one_or_none()
    assert charging_station.get_attribute("capacity_in_mw") == 2
    assert Sensor.query.get(
        charging_station.get_attribute("market_id")) == epex_da
    start = as_server_time(datetime(2015, 1, 2))
    end = as_server_time(datetime(2015, 1, 3))
    resolution = timedelta(minutes=15)
    target_soc_datetime = start + duration_until_target
    soc_targets = pd.Series(np.nan,
                            index=pd.date_range(start,
                                                end,
                                                freq=resolution,
                                                closed="right"))
    soc_targets.loc[target_soc_datetime] = target_soc
    consumption_schedule = schedule_charging_station(charging_station, start,
                                                     end, resolution,
                                                     soc_at_start, soc_targets)
    soc_schedule = integrate_time_series(consumption_schedule,
                                         soc_at_start,
                                         decimal_precision=6)

    # Check if constraints were met
    assert (min(consumption_schedule.values) >=
            charging_station.get_attribute("capacity_in_mw") * -1)
    assert max(consumption_schedule.values) <= charging_station.get_attribute(
        "capacity_in_mw")
    print(consumption_schedule.head(12))
    print(soc_schedule.head(12))
    assert (abs(
        abs(soc_schedule.loc[target_soc_datetime] - target_soc) - expected_gap)
            < TOLERANCE)
def test_battery_solver_day_1():
    epex_da = Market.query.filter(Market.name == "epex_da").one_or_none()
    battery = Asset.query.filter(Asset.name == "Test battery").one_or_none()
    start = as_server_time(datetime(2015, 1, 1))
    end = as_server_time(datetime(2015, 1, 2))
    resolution = timedelta(minutes=15)
    soc_at_start = battery.soc_in_mwh
    schedule = schedule_battery(battery, epex_da, start, end, resolution,
                                soc_at_start)
    soc_schedule = integrate_time_series(schedule,
                                         soc_at_start,
                                         decimal_precision=6)

    with pd.option_context("display.max_rows", None, "display.max_columns", 3):
        print(soc_schedule)

    # Check if constraints were met
    assert min(schedule.values) >= battery.capacity_in_mw * -1
    assert max(schedule.values) <= battery.capacity_in_mw
    for soc in soc_schedule.values:
        assert soc >= battery.min_soc_in_mwh
        assert soc <= battery.max_soc_in_mwh
Beispiel #5
0
def test_battery_solver_day_1(add_battery_assets):
    epex_da = Sensor.query.filter(Sensor.name == "epex_da").one_or_none()
    battery = Sensor.query.filter(Sensor.name == "Test battery").one_or_none()
    assert Sensor.query.get(battery.get_attribute("market_id")) == epex_da
    start = as_server_time(datetime(2015, 1, 1))
    end = as_server_time(datetime(2015, 1, 2))
    resolution = timedelta(minutes=15)
    soc_at_start = battery.get_attribute("soc_in_mwh")
    schedule = schedule_battery(battery, start, end, resolution, soc_at_start)
    soc_schedule = integrate_time_series(schedule,
                                         soc_at_start,
                                         decimal_precision=6)

    with pd.option_context("display.max_rows", None, "display.max_columns", 3):
        print(soc_schedule)

    # Check if constraints were met
    assert (min(schedule.values) >=
            battery.get_attribute("capacity_in_mw") * -1 - TOLERANCE)
    assert max(schedule.values) <= battery.get_attribute("capacity_in_mw")
    for soc in soc_schedule.values:
        assert soc >= battery.get_attribute("min_soc_in_mwh")
        assert soc <= battery.get_attribute("max_soc_in_mwh")
Beispiel #6
0
def test_battery_solver_day_2(add_battery_assets, roundtrip_efficiency: float):
    """Check battery scheduling results for day 2, which is set up with
    8 expensive, then 8 cheap, then again 8 expensive hours.
    If efficiency losses aren't too bad, we expect the scheduler to:
    - completely discharge within the first 8 hours
    - completely charge within the next 8 hours
    - completely discharge within the last 8 hours
    If efficiency losses are bad, the price difference is not worth cycling the battery,
    and so we expect the scheduler to only:
    - completely discharge within the last 8 hours
    """
    epex_da = Sensor.query.filter(Sensor.name == "epex_da").one_or_none()
    battery = Sensor.query.filter(Sensor.name == "Test battery").one_or_none()
    assert Sensor.query.get(battery.get_attribute("market_id")) == epex_da
    start = as_server_time(datetime(2015, 1, 2))
    end = as_server_time(datetime(2015, 1, 3))
    resolution = timedelta(minutes=15)
    soc_at_start = battery.get_attribute("soc_in_mwh")
    soc_min = 0.5
    soc_max = 4.5
    schedule = schedule_battery(
        battery,
        start,
        end,
        resolution,
        soc_at_start,
        soc_min=soc_min,
        soc_max=soc_max,
        roundtrip_efficiency=roundtrip_efficiency,
    )
    soc_schedule = integrate_time_series(schedule,
                                         soc_at_start,
                                         decimal_precision=6)

    with pd.option_context("display.max_rows", None, "display.max_columns", 3):
        print(soc_schedule)

    # Check if constraints were met
    assert min(schedule.values) >= battery.get_attribute("capacity_in_mw") * -1
    assert max(
        schedule.values) <= battery.get_attribute("capacity_in_mw") + TOLERANCE
    for soc in soc_schedule.values:
        assert soc >= max(soc_min, battery.get_attribute("min_soc_in_mwh"))
        assert soc <= battery.get_attribute("max_soc_in_mwh")

    # Check whether the resulting soc schedule follows our expectations for 8 expensive, 8 cheap and 8 expensive hours
    assert soc_schedule.iloc[-1] == max(
        soc_min, battery.get_attribute("min_soc_in_mwh")
    )  # Battery sold out at the end of its planning horizon

    # As long as the roundtrip efficiency isn't too bad (I haven't computed the actual switch point)
    if roundtrip_efficiency > 0.9:
        assert soc_schedule.loc[start + timedelta(hours=8)] == max(
            soc_min, battery.get_attribute(
                "min_soc_in_mwh"))  # Sell what you begin with
        assert soc_schedule.loc[start + timedelta(hours=16)] == min(
            soc_max, battery.get_attribute(
                "max_soc_in_mwh"))  # Buy what you can to sell later
    else:
        # If the roundtrip efficiency is poor, best to stand idle
        assert soc_schedule.loc[start + timedelta(
            hours=8)] == battery.get_attribute("soc_in_mwh")
        assert soc_schedule.loc[start + timedelta(
            hours=16)] == battery.get_attribute("soc_in_mwh")
def test_trigger_and_get_schedule(
    app,
    add_market_prices,
    add_battery_assets,
    battery_soc_sensor,
    add_charging_station_assets,
    message,
    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"] = 25
    with app.test_client() as client:
        sensor = Sensor.query.filter(Sensor.name == asset_name).one_or_none()
        message[
            "soc-sensor"] = f"ea1.2018-06.localhost:fm1.{battery_soc_sensor.id}"
        auth_token = get_auth_token(client, "*****@*****.**",
                                    "testtest")
        trigger_schedule_response = client.post(
            url_for("SensorAPI:trigger_schedule", id=sensor.id),
            json=message,
            headers={"Authorization": auth_token},
        )
        print("Server responded with:\n%s" % trigger_schedule_response.json)
        assert trigger_schedule_response.status_code == 200
        job_id = trigger_schedule_response.json["schedule"]

    # look for scheduling jobs in queue
    assert (len(app.queues["scheduling"]) == 1
            )  # only 1 schedule should be made for 1 asset
    job = app.queues["scheduling"].jobs[0]
    assert job.kwargs["sensor_id"] == sensor.id
    assert job.kwargs["start"] == parse_datetime(message["start"])
    assert job.id == job_id

    # process the scheduling queue
    work_on_rq(app.queues["scheduling"],
               exc_handler=handle_scheduling_exception)
    assert (Job.fetch(
        job_id, connection=app.queues["scheduling"].connection).is_finished is
            True)

    # check results are in the database
    resolution = timedelta(minutes=15)
    scheduler_source = DataSource.query.filter_by(
        name="Seita", type="scheduling script").one_or_none()
    assert (scheduler_source
            is not None)  # Make sure the scheduler data source is now there
    power_values = (TimedBelief.query.filter(
        TimedBelief.sensor_id == sensor.id).filter(
            TimedBelief.source_id == scheduler_source.id).all())
    consumption_schedule = pd.Series(
        [-v.event_value for v in power_values],
        index=pd.DatetimeIndex([v.event_start for v in power_values],
                               freq=resolution),
    )  # For consumption schedules, positive values denote consumption. For the db, consumption is negative
    assert (len(consumption_schedule) ==
            app.config.get("FLEXMEASURES_PLANNING_HORIZON") / resolution)

    # check targets, if applicable
    if "targets" in message:
        start_soc = message["soc-at-start"] / 1000  # in MWh
        soc_schedule = integrate_time_series(consumption_schedule, start_soc,
                                             6)
        print(consumption_schedule)
        print(soc_schedule)
        for target in message["targets"]:
            assert soc_schedule[
                target["datetime"]] == target["soc-target"] / 1000

    # try to retrieve the schedule through the /sensors/<id>/schedules/<job_id> [GET] api endpoint
    get_schedule_message = message_for_get_device_message(
        targets="soc-targets" in message)
    del get_schedule_message["type"]
    auth_token = get_auth_token(client, "*****@*****.**",
                                "testtest")
    get_schedule_response = client.get(
        url_for("SensorAPI:get_schedule", id=sensor.id, uuid=job_id),
        query_string=get_schedule_message,
        headers={
            "content-type": "application/json",
            "Authorization": auth_token
        },
    )
    print("Server responded with:\n%s" % get_schedule_response.json)
    assert get_schedule_response.status_code == 200
    # assert get_schedule_response.json["type"] == "GetDeviceMessageResponse"
    assert len(get_schedule_response.json["values"]) == 192

    # Test that a shorter planning horizon yields the same result for the shorter planning horizon
    get_schedule_message["duration"] = "PT6H"
    get_schedule_response_short = client.get(
        url_for("SensorAPI:get_schedule", id=sensor.id, uuid=job_id),
        query_string=get_schedule_message,
        headers={
            "content-type": "application/json",
            "Authorization": auth_token
        },
    )
    assert (get_schedule_response_short.json["values"] ==
            get_schedule_response.json["values"][0:24])

    # Test that a much longer planning horizon yields the same result (when there are only 2 days of prices)
    get_schedule_message["duration"] = "PT1000H"
    get_schedule_response_long = client.get(
        url_for("SensorAPI:get_schedule", id=sensor.id, uuid=job_id),
        query_string=get_schedule_message,
        headers={
            "content-type": "application/json",
            "Authorization": auth_token
        },
    )
    assert (get_schedule_response_long.json["values"][0:192] ==
            get_schedule_response.json["values"])
Beispiel #8
0
def test_post_udi_event_and_get_device_message(app, message, asset_name):
    auth_token = None
    with app.test_client() as client:
        asset = Asset.query.filter(Asset.name == asset_name).one_or_none()
        asset_id = asset.id
        asset_owner_id = asset.owner_id
        message["event"] = message["event"] % (asset.owner_id, asset.id)
        auth_token = get_auth_token(client, "*****@*****.**", "testtest")
        post_udi_event_response = client.post(
            url_for("flexmeasures_api_v1_3.post_udi_event"),
            json=message,
            headers={"Authorization": auth_token},
        )
        print("Server responded with:\n%s" % post_udi_event_response.json)
        assert post_udi_event_response.status_code == 200
        assert post_udi_event_response.json["type"] == "PostUdiEventResponse"

    # test asset state in database
    msg_dt = parse_datetime(message["datetime"])
    asset = Asset.query.filter(Asset.name == asset_name).one_or_none()
    assert asset.soc_datetime == msg_dt
    assert asset.soc_in_mwh == message["value"] / 1000
    assert asset.soc_udi_event_id == 204

    # look for scheduling jobs in queue
    assert (
        len(app.queues["scheduling"]) == 1
    )  # only 1 schedule should be made for 1 asset
    job = app.queues["scheduling"].jobs[0]
    assert job.kwargs["asset_id"] == asset_id
    assert job.kwargs["start"] == parse_datetime(message["datetime"])
    assert job.id == message["event"]

    # process the scheduling queue
    work_on_rq(app.queues["scheduling"], exc_handler=handle_scheduling_exception)
    assert (
        Job.fetch(
            message["event"], connection=app.queues["scheduling"].connection
        ).is_finished
        is True
    )

    # check results are in the database
    resolution = timedelta(minutes=15)
    scheduler_source = DataSource.query.filter_by(
        name="Seita", type="scheduling script"
    ).one_or_none()
    assert (
        scheduler_source is not None
    )  # Make sure the scheduler data source is now there
    power_values = (
        Power.query.filter(Power.asset_id == asset_id)
        .filter(Power.data_source_id == scheduler_source.id)
        .all()
    )
    consumption_schedule = pd.Series(
        [-v.value for v in power_values],
        index=pd.DatetimeIndex([v.datetime for v in power_values], freq=resolution),
    )  # For consumption schedules, positive values denote consumption. For the db, consumption is negative
    assert (
        len(consumption_schedule)
        == app.config.get("FLEXMEASURES_PLANNING_HORIZON") / resolution
    )

    # check targets, if applicable
    if "targets" in message:
        start_soc = message["value"] / 1000  # in MWh
        soc_schedule = integrate_time_series(consumption_schedule, start_soc, 6)
        print(consumption_schedule)
        print(soc_schedule)
        for target in message["targets"]:
            assert soc_schedule[target["datetime"]] == target["value"] / 1000

    # try to retrieve the schedule through the getDeviceMessage api endpoint
    get_device_message = message_for_get_device_message()
    get_device_message["event"] = get_device_message["event"] % (
        asset_owner_id,
        asset_id,
    )
    auth_token = get_auth_token(client, "*****@*****.**", "testtest")
    get_device_message_response = client.get(
        url_for("flexmeasures_api_v1_3.get_device_message"),
        query_string=get_device_message,
        headers={"content-type": "application/json", "Authorization": auth_token},
    )
    print("Server responded with:\n%s" % get_device_message_response.json)
    assert get_device_message_response.status_code == 200
    assert get_device_message_response.json["type"] == "GetDeviceMessageResponse"
    assert len(get_device_message_response.json["values"]) == 192

    # Test that a shorter planning horizon yields the same result for the shorter planning horizon
    get_device_message["duration"] = "PT6H"
    get_device_message_response_short = client.get(
        url_for("flexmeasures_api_v1_3.get_device_message"),
        query_string=get_device_message,
        headers={"content-type": "application/json", "Authorization": auth_token},
    )
    assert (
        get_device_message_response_short.json["values"]
        == get_device_message_response.json["values"][0:24]
    )

    # Test that a much longer planning horizon yields the same result (when there are only 2 days of prices)
    get_device_message["duration"] = "PT1000H"
    get_device_message_response_long = client.get(
        url_for("flexmeasures_api_v1_3.get_device_message"),
        query_string=get_device_message,
        headers={"content-type": "application/json", "Authorization": auth_token},
    )
    assert (
        get_device_message_response_long.json["values"][0:192]
        == get_device_message_response.json["values"]
    )

    # sending again results in an error, unless we increase the event ID
    with app.test_client() as client:
        next_msg_dt = msg_dt + timedelta(minutes=5)
        message["datetime"] = next_msg_dt.strftime("%Y-%m-%dT%H:%M:%S.%f%z")
        post_udi_event_response = client.post(
            url_for("flexmeasures_api_v1_3.post_udi_event"),
            json=message,
            headers={"Authorization": auth_token},
        )
        print("Server responded with:\n%s" % post_udi_event_response.json)
        assert post_udi_event_response.status_code == 400
        assert post_udi_event_response.json["type"] == "PostUdiEventResponse"
        assert post_udi_event_response.json["status"] == "OUTDATED_UDI_EVENT"

        message["event"] = message["event"].replace("204", "205")
        post_udi_event_response = client.post(
            url_for("flexmeasures_api_v1_3.post_udi_event"),
            json=message,
            headers={"Authorization": auth_token},
        )
        print("Server responded with:\n%s" % post_udi_event_response.json)
        assert post_udi_event_response.status_code == 200
        assert post_udi_event_response.json["type"] == "PostUdiEventResponse"

    # test database state
    asset = Asset.query.filter(Asset.name == asset_name).one_or_none()
    assert asset.soc_datetime == next_msg_dt
    assert asset.soc_in_mwh == message["value"] / 1000
    assert asset.soc_udi_event_id == 205

    # process the scheduling queue
    work_on_rq(app.queues["scheduling"], exc_handler=handle_scheduling_exception)
    # the job still fails due to missing prices for the last time slot, but we did test that the api and worker now processed the UDI event and attempted to create a schedule
    assert (
        Job.fetch(
            message["event"], connection=app.queues["scheduling"].connection
        ).is_failed
        is True
    )