From 954a16f8aed499a2d8cf2f3f9af157d00ccf6929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hasan=20H=C3=BCseyin=20Pay?= Date: Sun, 21 Apr 2024 00:07:01 +0300 Subject: [PATCH 1/4] fix typo for InternalError in exceptions.py --- ocpp/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ocpp/exceptions.py b/ocpp/exceptions.py index 867b6a793..c678442b9 100644 --- a/ocpp/exceptions.py +++ b/ocpp/exceptions.py @@ -49,7 +49,7 @@ class InternalError(OCPPError): code = "InternalError" default_description = ( "An internal error occurred and the receiver was " - "able to process the requested Action successfully" + "not able to process the requested Action successfully" ) From 50691567beecb2e190396fed9143c510c27545dc Mon Sep 17 00:00:00 2001 From: Chad <34003459+mdwcrft@users.noreply.github.com> Date: Wed, 20 Nov 2024 08:01:31 +0000 Subject: [PATCH 2/4] Add issue templates (#679) Adds issue templates, contributes to https://github.com/mobilityhouse/ocpp/issues/671 --------- Co-authored-by: Mohit Jain --- .../ISSUE_TEMPLATE/documentation_request.md | 11 ++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/documentation_request.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/documentation_request.md b/.github/ISSUE_TEMPLATE/documentation_request.md new file mode 100644 index 000000000..73ade80a5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation_request.md @@ -0,0 +1,11 @@ +--- +name: Documentation request +about: Suggest documentation for this project +title: '' +labels: documentation +assignees: '' + +--- + +**Describe the documentation you'd like** +A clear and concise description of what you'd want documented. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..11fc491ef --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From 92b94000e6f3a253181796031b8f829b030e2323 Mon Sep 17 00:00:00 2001 From: Wafa Yahyaoui <143813203+wafa-yah@users.noreply.github.com> Date: Wed, 4 Dec 2024 17:12:47 +0100 Subject: [PATCH 3/4] Update readme - ocpp 2 edition 3 is supported (#686) From the readme in the ocpp 2.0.1 edition 3 zipfile https://openchargealliance.org/my-oca/ocpp/: For historical reasons not all documents have the same release date. OCPP 2.0.1 Edition 3 is the same as OCPP 2.0.1 Edition 2 including all errata up and until OCPP-2.0.1_edition2_errata_2024-04 (2024-04-30). The errata do not affect any schemas of OCPP messages. The errata do contain changes to requirements or even new requirements, but only in cases where a requirement contains an obvious error and would not or could not be implemented literally. New requirements were only added when they were already implicitly there. No code changes needed to library then => Updated readme to mention edition 3 support. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index aa40c0726..9832ff545 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,7 @@ OCPP ---- Python package implementing the JSON version of the Open Charge Point Protocol -(OCPP). Currently OCPP 1.6 (errata v4), OCPP 2.0.1 (Edition 2 FINAL, 2022-12-15) +(OCPP). Currently OCPP 1.6 (errata v4), OCPP 2.0.1 (Edition 2 FINAL, 2022-12-15 and Edition 3 errata 2024-11) are supported. You can find the documentation on `rtd`_. From 0dc23fb589af2cd1eb2566028e22f35af8f3475e Mon Sep 17 00:00:00 2001 From: astrand Date: Sat, 7 Dec 2024 06:30:11 +0100 Subject: [PATCH 4/4] Disable threads if messages.ASYNC_VALIDATION = False (#678) See discussion at https://github.com/mobilityhouse/ocpp/pull/676 --------- Co-authored-by: Peter Astrand Co-authored-by: Patrick Roelke --- ocpp/charge_point.py | 17 +++++---------- ocpp/messages.py | 15 ++++++++++++- tests/test_exceptions.py | 6 +++--- tests/test_messages.py | 46 ++++++++++++++++++++++++++++++---------- 4 files changed, 57 insertions(+), 27 deletions(-) diff --git a/ocpp/charge_point.py b/ocpp/charge_point.py index 7314804b1..0f3ceacff 100644 --- a/ocpp/charge_point.py +++ b/ocpp/charge_point.py @@ -295,9 +295,8 @@ async def _handle_call(self, msg): return if not handlers.get("_skip_schema_validation", False): - await asyncio.get_event_loop().run_in_executor( - None, validate_payload, msg, self._ocpp_version - ) + await validate_payload(msg, self._ocpp_version) + # OCPP uses camelCase for the keys in the payload. It's more pythonic # to use snake_case for keyword arguments. Therefore the keys must be # 'translated'. Some examples: @@ -344,9 +343,7 @@ async def _handle_call(self, msg): response = msg.create_call_result(camel_case_payload) if not handlers.get("_skip_schema_validation", False): - await asyncio.get_event_loop().run_in_executor( - None, validate_payload, response, self._ocpp_version - ) + await validate_payload(response, self._ocpp_version) await self._send(response.to_json()) @@ -415,9 +412,7 @@ async def call( ) if not skip_schema_validation: - await asyncio.get_event_loop().run_in_executor( - None, validate_payload, call, self._ocpp_version - ) + await validate_payload(call, self._ocpp_version) # Use a lock to prevent make sure that only 1 message can be send at a # a time. @@ -440,9 +435,7 @@ async def call( raise response.to_exception() elif not skip_schema_validation: response.action = call.action - await asyncio.get_event_loop().run_in_executor( - None, validate_payload, response, self._ocpp_version - ) + await validate_payload(response, self._ocpp_version) snake_case_payload = camel_to_snake_case(response.payload) # Create the correct Payload instance based on the received payload. If diff --git a/ocpp/messages.py b/ocpp/messages.py index 3e82ff32d..ac70419df 100644 --- a/ocpp/messages.py +++ b/ocpp/messages.py @@ -1,7 +1,9 @@ """ Module containing classes that model the several OCPP messages types. It also contain some helper functions for packing and unpacking messages. """ + from __future__ import annotations +import asyncio import decimal import json import os @@ -24,6 +26,8 @@ _validators: Dict[str, Draft4Validator] = {} +ASYNC_VALIDATION = True + class _DecimalEncoder(json.JSONEncoder): """Encode values of type `decimal.Decimal` using 1 decimal point. @@ -169,8 +173,17 @@ def get_validator( return _validators[cache_key] -def validate_payload(message: Union[Call, CallResult], ocpp_version: str) -> None: +async def validate_payload(message: Union[Call, CallResult], ocpp_version: str) -> None: """Validate the payload of the message using JSON schemas.""" + if ASYNC_VALIDATION: + await asyncio.get_event_loop().run_in_executor( + None, _validate_payload, message, ocpp_version + ) + else: + _validate_payload(message, ocpp_version) + + +def _validate_payload(message: Union[Call, CallResult], ocpp_version: str) -> None: if type(message) not in [Call, CallResult]: raise ValidationError( "Payload can't be validated because message " diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 35c3c4ebc..59beb5fa2 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -5,7 +5,7 @@ ProtocolError, TypeConstraintViolationError, ) -from ocpp.messages import Call, validate_payload +from ocpp.messages import Call, _validate_payload def test_exception_with_error_details(): @@ -36,7 +36,7 @@ def test_exception_show_triggered_message_type_constraint(): ) with pytest.raises(TypeConstraintViolationError) as exception_info: - validate_payload(call, "1.6") + _validate_payload(call, "1.6") assert ocpp_message in str(exception_info.value) @@ -54,5 +54,5 @@ def test_exception_show_triggered_message_format(): ) with pytest.raises(FormatViolationError) as exception_info: - validate_payload(call, "1.6") + _validate_payload(call, "1.6") assert ocpp_message in str(exception_info.value) diff --git a/tests/test_messages.py b/tests/test_messages.py index 4028a1e3f..343691aa3 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -1,11 +1,13 @@ import decimal import json +import threading from datetime import datetime import pytest from hypothesis import given from hypothesis.strategies import binary +import ocpp from ocpp.exceptions import ( FormatViolationError, NotImplementedError, @@ -21,6 +23,7 @@ CallResult, MessageType, _DecimalEncoder, + _validate_payload, _validators, get_validator, pack, @@ -137,7 +140,7 @@ def test_validate_set_charging_profile_payload(): }, ) - validate_payload(message, ocpp_version="1.6") + _validate_payload(message, ocpp_version="1.6") def test_validate_get_composite_profile_payload(): @@ -162,7 +165,7 @@ def test_validate_get_composite_profile_payload(): }, ) - validate_payload(message, ocpp_version="1.6") + _validate_payload(message, ocpp_version="1.6") @pytest.mark.parametrize("ocpp_version", ["1.6", "2.0.1"]) @@ -177,7 +180,7 @@ def test_validate_payload_with_valid_payload(ocpp_version): payload={"currentTime": datetime.now().isoformat()}, ) - validate_payload(message, ocpp_version=ocpp_version) + _validate_payload(message, ocpp_version=ocpp_version) def test_validate_payload_with_invalid_additional_properties_payload(): @@ -192,7 +195,7 @@ def test_validate_payload_with_invalid_additional_properties_payload(): ) with pytest.raises(FormatViolationError): - validate_payload(message, ocpp_version="1.6") + _validate_payload(message, ocpp_version="1.6") def test_validate_payload_with_invalid_type_payload(): @@ -212,7 +215,7 @@ def test_validate_payload_with_invalid_type_payload(): ) with pytest.raises(TypeConstraintViolationError): - validate_payload(message, ocpp_version="1.6") + _validate_payload(message, ocpp_version="1.6") def test_validate_payload_with_invalid_missing_property_payload(): @@ -232,7 +235,7 @@ def test_validate_payload_with_invalid_missing_property_payload(): ) with pytest.raises(ProtocolError): - validate_payload(message, ocpp_version="1.6") + _validate_payload(message, ocpp_version="1.6") def test_validate_payload_with_invalid_message_type_id(): @@ -241,7 +244,7 @@ def test_validate_payload_with_invalid_message_type_id(): a message type id other than 2, Call, or 3, CallError. """ with pytest.raises(ValidationError): - validate_payload(dict(), ocpp_version="1.6") + _validate_payload(dict(), ocpp_version="1.6") def test_validate_payload_with_non_existing_schema(): @@ -256,7 +259,7 @@ def test_validate_payload_with_non_existing_schema(): ) with pytest.raises(NotImplementedError): - validate_payload(message, ocpp_version="1.6") + _validate_payload(message, ocpp_version="1.6") def test_call_error_representation(): @@ -342,7 +345,7 @@ def test_serializing_custom_types(): ) try: - validate_payload(message, ocpp_version="1.6") + _validate_payload(message, ocpp_version="1.6") except TypeConstraintViolationError as error: # Before the fix, this call would fail with a TypError. Lack of any error # makes this test pass. @@ -376,7 +379,7 @@ def test_validate_meter_values_hertz(): }, ) - validate_payload(message, ocpp_version="1.6") + _validate_payload(message, ocpp_version="1.6") def test_validate_set_maxlength_violation_payload(): @@ -394,4 +397,25 @@ def test_validate_set_maxlength_violation_payload(): ) with pytest.raises(TypeConstraintViolationError): - validate_payload(message, ocpp_version="1.6") + _validate_payload(message, ocpp_version="1.6") + + +@pytest.mark.parametrize("use_threads", [False, True]) +@pytest.mark.asyncio +async def test_validate_payload_threads(use_threads): + """ + Test that threads usage can be configured + """ + message = CallResult( + unique_id="1234", + action="Heartbeat", + payload={"currentTime": datetime.now().isoformat()}, + ) + + assert threading.active_count() == 1 + ocpp.messages.ASYNC_VALIDATION = use_threads + await validate_payload(message, ocpp_version="1.6") + if use_threads: + assert threading.active_count() > 1 + else: + assert threading.active_count() == 1