From c2f37188c8ed7be4ac8dca09ac59cc09010dc37b Mon Sep 17 00:00:00 2001 From: Auke Willem Oosterhoff Date: Wed, 13 Nov 2019 21:39:50 +0100 Subject: [PATCH] Fix validation of payloads containing floats The validation of payloads using jsonschemas could fail when the payload contained a float. This problem is described in this issue: https://github.com/Julian/jsonschema/issues/247 This commit implements a work around for this issue by changing the float parser for certain payloads from `float()` to `decimal.Decimal()`. Fixes: #43 --- ocpp/messages.py | 53 +++++++++++++++++++++++++++++++----------- tests/test_messages.py | 32 +++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 13 deletions(-) diff --git a/ocpp/messages.py b/ocpp/messages.py index 55950ee20..09354ef3a 100644 --- a/ocpp/messages.py +++ b/ocpp/messages.py @@ -2,6 +2,7 @@ also contain some helper functions for packing and unpacking messages. """ import os import json +import decimal from dataclasses import asdict, is_dataclass from jsonschema import validate @@ -61,11 +62,14 @@ def pack(msg): return msg.to_json() -def get_schema(message_type_id, action, ocpp_version): +def get_schema(message_type_id, action, ocpp_version, parse_float=float): """ Read schema from disk and return in. Reads will be cached for performance reasons. + The `parse_float` argument can be used to set the conversion method that + is used to parse floats. It must be a callable taking 1 argument. By + default it is `float()`, but certain schema's require `decimal.Decimal()`. """ if ocpp_version not in ["1.6", "2.0"]: raise ValueError @@ -95,7 +99,7 @@ def get_schema(message_type_id, action, ocpp_version): # Unexpected UTF-8 BOM (decode using utf-8-sig): with open(path, 'r', encoding='utf-8-sig') as f: data = f.read() - _schemas[relative_path] = json.loads(data) + _schemas[relative_path] = json.loads(data, parse_float=parse_float) return _schemas[relative_path] @@ -108,21 +112,44 @@ def validate_payload(message, ocpp_version): "be either 'Call' or 'CallResult'.") try: - schema = get_schema( - message.message_type_id, message.action, ocpp_version - ) + # 3 OCPP 1.6 schedules have fields of type floats. The JSON schema + # defines a certain precision for these fields of 1 decimal. A value of + # 21.4 is valid, whereas a value if 4.11 is not. + # + # The problem is that Python's internal representation of 21.4 might + # have more than 1 decimal. It might be 21.399999999999995. This would + # make the validation fail, although the payload is correct. This is a + # known issue with jsonschemas, see: + # https://github.com/Julian/jsonschema/issues/247 + # + # This issue can be fixed by using a different parser for floats than + # the default one that is used. + # + # Both the schema and the payload must be parsed using the different + # parser for floats. + if ocpp_version == '1.6' and ( + (type(message) == Call and + message.action in ['SetChargingProfile', 'RemoteStartTransaction']) # noqa + or + (type(message) == CallResult and + message.action == ['GetCompositeSchedule']) + ): + schema = get_schema( + message.message_type_id, message.action, + ocpp_version, parse_float=decimal.Decimal + ) + + message.payload = json.loads( + json.dumps(message.payload), parse_float=decimal.Decimal + ) + else: + schema = get_schema( + message.message_type_id, message.action, ocpp_version + ) except (OSError, json.JSONDecodeError) as e: raise ValidationError("Failed to load validation schema for action " f"'{message.action}': {e}") - if message.action in [ - 'RemoteStartTransaction', - 'SetChargingProfile', - 'RequestStartTransaction', - ]: - # todo: special actions - pass - try: validate(message.payload, schema) except SchemaValidationError as e: diff --git a/tests/test_messages.py b/tests/test_messages.py index 7d5d6089a..c4ed1bb62 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -76,6 +76,38 @@ def test_get_schema_with_valid_name(): } +def test_validate_set_charging_profile_payload(): + """" Test if payloads with floats are validated correctly. + + This test uses the value of 21.4, which is internally represented as + 21.39999999999999857891452847979962825775146484375. + You can verify this using `decimal.Decimal(21.4)` + """ + message = Call( + unique_id="1234", + action="SetChargingProfile", + payload={ + 'connectorId': 1, + 'csChargingProfiles': { + 'chargingProfileId': 1, + 'stackLevel': 0, + 'chargingProfilePurpose': 'TxProfile', + 'chargingProfileKind': 'Relative', + 'chargingSchedule': { + 'chargingRateUnit': 'A', + 'chargingSchedulePeriod': [{ + 'startPeriod': 0, + 'limit': 21.4 + }] + }, + 'transactionId': 123456789, + } + } + ) + + validate_payload(message, ocpp_version="1.6") + + def test_get_schema_with_invalid_name(): """ Test if OSError is raised when schema validation file cannnot be found.