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

Fix tc secc ac vtb power delivery 010 #150

Merged
merged 1 commit into from
Nov 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 108 additions & 1 deletion iso15118/secc/states/iso15118_2_states.py
Original file line number Diff line number Diff line change
Expand Up @@ -1472,7 +1472,21 @@ async def process_message(
# )
# return

# TODO We should also do a more detailed check of the charging profile
# [V2G2-225] The SECC shall send the negative ResponseCode
# FAILED_ChargingProfileInvalid in
# the PowerDelivery response message if the EVCC sends a ChargingProfile which
# is not adhering to the PMax values of all PMaxScheduleEntry elements according
# to the chosen SAScheduleTuple element in the last ChargeParameterDiscoveryRes
# message sent by the SECC.
if power_delivery_req.charging_profile:
if not self._is_charging_profile_valid(power_delivery_req):
self.stop_state_machine(
"[V2G2-225] ChargingProfile is not adhering to the Pmax values in "
"ChargeParameterDiscoveryRes",
message,
ResponseCode.FAILED_CHARGING_PROFILE_INVALID,
)
return

logger.debug(f"ChargeProgress set to {power_delivery_req.charge_progress}")

Expand Down Expand Up @@ -1611,6 +1625,99 @@ async def check_state():
CpState.D2,
]

def _is_charging_profile_valid(self, power_delivery_req: PowerDeliveryReq) -> bool:
for schedule in self.comm_session.offered_schedules:
if schedule.sa_schedule_tuple_id == power_delivery_req.sa_schedule_tuple_id:
schedule_entries = schedule.p_max_schedule.schedule_entries
# schedule_entries shall look like [PMaxScheduleEntry_0,
# PMaxScheduleEntry_1, ...]. The enumerate will pair an index with
# each list entry: (0, PMaxScheduleEntry_0), (1,
# PMaxScheduleEntry_1), ...
ev_profile = power_delivery_req.charging_profile
ev_profile_entries = ev_profile.profile_entries
cached_start_idx_ev: int = 0
last_ev_running_idx: int = 0

for idx, sa_profile_entry in enumerate(schedule_entries):
sa_profile_entry_start = sa_profile_entry.time_interval.start

sa_entry_pmax = (
sa_profile_entry.p_max.value
* 10**sa_profile_entry.p_max.multiplier
)

try:
# By getting the next entry/slot, we can know when
# the current entry ends
next_sa_profile_entry = schedule_entries[idx + 1]
sa_profile_entry_end = next_sa_profile_entry.time_interval.start

except IndexError:
# As the index is out of rage, it signals this is
# the final entry of the sa profile. The last entry
# must have a duration field, so we can calculate
# the end of the period summing the relative start
# of the entry with the duration of it
sa_profile_entry_end = (
sa_profile_entry_start
+ sa_profile_entry.time_interval.duration
)
# The cached index and last ev running index are helpers that
# allow the for loop to start in the ev profile entry belonging
# to the start range of the SA entry. This avoids looping all
# over the ev profiles again for each SA entry schedule.
# For example, if the SA time schedule is
# [0; 1000[ [1000; 2000[ [2000; 3000[
# And the EV profile time schedule is
# [0, 100[ [100, 400[ [400; 1000[ [1000, inf+[
# The first three entries of the ev profile belong
# to the time range [0; 1000[ of the SA schedule and
# if the EV power, for those three entries
# does not surpass the limit imposed by the SA schedule
# during that time, then for the next loop iteration,
# we dont need to go test those EV time entries again,
# as they dont belong to the [1000; 2000[ SA time schedule slot.
cached_start_idx_ev += last_ev_running_idx
last_ev_running_idx = 0
# fmt: off
for (ev_profile_idx, ev_profile_entry) in enumerate(
ev_profile_entries[cached_start_idx_ev:]
):
_is_last_ev_profile = (
ev_profile_entry.start == ev_profile_entries[-1].start
)

if (ev_profile_entry.start < sa_profile_entry_end or _is_last_ev_profile ): # noqa
ev_entry_pmax = (ev_profile_entry.max_power.value * 10 ** ev_profile_entry.max_power.multiplier) # noqa
if ev_entry_pmax > sa_entry_pmax:
logger.error(
f"EV Profile start {ev_profile_entry.start}s"
f"is out of power range: "
f"EV Max {ev_entry_pmax} W > EVSE Max "
f"{sa_entry_pmax} W \n"
)
return False

if not _is_last_ev_profile:
ev_profile_entry_end = ev_profile_entries[
ev_profile_idx + 1
].start
if ev_profile_entry_end <= sa_profile_entry_end:
last_ev_running_idx = ev_profile_idx + 1
else:
logger.debug(
f"EV last Profile start "
f"{ev_profile_entry.start}s is "
f"within time range [{sa_profile_entry_start}; "
f"{sa_profile_entry_end}[ and power range: "
f"EV Max {ev_entry_pmax} W <= EVSE Max "
f"{sa_entry_pmax} W \n"
)
else:
break
# fmt: on
return True


class MeteringReceipt(StateSECC):
"""
Expand Down
58 changes: 58 additions & 0 deletions tests/secc/states/test_iso15118_2_states.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,19 @@
get_charge_parameter_discovery_req_message_departure_time_one_hour,
get_charge_parameter_discovery_req_message_no_departure_time,
get_dummy_charging_status_req,
get_dummy_sa_schedule,
get_dummy_v2g_message_authorization_req,
get_dummy_v2g_message_payment_details_req,
get_dummy_v2g_message_power_delivery_req_charge_start,
get_dummy_v2g_message_power_delivery_req_charge_stop,
get_dummy_v2g_message_service_discovery_req,
get_dummy_v2g_message_welding_detection_req,
get_power_delivery_req_charging_profile_in_boundary_invalid,
get_power_delivery_req_charging_profile_in_limits,
get_power_delivery_req_charging_profile_not_in_limits_span_over_sa,
get_power_delivery_req_charging_profile_out_of_boundary,
get_v2g_message_power_delivery_req,
get_v2g_message_power_delivery_req_charging_profile_in_boundary_valid,
)


Expand Down Expand Up @@ -360,6 +366,58 @@ async def test_charge_parameter_discovery_res_v2g2_761(self):

assert found_entry_indicating_start_without_delay is True

@pytest.mark.parametrize(
"power_delivery_message,expected_state, expected_response_code",
[
(
get_v2g_message_power_delivery_req_charging_profile_in_boundary_valid(),
CurrentDemand,
ResponseCode.OK,
),
(
get_power_delivery_req_charging_profile_in_boundary_invalid(),
Terminate,
ResponseCode.FAILED_CHARGING_PROFILE_INVALID,
),
(
get_power_delivery_req_charging_profile_in_limits(),
CurrentDemand,
ResponseCode.OK,
),
(
get_power_delivery_req_charging_profile_not_in_limits_span_over_sa(),
Terminate,
ResponseCode.FAILED_CHARGING_PROFILE_INVALID,
),
(
get_power_delivery_req_charging_profile_out_of_boundary(),
Terminate,
ResponseCode.FAILED_CHARGING_PROFILE_INVALID,
),
],
)
async def test_charge_parameter_discovery_req_v2g2_225(
self, power_delivery_message, expected_state, expected_response_code
):
# [V2G2-225] The SECC shall send the negative ResponseCode
# FAILED_ChargingProfileInvalid in
# the PowerDelivery response message if the EVCC sends a ChargingProfile which
# is not adhering to the PMax values of all PMaxScheduleEntry elements according
# to the chosen SAScheduleTuple element in the last ChargeParameterDiscoveryRes
# message sent by the SECC.
self.comm_session.writer = Mock()
self.comm_session.writer.get_extra_info = Mock()

self.comm_session.offered_schedules = get_dummy_sa_schedule()
power_delivery = PowerDelivery(self.comm_session)

await power_delivery.process_message(message=power_delivery_message)
assert power_delivery.next_state is expected_state
assert (
power_delivery.message.body.power_delivery_res.response_code
is expected_response_code
)

async def test_power_delivery_set_hlc_charging(
self,
):
Expand Down
Loading