From fbd1f2c61c3c87421e6032e28bf611ebaae5d1f2 Mon Sep 17 00:00:00 2001 From: tropxy Date: Sat, 29 Oct 2022 02:21:19 +0100 Subject: [PATCH] enhanced the ev profile profile validation by skipping entries that were already scanned and tested --- iso15118/secc/states/iso15118_2_states.py | 56 +++++++++-- tests/secc/states/test_iso15118_2_states.py | 11 +-- tests/secc/states/test_messages.py | 100 ++++++++++++++++++++ 3 files changed, 152 insertions(+), 15 deletions(-) diff --git a/iso15118/secc/states/iso15118_2_states.py b/iso15118/secc/states/iso15118_2_states.py index 36d3d168..f49b9a0f 100644 --- a/iso15118/secc/states/iso15118_2_states.py +++ b/iso15118/secc/states/iso15118_2_states.py @@ -1619,15 +1619,38 @@ def _is_charging_profile_valid(self, power_delivery_req: PowerDeliveryReq) -> bo # PMaxScheduleEntry_1, ...]. The enumerate will pair an index with # each list entry: (0, PMaxScheduleEntry_0), (1, # PMaxScheduleEntry_1), ... + cached_start_idx: 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 ) - for ( - ev_profile_entry - ) in power_delivery_req.charging_profile.profile_entries: + ev_profile = power_delivery_req.charging_profile + ev_profile_entries = ev_profile.profile_entries + + # 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 += last_ev_running_idx + last_ev_running_idx = 0 + + for (ev_profile_idx, ev_profile_entry) in enumerate( + ev_profile_entries[cached_start_idx:] + ): try: # By getting the next entry/slot, we can know when # the current entry ends @@ -1635,7 +1658,6 @@ def _is_charging_profile_valid(self, power_delivery_req: PowerDeliveryReq) -> bo 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 @@ -1646,12 +1668,32 @@ def _is_charging_profile_valid(self, power_delivery_req: PowerDeliveryReq) -> bo sa_profile_entry_start + sa_profile_entry.time_interval.duration ) + + _is_last_ev_profile = ( + ev_profile_entry.start == ev_profile_entries[-1].start + ) # fmt: off - if sa_profile_entry_start <= ev_profile_entry.start <= sa_profile_entry_end: # noqa - ev_entry_pmax = (ev_profile_entry.max_power.value - * 10 ** ev_profile_entry.max_power.multiplier) # noqa + if sa_profile_entry_start <= 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: + 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 diff --git a/tests/secc/states/test_iso15118_2_states.py b/tests/secc/states/test_iso15118_2_states.py index 3535f7a4..d6867038 100644 --- a/tests/secc/states/test_iso15118_2_states.py +++ b/tests/secc/states/test_iso15118_2_states.py @@ -30,6 +30,7 @@ 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, @@ -393,16 +394,10 @@ async def test_charge_parameter_discovery_req_v2g2_225_out_of_boundary(self): # message sent by the SECC. self.comm_session.writer = Mock() self.comm_session.writer.get_extra_info = Mock() - charge_parameter_discovery = ChargeParameterDiscovery(self.comm_session) - await charge_parameter_discovery.process_message( - message=get_charge_parameter_discovery_req_message_departure_time_one_hour() - ) - assert ( - charge_parameter_discovery.message.body.charge_parameter_discovery_res - is not None - ) + self.comm_session.offered_schedules = get_dummy_sa_schedule() power_delivery = PowerDelivery(self.comm_session) + await power_delivery.process_message( message=get_v2g_message_power_delivery_req_charging_profile_out_of_boundary() # noqa ) diff --git a/tests/secc/states/test_messages.py b/tests/secc/states/test_messages.py index 8b511f13..e05ba3e5 100644 --- a/tests/secc/states/test_messages.py +++ b/tests/secc/states/test_messages.py @@ -264,6 +264,71 @@ def get_v2g_message_power_delivery_req_charging_profile_in_boundary(): ) +def get_dummy_sa_schedule(): + sa_schedule_list: list[SAScheduleTuple] = [] + # PMaxSchedule + p_max_schedule_entry_1 = PMaxScheduleEntry( + p_max=PVPMax(multiplier=0, value=11000, unit=UnitSymbol.WATT), + time_interval=RelativeTimeInterval(start=0), + ) + p_max_schedule_entry_2 = PMaxScheduleEntry( + p_max=PVPMax(multiplier=0, value=10000, unit=UnitSymbol.WATT), + time_interval=RelativeTimeInterval(start=1000), + ) + p_max_schedule_entry_3 = PMaxScheduleEntry( + p_max=PVPMax(multiplier=0, value=8000, unit=UnitSymbol.WATT), + time_interval=RelativeTimeInterval(start=1500), + ) + p_max_schedule_entry_4 = PMaxScheduleEntry( + p_max=PVPMax(multiplier=0, value=10500, unit=UnitSymbol.WATT), + time_interval=RelativeTimeInterval(start=2000), + ) + p_max_schedule_entry_5 = PMaxScheduleEntry( + p_max=PVPMax(multiplier=0, value=9530, unit=UnitSymbol.WATT), + time_interval=RelativeTimeInterval(start=2100), + ) + p_max_schedule_entry_6 = PMaxScheduleEntry( + p_max=PVPMax(multiplier=0, value=10530, unit=UnitSymbol.WATT), + time_interval=RelativeTimeInterval(start=2500), + ) + p_max_schedule_entry_7 = PMaxScheduleEntry( + p_max=PVPMax(multiplier=0, value=8535, unit=UnitSymbol.WATT), + time_interval=RelativeTimeInterval(start=3000), + ) + p_max_schedule_entry_8 = PMaxScheduleEntry( + p_max=PVPMax(multiplier=0, value=8215, unit=UnitSymbol.WATT), + time_interval=RelativeTimeInterval(start=3100), + ) + + p_max_schedule_entry_9 = PMaxScheduleEntry( + p_max=PVPMax(multiplier=0, value=7000, unit=UnitSymbol.WATT), + time_interval=RelativeTimeInterval( + start=3500, + duration=100, + ), + ) + p_max_schedule = PMaxSchedule( + schedule_entries=[ + p_max_schedule_entry_1, + p_max_schedule_entry_2, + p_max_schedule_entry_3, + p_max_schedule_entry_4, + p_max_schedule_entry_5, + p_max_schedule_entry_6, + p_max_schedule_entry_7, + p_max_schedule_entry_8, + p_max_schedule_entry_9, + ] + ) + # Putting the list of SAScheduleTuple entries together + sa_schedule_tuple = SAScheduleTuple( + sa_schedule_tuple_id=1, + p_max_schedule=p_max_schedule, + ) + sa_schedule_list.append(sa_schedule_tuple) + return sa_schedule_list + + def get_v2g_message_power_delivery_req_charging_profile_out_of_boundary(): charging_profile = ChargingProfile( profile_entries=[ @@ -284,6 +349,41 @@ def get_v2g_message_power_delivery_req_charging_profile_out_of_boundary(): ), ProfileEntryDetails( start=2000, + max_power=PVPMax(multiplier=0, value=5000, unit=UnitSymbol.WATT), + max_phases_in_use=3, + ), + ProfileEntryDetails( + start=2100, + max_power=PVPMax(multiplier=0, value=6300, unit=UnitSymbol.WATT), + max_phases_in_use=3, + ), + ProfileEntryDetails( + start=2300, + max_power=PVPMax(multiplier=0, value=2000, unit=UnitSymbol.WATT), + max_phases_in_use=3, + ), + ProfileEntryDetails( + start=2400, + max_power=PVPMax(multiplier=0, value=4000, unit=UnitSymbol.WATT), + max_phases_in_use=3, + ), + ProfileEntryDetails( + start=2450, + max_power=PVPMax(multiplier=0, value=700, unit=UnitSymbol.WATT), + max_phases_in_use=3, + ), + ProfileEntryDetails( + start=2500, + max_power=PVPMax(multiplier=0, value=6999, unit=UnitSymbol.WATT), + max_phases_in_use=3, + ), + ProfileEntryDetails( + start=3000, + max_power=PVPMax(multiplier=0, value=1400, unit=UnitSymbol.WATT), + max_phases_in_use=3, + ), + ProfileEntryDetails( + start=3100, max_power=PVPMax(multiplier=0, value=8000, unit=UnitSymbol.WATT), max_phases_in_use=3, ),