Skip to content

Commit

Permalink
wrong message parameters will return FAILED_WRONG_CHARGE_PARAMETER
Browse files Browse the repository at this point in the history
Value error has changed to ValidationError
  • Loading branch information
ikaratass committed Aug 26, 2022
1 parent 0da97d7 commit 0c75211
Show file tree
Hide file tree
Showing 10 changed files with 162 additions and 43 deletions.
30 changes: 23 additions & 7 deletions iso15118/secc/states/secc_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,10 +294,13 @@ def stop_state_machine(
V2GMessageV2,
V2GMessageV20,
V2GMessageDINSPEC,
None,
],
response_code: Union[
ResponseCodeSAP, ResponseCodeV2, ResponseCodeV20, ResponseCodeDINSPEC
],
message_body_type: Optional[type] = None,
namespace: Optional[Namespace] = None,
):
"""
In case the processing of a message from the EVCC fails, the SECC needs
Expand All @@ -317,31 +320,44 @@ def stop_state_machine(

if isinstance(faulty_request, V2GMessageV2):
msg_type = get_msg_type(str(faulty_request))
msg_namespace = Namespace.ISO_V2_MSG_DEF
elif isinstance(faulty_request, V2GMessageDINSPEC):
msg_type = get_msg_type_dinspec(str(faulty_request))
msg_namespace = Namespace.DIN_MSG_DEF
elif isinstance(faulty_request, V2GMessageV20):
msg_type = type(faulty_request)
msg_namespace = Namespace.ISO_V20_BASE
elif isinstance(faulty_request, SupportedAppProtocolReq):
msg_namespace = Namespace.SAP
msg_type = faulty_request
else:
msg_type = message_body_type
msg_namespace = namespace

if msg_namespace == Namespace.ISO_V2_MSG_DEF:
error_res = self.comm_session.failed_responses_isov2.get(msg_type)
error_res.response_code = response_code
self.create_next_message(Terminate, error_res, 0, Namespace.ISO_V2_MSG_DEF)
elif isinstance(faulty_request, V2GMessageDINSPEC):
msg_type = get_msg_type_dinspec(str(faulty_request))
elif msg_namespace == Namespace.DIN_MSG_DEF:
error_res = self.comm_session.failed_responses_din_spec.get(msg_type)
error_res.response_code = response_code
self.create_next_message(Terminate, error_res, 0, Namespace.DIN_MSG_DEF)
# Here we could have been more specific and check if it is a V2GRequestV20,
# but to be consistent with the other if clauses and since there is no negative
# consequences in the behavior of the code, we check if it is a V2GMessageV20
elif isinstance(faulty_request, V2GMessageV20):
elif msg_namespace.startswith(Namespace.ISO_V20_BASE):
(
error_res,
namespace,
payload_type,
) = self.comm_session.failed_responses_isov20.get(type(faulty_request))
) = self.comm_session.failed_responses_isov20.get(msg_type)
# As the Header in the case of -20 is part of the -20 message payload,
# we need to set the session id of the the current session to it
# we need to set the session id of the current session to it
error_res.header.session_id = self.comm_session.session_id
error_res.response_code = response_code
self.create_next_message(Terminate, error_res, 0, namespace, payload_type)
elif isinstance(faulty_request, SupportedAppProtocolReq):
elif msg_namespace == Namespace.SAP:
error_res = SupportedAppProtocolRes(response_code=response_code)

self.create_next_message(Terminate, error_res, 0, Namespace.SAP)
else:
# Should actually never happen
Expand Down
10 changes: 10 additions & 0 deletions iso15118/shared/comm_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
FaultyStateImplementationError,
InvalidV2GTPMessageError,
MessageProcessingError,
V2GMessageValidationError,
)
from iso15118.shared.exi_codec import EXI
from iso15118.shared.messages.app_protocol import (
Expand Down Expand Up @@ -204,6 +205,15 @@ async def process_message(self, message: bytes):
f"{self.get_exi_ns(v2gtp_msg.payload_type).value}"
)

except V2GMessageValidationError as exc:
self.comm_session.current_state.stop_state_machine(
exc.reason,
None,
exc.response_code,
exc.message,
self.get_exi_ns(v2gtp_msg.payload_type),
)
return
except EXIDecodingError as exc:
logger.exception(f"{exc}")
raise exc
Expand Down
12 changes: 12 additions & 0 deletions iso15118/shared/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from typing import Any

from iso15118.shared.messages.iso15118_2.datatypes import ResponseCode


class InvalidInterfaceError(Exception):
"""
Expand Down Expand Up @@ -235,3 +237,13 @@ def __init__(self):
self,
"No OCSP server entry in Authority Information Access extension field.",
)


class V2GMessageValidationError(Exception):
"""Is thrown if message validation is failed"""

def __init__(self, reason: str, response_code: ResponseCode, message: Any):
Exception.__init__(self)
self.reason = reason
self.response_code = response_code
self.message = message
33 changes: 29 additions & 4 deletions iso15118/shared/exi_codec.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,23 @@

from pydantic import ValidationError

from iso15118.shared.exceptions import EXIDecodingError, EXIEncodingError
from iso15118.shared.exceptions import (
EXIDecodingError,
EXIEncodingError,
V2GMessageValidationError,
)
from iso15118.shared.exificient_exi_codec import ExificientEXICodec
from iso15118.shared.iexi_codec import IEXICodec
from iso15118.shared.messages import BaseModel
from iso15118.shared.messages.app_protocol import (
SupportedAppProtocolReq,
SupportedAppProtocolRes,
)
from iso15118.shared.messages.din_spec.body import get_msg_type as get_msg_type_dinspec
from iso15118.shared.messages.din_spec.msgdef import V2GMessage as V2GMessageDINSPEC
from iso15118.shared.messages.enums import Namespace
from iso15118.shared.messages.iso15118_2.body import get_msg_type
from iso15118.shared.messages.iso15118_2.datatypes import ResponseCode
from iso15118.shared.messages.iso15118_2.msgdef import V2GMessage as V2GMessageV2
from iso15118.shared.messages.iso15118_20.ac import (
ACChargeLoopReq,
Expand Down Expand Up @@ -379,10 +386,28 @@ def from_exi(

raise EXIDecodingError("Can't identify protocol to use for decoding")
except ValidationError as exc:
raise EXIDecodingError(
f"Error parsing the decoded EXI into a Pydantic class: {exc}. "
f"\n\nDecoded dict: {decoded_dict}"
if namespace == Namespace.ISO_V2_MSG_DEF:
msg_name = next(iter(decoded_dict["V2G_Message"]["Body"]))
msg_type = get_msg_type(msg_name)
elif namespace == Namespace.DIN_MSG_DEF:
msg_name = next(iter(decoded_dict["V2G_Message"]["Body"]))
msg_type = get_msg_type_dinspec(msg_name)
elif namespace.startswith(Namespace.ISO_V20_BASE):
msg_type = msg_class
elif namespace == Namespace.SAP:
if "supportedAppProtocolReq" in decoded_dict:
msg_type = SupportedAppProtocolReq
elif "supportedAppProtocolRes" in decoded_dict:
msg_type = SupportedAppProtocolRes

raise V2GMessageValidationError(
f"Validation error: {exc}. \n\nDecoded dict: " f"{decoded_dict}",
ResponseCode.FAILED,
msg_type,
) from exc

except V2GMessageValidationError as exc:
raise exc
except EXIDecodingError as exc:
raise EXIDecodingError(
f"EXI decoding error: {exc}. \n\nDecoded dict: " f"{decoded_dict}"
Expand Down
21 changes: 11 additions & 10 deletions iso15118/shared/messages/din_spec/body.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from pydantic import Field, root_validator, validator

from iso15118.shared.exceptions import V2GMessageValidationError
from iso15118.shared.messages import BaseModel
from iso15118.shared.messages.datatypes import (
DCEVSEChargeParameter,
Expand Down Expand Up @@ -234,11 +235,6 @@ def validate_requested_energy_mode(cls, values):
Pydantic validators are "class methods",
see https://pydantic-docs.helpmanual.io/usage/validators/
TODO We need to actually send FAILED_WrongChargeParameter or
FAILED_WrongEnergyTransferMode if the wrong parameter set is
provided, one or multiple parameters can not be interpreted
(see [V2G2-477]). Need to check how to not just bury
that information in a pydantic validation error.
"""
# pylint: disable=no-self-argument
# pylint: disable=no-self-use
Expand All @@ -249,15 +245,20 @@ def validate_requested_energy_mode(cls, values):
values.get("dc_ev_charge_parameter"),
)
if requested_energy_mode not in ("DC_extended", "DC_core"):
raise ValueError(
f"Wrong energy transfer mode transfer mode {requested_energy_mode}"
raise V2GMessageValidationError(
f"[V2G2-476] Wrong energy transfer mode transfer mode "
f"{requested_energy_mode}",
ResponseCode.FAILED_WRONG_ENERGY_TRANSFER_MODE,
cls,
)
if ("AC_" in requested_energy_mode and dc_params) or (
"DC_" in requested_energy_mode and ac_params
):
raise ValueError(
"Wrong charge parameters for requested energy "
f"transfer mode {requested_energy_mode}"
raise V2GMessageValidationError(
"[V2G2-477] Wrong charge parameters for requested energy "
f"transfer mode {requested_energy_mode}",
ResponseCode.FAILED_WRONG_CHARGE_PARAMETER,
cls,
)
return values

Expand Down
16 changes: 7 additions & 9 deletions iso15118/shared/messages/iso15118_2/body.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from pydantic import Field, root_validator, validator

from iso15118.shared.exceptions import V2GMessageValidationError
from iso15118.shared.messages import BaseModel
from iso15118.shared.messages.datatypes import (
DCEVSEChargeParameter,
Expand Down Expand Up @@ -251,15 +252,9 @@ def requested_energy_mode_must_match_charge_parameter(cls, values):
Pydantic validators are "class methods",
see https://pydantic-docs.helpmanual.io/usage/validators/
TODO We need to actually send FAILED_WrongChargeParameter or
FAILED_WrongEnergyTransferMode if the wrong parameter set is
provided, one or multiple parameters can not be interpreted
(see [V2G2-477]). Need to check how to not just bury
that information in a pydantic validation error.
"""
# pylint: disable=no-self-argument
# pylint: disable=no-self-use

requested_energy_mode, ac_params, dc_params = (
values.get("requested_energy_mode"),
values.get("ac_ev_charge_parameter"),
Expand All @@ -268,10 +263,13 @@ def requested_energy_mode_must_match_charge_parameter(cls, values):
if ("AC_" in requested_energy_mode and dc_params) or (
"DC_" in requested_energy_mode and ac_params
):
raise ValueError(
"Wrong charge parameters for requested energy "
f"transfer mode {requested_energy_mode}"
raise V2GMessageValidationError(
"[V2G2-477] Wrong charge parameters for requested energy "
f"transfer mode {requested_energy_mode}",
ResponseCode.FAILED_WRONG_CHARGE_PARAMETER,
cls,
)

return values


Expand Down
1 change: 0 additions & 1 deletion iso15118/shared/messages/iso15118_2/msgdef.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
(or class) that matches the definitions in the XSD schema, including the XSD
element names by using the 'alias' attribute.
"""

from pydantic import Field

from iso15118.shared.messages import BaseModel
Expand Down
31 changes: 20 additions & 11 deletions iso15118/shared/messages/iso15118_20/ac.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from pydantic import Field, root_validator

from iso15118.shared.exceptions import V2GMessageValidationError
from iso15118.shared.messages import BaseModel
from iso15118.shared.messages.iso15118_20.common_types import (
ChargeLoopReq,
Expand All @@ -22,6 +23,7 @@
DynamicChargeLoopReqParams,
DynamicChargeLoopResParams,
RationalNumber,
ResponseCode,
ScheduledChargeLoopReqParams,
ScheduledChargeLoopResParams,
)
Expand Down Expand Up @@ -335,17 +337,24 @@ def either_ac_or_ac_bpt_params(cls, values):
"""
# pylint: disable=no-self-argument
# pylint: disable=no-self-use
if one_field_must_be_set(
[
"ac_params",
"AC_CPDReqEnergyTransferMode",
"bpt_ac_params",
"BPT_AC_CPDReqEnergyTransferMode",
],
values,
True,
):
return values
try:
if one_field_must_be_set(
[
"ac_params",
"AC_CPDReqEnergyTransferMode",
"bpt_ac_params",
"BPT_AC_CPDReqEnergyTransferMode",
],
values,
True,
):
return values
except ValueError as exc:
raise V2GMessageValidationError(
str(exc),
ResponseCode.FAILED_WRONG_CHARGE_PARAMETER,
ChargeParameterDiscoveryReq,
)

def __str__(self):
# The XSD-conform name
Expand Down
2 changes: 1 addition & 1 deletion iso15118/shared/messages/iso15118_20/common_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class V2GMessage(BaseModel, ABC):
"""See section 8.3 in ISO 15118-20
This class model follows the schemas, where the
V2GMessage type is defined, in the V2G_CI_CommonTypes.xsd schema.
This type is the base of all messages and contains the the Header
This type is the base of all messages and contains the Header
This is a tiny but quite important difference in respect to ISO 15118-2 payload
structure, where the header is not included within each Request and Response message
Expand Down
49 changes: 49 additions & 0 deletions tests/secc/messages/15118_2_invalid_messages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import json
from dataclasses import dataclass

from iso15118.shared.exceptions import V2GMessageValidationError
from iso15118.shared.messages.iso15118_2.body import Body, ChargeParameterDiscoveryReq
from iso15118.shared.messages.iso15118_2.datatypes import ResponseCode
from iso15118.shared.messages.iso15118_2.msgdef import V2GMessage


@dataclass
class InvalidV2GMessage:
msg: str
msg_body: Body
response_code: ResponseCode


invalid_v2g_2_messages = [
(
InvalidV2GMessage(
(
# [V2G2-477]
# Parameters are not compatible with RequestedEnergyTransferMode
'{"V2G_Message":{"Header":{"SessionID":"82DBA3A44ED6E5B9"},"Body":'
'{"ChargeParameterDiscoveryReq":{"MaxEntriesSAScheduleTuple":16,'
'"RequestedEnergyTransferMode":"AC_three_phase_core",'
'"DC_EVChargeParameter":'
'{"DepartureTime":0,"DC_EVStatus":{"EVReady":false,"EVErrorCode":'
'"NO_ERROR","EVRESSSOC":20},"EVMaximumCurrentLimit":{"Multiplier":'
'1,"Unit":"A","Value":8},"EVMaximumPowerLimit":{"Multiplier":3,'
'"Unit":"W","Value":29},"EVMaximumVoltageLimit":{"Multiplier":2,'
'"Unit":"V","Value":5},"EVEnergyCapacity":{"Multiplier":3,"Unit":'
'"Wh","Value":200},"EVEnergyRequest":{"Multiplier":3,"Unit":"Wh",'
'"Value":160},"FullSOC":99,"BulkSOC":80}}}}}'
),
ChargeParameterDiscoveryReq,
ResponseCode.FAILED_WRONG_CHARGE_PARAMETER,
)
),
]


def test_invalid_v2g_2_messages():
for message in invalid_v2g_2_messages:
try:
invalid_msg = json.loads(message.msg)
V2GMessage.parse_obj(invalid_msg["V2G_Message"])
except V2GMessageValidationError as exc:
assert exc.message is message.msg_body
assert exc.response_code is message.response_code

0 comments on commit 0c75211

Please sign in to comment.