This article is based on the EV
user profile concept from package evprof
, and how the
extra-knowledge that a classification of EV users into user profiles
could help in the smart charging ecosystem.
A user profile is defined by a pattern in the connection times of EV users, so in most of cases, there are EV user profiles with more flexibility potential than others. A Charging Point Operator (CPO) can take advantage of this knowledge and use this classification to set priorities when scheduling EV charging sessions.
Shifting only the sessions of specific EV user profiles can increase the efficiency of the system since the forecast of the flexibility potential form EV users is more reliable. The lower number of EV sessions that have to be re-scheduled, the better the solution, for both the CPO and the final EV user.
EV charging sessions simulation
First of all, we can simulate EV sessions using the
evsim
package, which provides the function
evsim::get_custom_ev_model
to create a custom EV model to
later simulate EV sessions with evsim::simulate_sessions()
function. We can create an EV model with custom time-cycles and user
profiles. In this case, we will consider two different EV user
profiles:
- Worktime: present during working days. Low-variability in connection times. High flexibility potential.
- Visit: present the whole week but with different behaviour on weekends. High variability in connection times. Low flexibility potential.
# For workdays time cycle
workdays_parameters <- dplyr::tibble(
profile = c("Worktime", "Visit"),
ratio = c(50, 50),
start_mean = c(9, 11),
start_sd = c(1, 4),
duration_mean = c(8, 4),
duration_sd = c(0.5, 2),
energy_mean = c(15, 6),
energy_sd = c(4, 3)
)
# For weekends time cycle
weekends_parameters <- dplyr::tibble(
profile = "Visit",
ratio = 100,
start_mean = 12,
start_sd = 4,
duration_mean = 3,
duration_sd = 2,
energy_mean = 4,
energy_sd = 4
)
parameters_lst <- list(workdays_parameters, weekends_parameters)
# Get the whole model
ev_model <- get_custom_ev_model(
names = c("Workdays", "Weekends"),
months_lst = list(1:12, 1:12),
wdays_lst = list(1:5, 6:7),
parameters_lst = parameters_lst,
connection_log = FALSE,
energy_log = FALSE,
data_tz = "Europe/Amsterdam"
)
Once we have our own model, we can simulate EV sessions for three different days as example:
set.seed(1234)
ev_sessions <- simulate_sessions(
evmodel = ev_model,
sessions_day = tibble(time_cycle = c("Workdays", "Weekends"), n_sessions = c(10, 10)),
user_profiles = NULL,
charging_powers = tibble(power = 3.7, ratio = 1),
dates = seq.Date(from = dmy("05-08-2024"), to = dmy("05-08-2024")+days(2), by = "day"),
resolution = 15
)
## # A tibble: 21 × 11
## Session Timecycle Profile ConnectionStartDateTime ConnectionEndDateTime
## <chr> <chr> <chr> <dttm> <dttm>
## 1 S1 Workdays Worktime 2024-08-05 08:45:00 2024-08-05 16:58:00
## 2 S2 Workdays Worktime 2024-08-05 09:15:00 2024-08-05 17:04:00
## 3 S3 Workdays Worktime 2024-08-05 09:30:00 2024-08-05 17:37:00
## 4 S4 Workdays Visit 2024-08-05 09:45:00 2024-08-05 12:29:00
## 5 S5 Workdays Worktime 2024-08-05 09:45:00 2024-08-05 17:22:00
## 6 S6 Workdays Visit 2024-08-05 10:00:00 2024-08-05 14:29:00
## 7 S7 Workdays Visit 2024-08-05 10:45:00 2024-08-05 17:46:00
## 8 S8 Workdays Visit 2024-08-05 12:45:00 2024-08-05 18:29:00
## 9 S9 Workdays Visit 2024-08-05 14:45:00 2024-08-05 18:59:00
## 10 S10 Workdays Visit 2024-08-05 20:00:00 2024-08-05 23:02:00
## # ℹ 11 more rows
## # ℹ 6 more variables: ChargingStartDateTime <dttm>, ChargingEndDateTime <dttm>,
## # Power <dbl>, Energy <dbl>, ConnectionHours <dbl>, ChargingHours <dbl>
Finally we can calculate the time-series power demand from each EV
with function evsim::get_demand()
, using parameter
by="Sessions"
:
ev_demand <- ev_sessions %>%
get_demand(by = "Profile")
Smart charging to solve grid congestion
Imagine that we have to charge these EVs in an installation that has a maximum grid connection of 12 kW:
grid_capacity <- 12
Since our peak goes above 12kW, we need to use smart charging to allow the EV users to charge under the grid capacity. In our case, we decide to only use the flexibility from the Worktime user profile, since it has a clear flexibility potential compared to the Visit user profile.
In order to define a grid capacity in the
smart_charing()
function, a column with the same name than
the EV user profile in the sessions
parameter (in this
example “Worktime”) must be found in the opt_data
parameter. Therefore, a "Worktime"
column is added to the
opt_data
object as a setpoint for the EV user profile
“Worktime”, and it is defined as the difference between the grid
capacity and the rest of power demand (in this case the “Visit”
demand):
opt_data <- tibble(
datetime = ev_demand$datetime, # Same date-time sequence than the demand
Worktime = grid_capacity - ev_demand$Visit
)
opt_data %>%
dyplot(ylab = "Power demand (kW)", stackedGraph = T, fillGraph = T, legend_width = 200) %>%
dySeries("Worktime", "Worktime free capacity", color = "navy", strokeWidth = 2, strokePattern = "dashed") %>%
dyLimit(grid_capacity, "Grid capacity", color = "red")
Considering optimization windows of 24 hours from 6:00AM to 6:00AM,
and "curtail"
as a smart charging method:
sc_results <- ev_sessions %>%
smart_charging(
opt_data = opt_data,
opt_objective = "none",
method = "curtail",
window_days = 1,
window_start_hour = 6,
responsive = list(Workdays = list(Worktime = 1))
)
## Warning: `production` variable not found in `opt_data`. No local energy production will be considered.
ev_demand_opt <- sc_results$sessions %>%
get_demand(by = "Profile")
We can see that the Worktime power demand has been adapted to the non-flexbile users, i.e. Visit user profile, in order to not surpass the capacity limit of 12 kW.
Moreover, we can define which percentage of every user profile is
responsive to the smart charging signals with the parameter
responsive
from smart_charging()
function. For
example, we can set that, during working days, 60% of the
Worktime users and 20% of the Visit users
accept to participate to the demand-response program.
To do that, since now we consider different user profiles for an
aggregated grid capacity, the grid capacity limit has to be configured
in the opt_data
as "grid_capacity"
name:
opt_data <- tibble(
datetime = ev_demand$datetime, # Same date-time sequence than the demand
grid_capacity
)
We configure the responsiveness of every user
profile with the responsive
parameter. It’s always
advised that the responsiveness values are sorted from highest to
lowest:
sc_results <- ev_sessions %>%
smart_charging(
opt_data = opt_data,
opt_objective = "none",
method = "curtail",
window_days = 1,
window_start_hour = 6,
responsive = list(
Workdays = list(Worktime = 0.6, Visit = 0.2)
)
)
## Warning: `production` variable not found in `opt_data`. No local energy production will be considered.
plot(sc_results, ev_sessions)
From the plot we see that the grid capacity limit of 12 kW can’t be
always respected. Also, the setpoint in this plot doesn’t make a lot of
sense because represents the power limitation of the EV demand, but our
setpoint is avoiding a demand higher than 12 kW. The setpoint line in
the plot can be removed using the parameter
show_setpoint = FALSE
.
Is it for the lack of flexibility of the responsive sessions? Or maybe there aren’t enough responsive sessions? These questions can be answered by plotting the smart charging results by flexbility type:
plot(sc_results, ev_sessions, by = "FlexType", show_setpoint = FALSE) %>%
dyLimit(grid_capacity, "Grid capacity", color = "red")
The results show that during the hours where the grid capacity is surpassed the demand corresponds to Exploited and Not responsive sessions. Therefore, more responsive sessions are needed.
We can also plot the smart charging results by user profile to see that the demand causing the peaks over the grid capacity comes from non-responsive Visit sessions from 10:00 to 11:00:
plot(sc_results, ev_sessions, by = "Profile", show_setpoint = FALSE) %>%
dyLimit(grid_capacity, "Grid capacity", color = "red")
For a deeper analysis we can use function
summarise_smart_charging_sessions()
, which shows the
percentage of Considered, Responsive, Flexible and Exploited
sessions:
summarise_smart_charging_sessions(sc_results) %>%
knitr::kable()
timecycle | profile | group | subgroup | n_sessions | pct |
---|---|---|---|---|---|
Workdays | Visit | Total | Considered | 11 | 92 |
Workdays | Visit | Total | Not considered | 1 | 8 |
Workdays | Visit | Considered | Responsive | 2 | 18 |
Workdays | Visit | Considered | Non responsive | 9 | 82 |
Workdays | Visit | Responsive | Flexible | 2 | 100 |
Workdays | Visit | Flexible | Exploited | 1 | 50 |
Workdays | Visit | Flexible | Not exploited | 1 | 50 |
Workdays | Worktime | Total | Considered | 9 | 100 |
Workdays | Worktime | Considered | Responsive | 5 | 56 |
Workdays | Worktime | Considered | Non responsive | 4 | 44 |
Workdays | Worktime | Responsive | Flexible | 5 | 100 |
Workdays | Worktime | Flexible | Exploited | 5 | 100 |
We can extract the following conclusions from this table:
- The Responsive percentages correspond to the configured 60% for
Worktime and 20% for Visit in the
responsive
parameter (the difference is due to the small amount of sessions in the data) - None of the Flexible Visit sessions are Exploited, so they charge in times where flexibility is not needed
- All Worktime Flexible sessions are Exploited, trying to match the setpoint and adapt to the Visit demand
If we increase the participation of Worktime users up to 70% then we accomplish with our grid capacity again:
sc_results <- ev_sessions %>%
smart_charging(
opt_data = opt_data,
opt_objective = "none",
method = "curtail",
window_days = 1,
window_start_hour = 6,
responsive = list(
Workdays = list(Worktime = 0.7, Visit = 0.2)
)
)
## Warning: `production` variable not found in `opt_data`. No local energy production will be considered.
plot(sc_results, ev_sessions, show_setpoint = FALSE) %>%
dyLimit(grid_capacity, "Grid capacity", color = "red")
Then we can say that we need at least 70% of the Worktime users to participate in the smart charging program if we want to respect our grid constraints.