Skip to content

Commit b06c6ca

Browse files
authored
Add arg skip_schema_validation to @on()
* Add arg `skip_schema_validation` to @on() The argument can be used to skip validation of a request and response. It defaults to `False`, so by default validation is enabled. The reasoning for this change is that TMH has charge points which use measurands inside MeterValues messages which are not compliant with the OCPP 1.6 specification. Therefore the validation of these messages fail. With this new argument validation of certain messages can be disabled. Fixes: #54
1 parent 3499b98 commit b06c6ca

File tree

4 files changed

+67
-12
lines changed

4 files changed

+67
-12
lines changed

ocpp/charge_point.py

+6-7
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,6 @@ async def route_message(self, raw_msg):
140140
return
141141

142142
if msg.message_type_id == MessageType.Call:
143-
# Call's can be validated right away because the 'action' is know.
144-
# The 'action' is required to get the correct schema.
145-
#
146-
# CallResult's don't have an action field. The action must be
147-
# deducted from corresponding Call.
148-
validate_payload(msg, self._ocpp_version)
149143
await self._handle_call(msg)
150144
elif msg.message_type_id in \
151145
[MessageType.CallResult, MessageType.CallError]:
@@ -168,6 +162,9 @@ async def _handle_call(self, msg):
168162
raise NotImplementedError(f"No handler for '{msg.action}' "
169163
"registered.")
170164

165+
if not handlers.get('_skip_schema_validation', False):
166+
validate_payload(msg, self._ocpp_version)
167+
171168
# OCPP uses camelCase for the keys in the payload. It's more pythonic
172169
# to use snake_case for keyword arguments. Therefore the keys must be
173170
# 'translated'. Some examples:
@@ -205,7 +202,9 @@ async def _handle_call(self, msg):
205202
camel_case_payload = snake_to_camel_case(response_payload)
206203

207204
response = msg.create_call_result(camel_case_payload)
208-
validate_payload(response, self._ocpp_version)
205+
206+
if not handlers.get('_skip_schema_validation', False):
207+
validate_payload(response, self._ocpp_version)
209208

210209
await self._send(response.to_json())
211210

ocpp/routing.py

+18-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import functools
22

33

4-
def on(action):
4+
def on(action, *, skip_schema_validation=False):
55
""" Function decorator to mark function as handler for specific action.
66
77
This hook's argument are the data that is in the payload for the specific
@@ -20,13 +20,18 @@ def on_boot_notification(charge_point_model, charge_point_vendor, **kwargs): # n
2020
status="Accepted",
2121
)
2222
23+
The decorator takes an optional argument `skip_schema_validation` which
24+
defaults to False. Setting this argument to `True` will disable schema
25+
validation of the request and the response of the specific route.
26+
2327
"""
2428
def decorator(func):
2529
@functools.wraps(func)
2630
def inner(*args, **kwargs):
2731
return func(*args, **kwargs)
2832

2933
inner._on_action = action
34+
inner._skip_schema_validation = skip_schema_validation
3035
return inner
3136
return decorator
3237

@@ -79,20 +84,29 @@ def after_boot_notification(self, *args, **kwargs):
7984
Action.BootNotification: {
8085
'_on_action': <reference to 'on_boot_notification'>,
8186
'_after_action': <reference to 'after_boot_notification'>,
87+
'_skip_schema_validation': False,
8288
},
8389
}
8490
8591
"""
8692
routes = {}
8793
for attr_name in dir(obj):
8894
attr = getattr(obj, attr_name)
89-
for hook in ['_on_action', '_after_action']:
95+
for option in ['_on_action', '_after_action']:
9096
try:
91-
action = getattr(attr, hook)
97+
action = getattr(attr, option)
9298

9399
if action not in routes:
94100
routes[action] = {}
95-
routes[action][hook] = attr
101+
102+
# Routes decorated with the `@on()` decorator can be configured
103+
# to skip validation of the input and output. For more info see
104+
# the docstring of `on()`.
105+
if option == '_on_action':
106+
routes[action]['_skip_schema_validation'] = \
107+
getattr(attr, '_skip_schema_validation', False)
108+
109+
routes[action][option] = attr
96110

97111
except AttributeError:
98112
continue

tests/test_routing.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def test_create_route_map():
99
1010
"""
1111
class ChargePoint:
12-
@on(Action.Heartbeat)
12+
@on(Action.Heartbeat, skip_schema_validation=True)
1313
def on_heartbeat(self):
1414
pass
1515

@@ -31,8 +31,10 @@ def undecorated(self):
3131
Action.Heartbeat: {
3232
'_on_action': cp.on_heartbeat,
3333
'_after_action': cp.after_heartbeat,
34+
'_skip_schema_validation': True,
3435
},
3536
Action.MeterValues: {
3637
'_on_action': cp.meter_values,
38+
'_skip_schema_validation': False,
3739
},
3840
}

tests/v16/test_v16_charge_point.py

+40
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22
import pytest
33
import asyncio
4+
from unittest import mock
45

56
from ocpp.exceptions import NotImplementedError
67
from ocpp.routing import on, after, create_route_map
@@ -52,6 +53,45 @@ def after_boot_notification(charge_point_model, charge_point_vendor,
5253
)
5354

5455

56+
@pytest.mark.asyncio
57+
async def test_route_message_without_validation(base_central_system):
58+
@on(Action.BootNotification, skip_schema_validation=True)
59+
def on_boot_notification(**kwargs): # noqa
60+
assert kwargs['firmware_version'] == "#1:3.4.0-2990#N:217H;1.0-223"
61+
62+
return call_result.BootNotificationPayload(
63+
current_time='2018-05-29T17:37:05.495259',
64+
interval=350,
65+
# 'Yolo' is not a valid value for for field status.
66+
status='Yolo',
67+
)
68+
69+
setattr(base_central_system, 'on_boot_notification', on_boot_notification)
70+
base_central_system.route_map = create_route_map(base_central_system)
71+
72+
await base_central_system.route_message(json.dumps([
73+
2,
74+
1,
75+
"BootNotification",
76+
{
77+
# The payload is missing the required fields 'chargepointVendor'
78+
# and 'chargePointModel'.
79+
"firmwareVersion": "#1:3.4.0-2990#N:217H;1.0-223"
80+
}
81+
]))
82+
83+
base_central_system._connection.send.call_args == \
84+
mock.call(json.dumps([
85+
3,
86+
"1",
87+
{
88+
'currentTime': '2018-05-29T17:37:05.495259',
89+
'interval': 350,
90+
'status': 'Yolo',
91+
}
92+
]))
93+
94+
5595
@pytest.mark.asyncio
5696
async def test_route_message_with_no_route(base_central_system,
5797
heartbeat_call):

0 commit comments

Comments
 (0)