Skip to content

Commit 4a4e093

Browse files
authored
[DVI-509] OCPP Validation logging (#384)
In case an OCPP validation fails TypeConstraint or Format, we should log which message caused the validation to fail. We noticed a reproducible TypeConstraintViolation as a reaction to an OCPP message from Etrel Chargers. A (OCPP) response from the Etrel chargers leads to a a) problem in data type conversion/parsing, b) a debug message that a transmitted string may be too long.
1 parent 627d87c commit 4a4e093

File tree

2 files changed

+64
-10
lines changed

2 files changed

+64
-10
lines changed

ocpp/messages.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
""" Module containing classes that model the several OCPP messages types. It
22
also contain some helper functions for packing and unpacking messages. """
3+
from __future__ import annotations
4+
35
import decimal
46
import json
57
import os
68
from dataclasses import asdict, is_dataclass
7-
from typing import Callable, Dict
9+
from typing import Callable, Dict, Union
810

911
from jsonschema import Draft4Validator
1012
from jsonschema import _validators as SchemaValidators
@@ -75,7 +77,9 @@ def unpack(msg):
7577
try:
7678
msg = json.loads(msg)
7779
except json.JSONDecodeError:
78-
raise FormatViolationError(details={"cause": "Message is not valid JSON"})
80+
raise FormatViolationError(
81+
details={"cause": "Message is not valid JSON", "ocpp_message": msg}
82+
)
7983

8084
if not isinstance(msg, list):
8185
raise ProtocolError(
@@ -100,7 +104,7 @@ def unpack(msg):
100104
raise ProtocolError(details={"cause": "Message is missing elements."})
101105

102106
raise PropertyConstraintViolationError(
103-
details={f"MessageTypeId '{msg[0]}' isn't valid"}
107+
details={"cause": f"MessageTypeId '{msg[0]}' isn't valid"}
104108
)
105109

106110

@@ -160,7 +164,7 @@ def get_validator(
160164
return _validators[cache_key]
161165

162166

163-
def validate_payload(message, ocpp_version):
167+
def validate_payload(message: Union[Call, CallResult], ocpp_version: str) -> None:
164168
"""Validate the payload of the message using JSON schemas."""
165169
if type(message) not in [Call, CallResult]:
166170
raise ValidationError(
@@ -217,18 +221,25 @@ def validate_payload(message, ocpp_version):
217221
validator.validate(message.payload)
218222
except SchemaValidationError as e:
219223
if e.validator == SchemaValidators.type.__name__:
220-
raise TypeConstraintViolationError(details={"cause": e.message})
224+
raise TypeConstraintViolationError(
225+
details={"cause": e.message, "ocpp_message": message}
226+
)
221227
elif e.validator == SchemaValidators.additionalProperties.__name__:
222-
raise FormatViolationError(details={"cause": e.message})
228+
raise FormatViolationError(
229+
details={"cause": e.message, "ocpp_message": message}
230+
)
223231
elif e.validator == SchemaValidators.required.__name__:
224232
raise ProtocolError(details={"cause": e.message})
225233
elif e.validator == "maxLength":
226-
raise TypeConstraintViolationError(details={"cause": e.message}) from e
234+
raise TypeConstraintViolationError(
235+
details={"cause": e.message, "ocpp_message": message}
236+
) from e
227237
else:
228238
raise FormatViolationError(
229239
details={
230240
"cause": f"Payload '{message.payload} for action "
231-
f"'{message.action}' is not valid: {e}"
241+
f"'{message.action}' is not valid: {e}",
242+
"ocpp_message": message,
232243
}
233244
)
234245

@@ -317,7 +328,7 @@ def __repr__(self):
317328
class CallResult:
318329
"""
319330
A CallResult is a message indicating that a Call has been handled
320-
succesfully.
331+
successfully.
321332
322333
From the specification:
323334

tests/test_exceptions.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
from ocpp.exceptions import ProtocolError
1+
import pytest
2+
3+
from ocpp.exceptions import (
4+
FormatViolationError,
5+
ProtocolError,
6+
TypeConstraintViolationError,
7+
)
8+
from ocpp.messages import Call, validate_payload
29

310

411
def test_exception_with_error_details():
@@ -13,3 +20,39 @@ def test_exception_without_error_details():
1320

1421
assert exception.description == "Payload for Action is incomplete"
1522
assert exception.details == {}
23+
24+
25+
def test_exception_show_triggered_message_type_constraint():
26+
# chargePointVendor should be a string, not an integer,
27+
# so this should raise a TypeConstraintViolationError
28+
call = Call(
29+
unique_id=123456,
30+
action="BootNotification",
31+
payload={"chargePointVendor": 1, "chargePointModel": "SingleSocketCharger"},
32+
)
33+
ocpp_message = (
34+
"'ocpp_message': <Call - unique_id=123456, action=BootNotification, "
35+
"payload={'chargePointVendor': 1, 'chargePointModel': 'SingleSocketCharger'}"
36+
)
37+
38+
with pytest.raises(TypeConstraintViolationError) as exception_info:
39+
validate_payload(call, "1.6")
40+
assert ocpp_message in str(exception_info.value)
41+
42+
43+
def test_exception_show_triggered_message_format():
44+
# The payload is syntactically incorrect, should trigger a FormatViolationError
45+
call = Call(
46+
unique_id=123457,
47+
action="BootNotification",
48+
payload={"syntactically": "incorrect"},
49+
)
50+
51+
ocpp_message = (
52+
"'ocpp_message': <Call - unique_id=123457, action=BootNotification, "
53+
"payload={'syntactically': 'incorrect'}"
54+
)
55+
56+
with pytest.raises(FormatViolationError) as exception_info:
57+
validate_payload(call, "1.6")
58+
assert ocpp_message in str(exception_info.value)

0 commit comments

Comments
 (0)