Load management of a single EV

In this tutorial we will optimize the loading of an EV.

The tutorial is set up in 5 different steps

  • Step 1: Plugged EV as load

  • Step 2: Unidircetional charging

  • Step 3: Free charging with PV system at work

  • Step 4: Fix free charging artefact and allow bidirectional use of the battery

  • Step 5: Variable electricity prices

Each section contains a step by step explanation of how the a management of an ev loading can be done is using oemof.solph. Additionally, the repository contains a fully functional python file of all five main steps for you to execute yourself or modify and play around with.

Step 1: Plugged EV as load

Within the first step we want to simulate a plugged EV as load with pre-calculated charging time series Charged EV with predefined trips for load. First of all, we create some input data. We use Pandas to do so and will also import matplotlib to plot the data. Further for plotting we use a helper function from helpers.py.


import matplotlib.pyplot as plt
import networkx as nx
import numpy as np
import pandas as pd
from helpers import plot_results
from oemof.network.graph import create_nx_graph

Now we can begin the modeling process of ev loading management. Every oemof.solph model starts be creating (also called “initializing”) the energy system. To create an instance of the solph.EnergySystem class, we have to import the solph package at first. Further we need a timeindex for the simulation.Within this example we will regard the first day of the year of 2025. We will use the date_range() function from the pandas package to create a timeindex with 5 minute resolution for one day.

import oemof.solph as solph

time_index = pd.date_range(
    start="2025-01-01",
    end="2025-01-02",
    freq="5min",
    inclusive="both",
)

ev_energy_system = solph.EnergySystem(
    timeindex=time_index,
    infer_last_interval=False,
)

After that, we need to define the trip demand series for the real trip scenario. As the demand is a power time series, it has N-1 entries when compared to the N entires of time axis for the energy. There is a morning drive from 07:10 a.m. to 08:10 a.m.. The power of 10 kW is required. Further there is an evening drive from 4:13 p.m. to 5:45 p.m.. The power of 9 kW is required.

ev_demand = pd.Series(0, index=time_index[:-1])

driving_start_morning = pd.Timestamp("2025-01-01 07:10")
driving_end_morning = pd.Timestamp("2025-01-01 08:10")
ev_demand.loc[driving_start_morning:driving_end_morning] = 10  # kW


driving_start_evening = pd.Timestamp("2025-01-01 16:13:37")
driving_end_evening = pd.Timestamp("2025-01-01 17:45:11")
ev_demand.loc[driving_start_evening:driving_end_evening] = 9  # kW

Note

Keep in mind that the units used in your energy system model are only implicit and that you have to check their consistency yourself.

Lets look at the driving pattern

plt.figure()
# plt.style.use("dark_background")
plt.title("Driving pattern")
plt.plot(ev_demand)
plt.ylabel("Power (kW)")
plt.gcf().autofmt_xdate()
Driving pattern
Driving pattern

Fig. 19 Driving pattern

Now we need to set up the electric energy carrying bus. We make sure to set a label to reference them later when we analyze the results. After initialization, we add them to the ev_energy_system object.

bus_car = solph.Bus(label="Car Electricity")

ev_energy_system.add(bus_car)

After setting up the energy system and the buses, we can now add the components to the energy system. We adding the driving demand as solph.components.Sink, where the loading profile is added as fix and nominal_capacity is set to one, because the loading profile is absolute. As we have a demand time series which is actually in kW, we use a common “hack” here: We set the nominal capacity to 1 (kW), so that multiplication by the time series will just yield the correct result.

The driving demand input is connected with the the electric energy carrying bus.

The car battery is added as solph.components.GenericStorage. The following parameters are set:

  • nominal_capacity is set to 50 (kWh), which is the capacity of the battery.

  • capacity_loss is set to 0.001. This means the battery loss per hour is 0.1% percent.

  • initial_capacity is set to 1. This indicates that the battery is full at the beginning of the simulation.

  • inflow_conversion_factor is set to 0.9, so the charging efficency is 90%.

  • balanced is set to False. This means the battery storage level at the end has not to be the same as at the beginning.

  • storage_costs is set to the defined storage revenue. Where “storage revenue” is defined as list with negative costs (of 60 ct/kWh) for the last time step, so that energy inside the storage in the last time step is worth something.

This leads to the fact that the battery is not necessary emptied at the end of the simulation.

The car battery inputs and outputs are connected with the the electric energy carrying bus.

demand_driving = solph.components.Sink(
    label="Driving Demand",
    inputs={bus_car: solph.Flow(nominal_capacity=1, fix=ev_demand)},
)

ev_energy_system.add(demand_driving)

storage_revenue = np.zeros(len(time_index) - 1)
storage_revenue[-1] = -0.6  # 60 ct/kWh in the last time step

car_battery = solph.components.GenericStorage(
    label="Car Battery",
    nominal_capacity=50,  # kWh
    inputs={bus_car: solph.Flow()},
    outputs={bus_car: solph.Flow()},
    initial_storage_level=1,  # full in the beginning
    loss_rate=0.001,  # 0.1 % / hr
    inflow_conversion_factor=0.9,  # 90 % charging efficiency
    balanced=False,  # True: content at beginning and end need to be equal
    storage_costs=storage_revenue,  # Only has an effect on charging.
)

ev_energy_system.add(car_battery)

As our system is complete for this step. Before we start the unit commitment optimization, let us have a look at the energy system graph

plt.figure()
graph = create_nx_graph(ev_energy_system)
nx.draw(graph, with_labels=True, font_size=8)

While nx.draw is handy to just have a quick look without too many extra tools, writing the graph dot allows for handling in specialised programs. The folloing has been created using

Energy system graph in step 1

For the actual optimisation, we first have to create a solph.Model instance from our ev_energy_system. Then we can use its solve() method to run the optimization. We decide to use the open source solver CBC and add the additional solve_kwargs parameter 'tee' to True, in order to get a more verbose solver logging output in the console.

model = solph.Model(ev_energy_system)
results = model.solve(solver="cbc", solve_kwargs={"tee": True})

Note

Optimisation will fail if supply and demand cannot be balanced. You can try this by setting initial_storage_level=0.

Now plot the results using the helper function from helpers.py.

The results are showing that the EV is using the battery while driving.

plot_results(
    results=results, plot_title="Driving demand only", dark_mode=False
)
plt.show()
Driving pattern
Driving pattern

Fig. 20 Driving pattern

Learning

The model balances supply and demand along flows in a graph based model. The operation is optimised so that the total costs are minimised.

You can get the complete (uncommented) code for this step: ev_charging_1.py

Click to display the code
# %%[imports_start]

import matplotlib.pyplot as plt
import networkx as nx
import numpy as np
import pandas as pd
from helpers import plot_results
from oemof.network.graph import create_nx_graph

# %%[imports_end]
# %%[create_time_index_set_up_energysystem_start]
import oemof.solph as solph

time_index = pd.date_range(
    start="2025-01-01",
    end="2025-01-02",
    freq="5min",
    inclusive="both",
)

ev_energy_system = solph.EnergySystem(
    timeindex=time_index,
    infer_last_interval=False,
)
# %%[create_time_index_set_up_energysystem_end]
# %%[trip_data_start]
ev_demand = pd.Series(0, index=time_index[:-1])

driving_start_morning = pd.Timestamp("2025-01-01 07:10")
driving_end_morning = pd.Timestamp("2025-01-01 08:10")
ev_demand.loc[driving_start_morning:driving_end_morning] = 10  # kW


driving_start_evening = pd.Timestamp("2025-01-01 16:13:37")
driving_end_evening = pd.Timestamp("2025-01-01 17:45:11")
ev_demand.loc[driving_start_evening:driving_end_evening] = 9  # kW
# %%[trip_data_end]
## %%[plot_trip_data_start]
plt.figure()
# plt.style.use("dark_background")
plt.title("Driving pattern")
plt.plot(ev_demand)
plt.ylabel("Power (kW)")
plt.gcf().autofmt_xdate()
## %%[plot_trip_data_end]
# %%[energysystem_and_bus_start]
bus_car = solph.Bus(label="Car Electricity")

ev_energy_system.add(bus_car)
# %%[energysystem_and_bus_end]
# %%[car_start]
demand_driving = solph.components.Sink(
    label="Driving Demand",
    inputs={bus_car: solph.Flow(nominal_capacity=1, fix=ev_demand)},
)

ev_energy_system.add(demand_driving)

storage_revenue = np.zeros(len(time_index) - 1)
storage_revenue[-1] = -0.6  # 60 ct/kWh in the last time step

car_battery = solph.components.GenericStorage(
    label="Car Battery",
    nominal_capacity=50,  # kWh
    inputs={bus_car: solph.Flow()},
    outputs={bus_car: solph.Flow()},
    initial_storage_level=1,  # full in the beginning
    loss_rate=0.001,  # 0.1 % / hr
    inflow_conversion_factor=0.9,  # 90 % charging efficiency
    balanced=False,  # True: content at beginning and end need to be equal
    storage_costs=storage_revenue,  # Only has an effect on charging.
)

ev_energy_system.add(car_battery)
# %%[car_end]
# %%[graph_start]
plt.figure()
graph = create_nx_graph(ev_energy_system)
nx.draw(graph, with_labels=True, font_size=8)

# %%[graph_end]
# %%[solve_start]
model = solph.Model(ev_energy_system)
results = model.solve(solver="cbc", solve_kwargs={"tee": True})
# %%[solve_end]
# %%[plot_results_start]
plot_results(
    results=results, plot_title="Driving demand only", dark_mode=False
)
plt.show()
# %%[plot_results_end]

Step 2: Plugged EV as load

Now, let’s assume the car battery can be charged at home. To be able to load the battery a charger is necessary. Unfortunately, there is only a power socket available, limiting the charging process to 16 A at 230 V. The costs for charging are 30 ct/kWh.

Note

Costs in the model are understood as a mathematical term, i.e. you could minimise emissions by choosing 363 g/kWh (German average for 2024) instead of the monetary costs.

So the charging is added as solph.components.Sink, where the nominal_capacity is set to 3.68 kW (= 230 V * 16 A) and variable_costs are set to 0.3 (30 ct/kWh). This, of course, can only happen while the car is present at home. To connect the car while it is at home, we create avalibility data series. The value 1 means that the car is at home, chargable. When it is set to 0, car is not present (between morning departure and the arrival back home in the evening). The timeseries is set as max.


car_at_home = pd.Series(1, index=time_index[:-1])
car_at_home.loc[driving_start_morning:driving_end_evening] = 0

charger230V = solph.components.Source(
    label="230V AC",
    outputs={
        bus_car: solph.Flow(
            nominal_capacity=3.68,  # 230 V * 16 A = 3.68 kW
            variable_costs=0.3,  # 30 ct/kWh
            maximum=car_at_home,
        )
    },
)

ev_energy_system.add(charger230V)

Now we are looking at the results: The EV will be charged as much as possible. As it’s not possible to load it completely in the afternoon, it is charged to 100 % just before the first leaving.

Driving pattern
Driving pattern

Learning

Optimisation is carried out under perfect foresight, meaning that things that happen later can imply things earlier in the time horison.

You can get the complete (uncommented) code for this step: ev_charging_2.py

Click to display the code
# %%[imports_start]

import matplotlib.pyplot as plt
import networkx as nx
import numpy as np
import pandas as pd
from helpers import plot_results
from oemof.network.graph import create_nx_graph

# %%[imports_end]
# %%[create_time_index_set_up_energysystem_start]
import oemof.solph as solph

time_index = pd.date_range(
    start="2025-01-01",
    end="2025-01-02",
    freq="5min",
    inclusive="both",
)

ev_energy_system = solph.EnergySystem(
    timeindex=time_index,
    infer_last_interval=False,
)
# %%[create_time_index_set_up_energysystem_end]
# %%[trip_data_start]
ev_demand = pd.Series(0, index=time_index[:-1])

driving_start_morning = pd.Timestamp("2025-01-01 07:10")
driving_end_morning = pd.Timestamp("2025-01-01 08:10")
ev_demand.loc[driving_start_morning:driving_end_morning] = 10  # kW


driving_start_evening = pd.Timestamp("2025-01-01 16:13:37")
driving_end_evening = pd.Timestamp("2025-01-01 17:45:11")
ev_demand.loc[driving_start_evening:driving_end_evening] = 9  # kW
# %%[trip_data_end]
## %%[plot_trip_data_start]
plt.figure()
# plt.style.use("dark_background")
plt.title("Driving pattern")
plt.plot(ev_demand)
plt.ylabel("Power (kW)")
plt.gcf().autofmt_xdate()
## %%[plot_trip_data_end]
# %%[energysystem_and_bus_start]
bus_car = solph.Bus(label="Car Electricity")

ev_energy_system.add(bus_car)
# %%[energysystem_and_bus_end]
# %%[car_start]
demand_driving = solph.components.Sink(
    label="Driving Demand",
    inputs={bus_car: solph.Flow(nominal_capacity=1, fix=ev_demand)},
)

ev_energy_system.add(demand_driving)

storage_revenue = np.zeros(len(time_index) - 1)
storage_revenue[-1] = -0.6  # 60 ct/kWh in the last time step

car_battery = solph.components.GenericStorage(
    label="Car Battery",
    nominal_capacity=50,  # kWh
    inputs={bus_car: solph.Flow()},
    outputs={bus_car: solph.Flow()},
    initial_storage_level=1,  # full in the beginning
    loss_rate=0.001,  # 0.1 % / hr
    inflow_conversion_factor=0.9,  # 90 % charging efficiency
    balanced=False,  # True: content at beginning and end need to be equal
    storage_costs=storage_revenue,  # Only has an effect on charging.
)

ev_energy_system.add(car_battery)
# %%[car_end]


# %%[AC_30ct_charging_start]

car_at_home = pd.Series(1, index=time_index[:-1])
car_at_home.loc[driving_start_morning:driving_end_evening] = 0

charger230V = solph.components.Source(
    label="230V AC",
    outputs={
        bus_car: solph.Flow(
            nominal_capacity=3.68,  # 230 V * 16 A = 3.68 kW
            variable_costs=0.3,  # 30 ct/kWh
            maximum=car_at_home,
        )
    },
)

ev_energy_system.add(charger230V)

# %%[AC_30ct_charging_end]
# %%[graph_start]
plt.figure()
graph = create_nx_graph(ev_energy_system)
nx.draw(graph, with_labels=True, font_size=8)
# %%[graph_end]
# %%[solve_start]# %%[solve_start]
model = solph.Model(ev_energy_system)
results = model.solve(solver="cbc", solve_kwargs={"tee": True})
# %%[solve_end]
# %%[plot_results_start]
plot_results(
    results=results,
    plot_title="Domestic power socket charging",
    dark_mode=False,
)
plt.show()
# %%[plot_results_end]

Step 3: Free charging with PV system at work

Within this step we are regarding a free charging option at work. So we add an 11 kW charger (free of charge) which is available at work. This, of course, can only happen while the car is present at work. Same with avalibility data at home charging, we will create a data set for avalibility at work. When it is set to 0, car is not present at the work. When it is set to 1, car is able to connect to work charge station (between morning arrival and the evening departure from work).

car_at_work = pd.Series(0, index=time_index[:-1])
car_at_work.loc[driving_end_morning:driving_start_evening] = 1

# variable_costs in the Flow default to 0, so it's free
charger11kW = solph.components.Source(
    label="11kW",
    outputs={
        bus_car: solph.Flow(
            nominal_capacity=11,  # 11 kW
            maximum=car_at_work,
        )
    },
)

ev_energy_system.add(charger11kW)

Looking at the results we see the battery is charging and discharing at the same time within the beginning. Charging and discharging at the same time is almost always a sign that something is not moddeled accurately in the energy system. A possible solution will be introducted within the next step.

Further we can see, the battery is charged when the car is at work, because the charging is free.

Driving pattern
Driving pattern

Learning

Among multiple optimal solutions, any one can be your results. If something is free in the model, this can include unintuitive things.

You can get the complete (uncommented) code for this step: ev_charging_3.py

Click to display the code
# %%[imports_start]

import matplotlib.pyplot as plt
import networkx as nx
import numpy as np
import pandas as pd
from helpers import plot_results
from oemof.network.graph import create_nx_graph

# %%[imports_end]
# %%[create_time_index_set_up_energysystem_start]
import oemof.solph as solph

time_index = pd.date_range(
    start="2025-01-01",
    end="2025-01-02",
    freq="5min",
    inclusive="both",
)

ev_energy_system = solph.EnergySystem(
    timeindex=time_index,
    infer_last_interval=False,
)
# %%[create_time_index_set_up_energysystem_end]
# %%[trip_data_start]
ev_demand = pd.Series(0, index=time_index[:-1])

driving_start_morning = pd.Timestamp("2025-01-01 07:10")
driving_end_morning = pd.Timestamp("2025-01-01 08:10")
ev_demand.loc[driving_start_morning:driving_end_morning] = 10  # kW


driving_start_evening = pd.Timestamp("2025-01-01 16:13:37")
driving_end_evening = pd.Timestamp("2025-01-01 17:45:11")
ev_demand.loc[driving_start_evening:driving_end_evening] = 9  # kW
# %%[trip_data_end]
## %%[plot_trip_data_start]
plt.figure()
# plt.style.use("dark_background")
plt.title("Driving pattern")
plt.plot(ev_demand)
plt.ylabel("Power (kW)")
plt.gcf().autofmt_xdate()
## %%[plot_trip_data_end]
# %%[energysystem_and_bus_start]
bus_car = solph.Bus(label="Car Electricity")

ev_energy_system.add(bus_car)
# %%[energysystem_and_bus_end]
# %%[car_start]
demand_driving = solph.components.Sink(
    label="Driving Demand",
    inputs={bus_car: solph.Flow(nominal_capacity=1, fix=ev_demand)},
)

ev_energy_system.add(demand_driving)

storage_revenue = np.zeros(len(time_index) - 1)
storage_revenue[-1] = -0.6  # 60 ct/kWh in the last time step

car_battery = solph.components.GenericStorage(
    label="Car Battery",
    nominal_capacity=50,  # kWh
    inputs={bus_car: solph.Flow()},
    outputs={bus_car: solph.Flow()},
    initial_storage_level=1,  # full in the beginning
    loss_rate=0.001,  # 0.1 % / hr
    inflow_conversion_factor=0.9,  # 90 % charging efficiency
    balanced=False,  # True: content at beginning and end need to be equal
    storage_costs=storage_revenue,  # Only has an effect on charging.
)

ev_energy_system.add(car_battery)
# %%[car_end]


# %%[AC_30ct_charging_start]

car_at_home = pd.Series(1, index=time_index[:-1])
car_at_home.loc[driving_start_morning:driving_end_evening] = 0

# To be able to load the battery a electric source e.g. electric grid is
# necessary. We set the maximum use to 1 if the car is present, while it
# is 0 between the morning start and the evening arrival back home.
# While the car itself can potentially charge with at a higher power,
# we just add an AC source with 16 A at 230 V.
charger230V = solph.components.Source(
    label="230V AC",
    outputs={
        bus_car: solph.Flow(
            nominal_capacity=3.68,  # 230 V * 16 A = 3.68 kW
            variable_costs=0.3,  # 30 ct/kWh
            maximum=car_at_home,
        )
    },
)

ev_energy_system.add(charger230V)

# %%[AC_30ct_charging_end]
# %%[DC_charging_start]
car_at_work = pd.Series(0, index=time_index[:-1])
car_at_work.loc[driving_end_morning:driving_start_evening] = 1

# variable_costs in the Flow default to 0, so it's free
charger11kW = solph.components.Source(
    label="11kW",
    outputs={
        bus_car: solph.Flow(
            nominal_capacity=11,  # 11 kW
            maximum=car_at_work,
        )
    },
)

ev_energy_system.add(charger11kW)
# %%[DC_charging_end]
# %%[graph_start]
plt.figure()
graph = create_nx_graph(ev_energy_system)
nx.draw(graph, with_labels=True, font_size=8)
# %%[graph_end]
# %%[solve_start]
# %%[solve_start]
model = solph.Model(ev_energy_system)
results = model.solve(solver="cbc", solve_kwargs={"tee": True})
# %%[solve_end]
# %%[plot_results_start]
plot_results(
    results=results,
    plot_title="Home and work charging",
    dark_mode=False,
)
plt.show()
# %%[plot_results_end]

Step 4: Fix free charging artefact and allow bidirectional use of the battery

To avoid the energy from looping in the battery, we introduce costs to battery charging. This is a way to model cyclic aging of the battery. This is done by adding some variable_costs on flow of to the input.

Further in this step we want to allow the bidirectional use of the battery. So we are setting the balanced to the default True. This means the battery storage level at the end has to be the same as at the beginning. We also have to remove the initial_capacity, so SOC(T=0) = SOC(T=T_max) is valid. To ensure to have a reserve of 10 % within the battery, the min_storage_level is set to 0.1.

car_battery = solph.components.GenericStorage(
    label="Car Battery",
    nominal_capacity=50,
    inputs={bus_car: solph.Flow(variable_costs=0.1)},
    outputs={bus_car: solph.Flow()},
    loss_rate=0.001,
    inflow_conversion_factor=0.9,
    balanced=True,  # this is the default: SOC(T=0) = SOC(T=T_max)
    min_storage_level=0.1,  # 10 % as reserve
)

ev_energy_system.add(car_battery)

To be able to discharge the battery we need a discharger, which will be added as solph.components.Sink. The nominal_capacity is set to 3.68 kW (= 230 V * 16 A) and variable_costs are set to -0.3 to save 30 ct/kWh. The battery can only be discharged is the car is at home, so the created timeseries from above is used and set to max.

discharger230V = solph.components.Sink(
    label="230V AC discharge",
    inputs={
        bus_car: solph.Flow(
            nominal_capacity=3.68,  # 230 V * 16 A = 3.68 kW
            variable_costs=-0.3,
            maximum=car_at_home,
        )
    },
)

ev_energy_system.add(discharger230V)

The final energy system graph now looks like this:

Energy system graph in step 4

Looking at the results:

  • The charging and discharing within the beginning does not accure anymore

  • The battery will be loaded for free within the working period

  • There is a discharging at home at the evening to save money

Driving pattern
Driving pattern

Learning

Looped energy flows can be used as an indicator for a flawed model. If possible, it should be fixed (instead of suppressed).

You can get the complete (uncommented) code for this step: ev_charging_4.py

Click to display the code
# %%[imports_start]

import matplotlib.pyplot as plt
import networkx as nx
import pandas as pd
from helpers import plot_results
from oemof.network.graph import create_nx_graph

# %%[imports_end]
# %%[create_time_index_set_up_energysystem_start]
import oemof.solph as solph

time_index = pd.date_range(
    start="2025-01-01",
    end="2025-01-02",
    freq="5min",
    inclusive="both",
)

ev_energy_system = solph.EnergySystem(
    timeindex=time_index,
    infer_last_interval=False,
)
# %%[create_time_index_set_up_energysystem_end]
# %%[trip_data_start]
ev_demand = pd.Series(0, index=time_index[:-1])

driving_start_morning = pd.Timestamp("2025-01-01 07:10")
driving_end_morning = pd.Timestamp("2025-01-01 08:10")
ev_demand.loc[driving_start_morning:driving_end_morning] = 10  # kW


driving_start_evening = pd.Timestamp("2025-01-01 16:13:37")
driving_end_evening = pd.Timestamp("2025-01-01 17:45:11")
ev_demand.loc[driving_start_evening:driving_end_evening] = 9  # kW
# %%[trip_data_end]
## %%[plot_trip_data_start]
plt.figure()
# plt.style.use("dark_background")
plt.title("Driving pattern")
plt.plot(ev_demand)
plt.ylabel("Power (kW)")
plt.gcf().autofmt_xdate()
## %%[plot_trip_data_end]
# %%[energysystem_and_bus_start]
bus_car = solph.Bus(label="Car Electricity")

ev_energy_system.add(bus_car)
# %%[energysystem_and_bus_end]
# %%[car_start]
demand_driving = solph.components.Sink(
    label="Driving Demand",
    inputs={bus_car: solph.Flow(nominal_capacity=1, fix=ev_demand)},
)

ev_energy_system.add(demand_driving)
# %%[car_end]
# %%[car_battery_start]
car_battery = solph.components.GenericStorage(
    label="Car Battery",
    nominal_capacity=50,
    inputs={bus_car: solph.Flow(variable_costs=0.1)},
    outputs={bus_car: solph.Flow()},
    loss_rate=0.001,
    inflow_conversion_factor=0.9,
    balanced=True,  # this is the default: SOC(T=0) = SOC(T=T_max)
    min_storage_level=0.1,  # 10 % as reserve
)

ev_energy_system.add(car_battery)
# %%[car_battery_end]


# %%[AC_30ct_charging_start]
car_at_home = pd.Series(1, index=time_index[:-1])
car_at_home.loc[driving_start_morning:driving_end_evening] = 0

charger230V = solph.components.Source(
    label="230V AC",
    outputs={
        bus_car: solph.Flow(
            nominal_capacity=3.68,  # 230 V * 16 A = 3.68 kW
            variable_costs=0.3,  # 30 ct/kWh
            maximum=car_at_home,
        )
    },
)

ev_energy_system.add(charger230V)
# %%[AC_30ct_charging_end]


# %%[DC_charging_start]
car_at_work = pd.Series(0, index=time_index[:-1])
car_at_work.loc[driving_end_morning:driving_start_evening] = 1

charger11kW = solph.components.Source(
    label="11kW",
    outputs={
        bus_car: solph.Flow(
            nominal_capacity=11,  # 11 kW
            maximum=car_at_work,
        )
    },
)

ev_energy_system.add(charger11kW)
# %%[DC_charging_end]


# %%[AC_discharging_start]
discharger230V = solph.components.Sink(
    label="230V AC discharge",
    inputs={
        bus_car: solph.Flow(
            nominal_capacity=3.68,  # 230 V * 16 A = 3.68 kW
            variable_costs=-0.3,
            maximum=car_at_home,
        )
    },
)

ev_energy_system.add(discharger230V)
# %%[AC_discharging_end]
# %%[graph_start]
plt.figure()
graph = create_nx_graph(ev_energy_system)
nx.draw(graph, with_labels=True, font_size=8)
# %%[graph_end]
# %%[solve_start]
# %%[solve_and_plot_start]
model = solph.Model(ev_energy_system)
results = model.solve(solver="cbc", solve_kwargs={"tee": False})

plot_results(results=results, plot_title="Bidirectional use constant costs")
plt.show()
# %%[solve_and_plot_end]

Step 5: Variable electricity prices

Within the last step we want to regard dynamic prices for the the charging and discharging at home. So the optimization is going to load when the prices are are load and discharge if the prices are high. Assuming the following prices:

  • Before 6 a.m.: 5 ct/kWh

  • After 4 p.m. 70 ct/kWh

  • Otherwise: 50 ct/kWh

dynamic_price = pd.Series(0.5, index=time_index[:-1])
dynamic_price.loc[: pd.Timestamp("2025-01-01 06:00")] = 0.05
dynamic_price.loc[
    pd.Timestamp("2025-01-01 06:00") : pd.Timestamp("2025-01-01 10:00")
] = 0.5
dynamic_price.loc[pd.Timestamp("2025-01-01 16:00") :] = 0.7

Lets have a look on the dynamic prices

Driving pattern
Driving pattern

The prices have to be set within the charger and discharger instead of the 30 ct/kWh before, we set the variable_costs to the dynamic price or rather to the negative dynamic price.

charger230V = solph.components.Source(
    label="230V AC",
    outputs={
        bus_car: solph.Flow(
            nominal_capacity=3.68,  # 230 V * 16 A = 3.68 kW
            variable_costs=dynamic_price,
            maximum=car_at_home,
        )
    },
)

discharger230V = solph.components.Sink(
    label="230V AC discharge",
    inputs={
        bus_car: solph.Flow(
            nominal_capacity=3.68,  # 230 V * 16 A = 3.68 kW
            variable_costs=-dynamic_price,
            maximum=car_at_home,
        )
    },
)

ev_energy_system.add(charger230V, discharger230V)

Looking at the results the battery is loaded before 6 a.m. with the cheap price of 5 ct/kWh right before the first leaving to get 50 ct/kWh. The battery is recharged for free at the work and in the evening discharged to get 70 ct/kWh.

Driving pattern
Driving pattern

Learning

Costs can be time-dependent. The optimal operation can be changed this way if the model includes a storage.

You can get the complete (uncommented) code for this step: ev_charging_5.py

Click to display the code
# %%[imports_start]

import matplotlib.pyplot as plt
import networkx as nx
import pandas as pd
from helpers import plot_results
from oemof.network.graph import create_nx_graph

# %%[imports_end]
# %%[create_time_index_set_up_energysystem_start]
import oemof.solph as solph

time_index = pd.date_range(
    start="2025-01-01",
    end="2025-01-02",
    freq="5min",
    inclusive="both",
)

ev_energy_system = solph.EnergySystem(
    timeindex=time_index,
    infer_last_interval=False,
)
# %%[create_time_index_set_up_energysystem_end]
# %%[trip_data_start]
ev_demand = pd.Series(0, index=time_index[:-1])

driving_start_morning = pd.Timestamp("2025-01-01 07:10")
driving_end_morning = pd.Timestamp("2025-01-01 08:10")
ev_demand.loc[driving_start_morning:driving_end_morning] = 10  # kW


driving_start_evening = pd.Timestamp("2025-01-01 16:13:37")
driving_end_evening = pd.Timestamp("2025-01-01 17:45:11")
ev_demand.loc[driving_start_evening:driving_end_evening] = 9  # kW
# %%[trip_data_end]
## %%[plot_trip_data_start]
plt.figure()
# plt.style.use("dark_background")
plt.title("Driving pattern")
plt.plot(ev_demand)
plt.ylabel("Power (kW)")
plt.gcf().autofmt_xdate()
## %%[plot_trip_data_end]
# %%[energysystem_and_bus_start]
bus_car = solph.Bus(label="Car Electricity")

ev_energy_system.add(bus_car)
# %%[energysystem_and_bus_end]
# %%[car_start]
demand_driving = solph.components.Sink(
    label="Driving Demand",
    inputs={bus_car: solph.Flow(nominal_capacity=1, fix=ev_demand)},
)

ev_energy_system.add(demand_driving)
# %%[car_end]
# %%[car_battery_start]
car_battery = solph.components.GenericStorage(
    label="Car Battery",
    nominal_capacity=50,
    inputs={bus_car: solph.Flow(variable_costs=0.1)},
    outputs={bus_car: solph.Flow()},
    loss_rate=0.001,
    inflow_conversion_factor=0.9,
    balanced=True,  # this is the default: SOC(T=0) = SOC(T=T_max)
    min_storage_level=0.1,  # 10 % as reserve
)

ev_energy_system.add(car_battery)
# %%[car_battery_end]


# %%[AC_dynamic_price_start]
dynamic_price = pd.Series(0.5, index=time_index[:-1])
dynamic_price.loc[: pd.Timestamp("2025-01-01 06:00")] = 0.05
dynamic_price.loc[
    pd.Timestamp("2025-01-01 06:00") : pd.Timestamp("2025-01-01 10:00")
] = 0.5
dynamic_price.loc[pd.Timestamp("2025-01-01 16:00") :] = 0.7
# %%[AC_dynamic_price_end]

## %%[plot_dynamic_price_start]
plt.figure()
# plt.style.use("dark_background")
plt.title("Dynamic prices")
plt.plot(dynamic_price)
plt.ylabel("€/MWh")
plt.gcf().autofmt_xdate()
## %%[plot_dynamic_price_end]

# %%[Car_at_home_start]
car_at_home = pd.Series(1, index=time_index[:-1])
car_at_home.loc[driving_start_morning:driving_end_evening] = 0
# %%[Car_at_home_end]

# %%[Charging_discharging_with_dynamic_prices_start]
charger230V = solph.components.Source(
    label="230V AC",
    outputs={
        bus_car: solph.Flow(
            nominal_capacity=3.68,  # 230 V * 16 A = 3.68 kW
            variable_costs=dynamic_price,
            maximum=car_at_home,
        )
    },
)

discharger230V = solph.components.Sink(
    label="230V AC discharge",
    inputs={
        bus_car: solph.Flow(
            nominal_capacity=3.68,  # 230 V * 16 A = 3.68 kW
            variable_costs=-dynamic_price,
            maximum=car_at_home,
        )
    },
)

ev_energy_system.add(charger230V, discharger230V)
# %%[Charging_discharging_with_dynamic_prices_end]


# %%[DC_charging_start]
"""
Now, we add an 11 kW charger (free of charge) which is available at work.
This, of course, can only happen while the car is present at work.
"""


car_at_work = pd.Series(0, index=time_index[:-1])
car_at_work.loc[driving_end_morning:driving_start_evening] = 1

# variable_costs in the Flow default to 0, so it's free
charger11kW = solph.components.Source(
    label="11kW",
    outputs={
        bus_car: solph.Flow(
            nominal_capacity=11,  # 11 kW
            maximum=car_at_work,
        )
    },
)

ev_energy_system.add(charger11kW)
# %%[DC_charging_end]
# %%[graph_start]
plt.figure()
graph = create_nx_graph(ev_energy_system)
nx.draw(graph, with_labels=True, font_size=8)
# %%[graph_end]
# %%[solve_and_plot_start]
model = solph.Model(ev_energy_system)
results = model.solve()

plot_results(
    results=results,
    plot_title="Bidirectional use dynamic prices",
    dark_mode=False,
)
plt.show()
# %%[solve_and_plot_end]