From 12ed77cccec451bba1db0b850a9c708c4fb93aa4 Mon Sep 17 00:00:00 2001 From: Oscar Rodriguez Date: Mon, 18 Mar 2024 15:04:15 +0100 Subject: [PATCH 1/4] fix typo CostUpdated enum for 201 --- ocpp/v201/enums.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ocpp/v201/enums.py b/ocpp/v201/enums.py index 1e0410143..2f903ca75 100644 --- a/ocpp/v201/enums.py +++ b/ocpp/v201/enums.py @@ -97,7 +97,7 @@ def __init__(self, *args, **kwargs): clear_display_message = "ClearDisplayMessage" cleared_charging_limit = "ClearedChargingLimit" clear_variable_monitoring = "ClearVariableMonitoring" - cost_update = "CostUpdate" + cost_updated = "CostUpdated" customer_information = "CustomerInformation" data_transfer = "DataTransfer" delete_certificate = "DeleteCertificate" From 1447f88ded9144eb4e3c22138fd971248280c506 Mon Sep 17 00:00:00 2001 From: Oscar Date: Tue, 26 Nov 2024 01:45:01 +0100 Subject: [PATCH 2/4] feat/allow_passing_answer_from_@on_to_@after_hook --- ocpp/charge_point.py | 19 +++++++++++++++++-- ocpp/routing.py | 3 ++- tests/test_charge_point.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/ocpp/charge_point.py b/ocpp/charge_point.py index 7314804b1..08a33e6e3 100644 --- a/ocpp/charge_point.py +++ b/ocpp/charge_point.py @@ -356,10 +356,25 @@ async def _handle_call(self, msg): call_unique_id_required = "call_unique_id" in handler_signature.parameters # call_unique_id should be passed as kwarg only if is defined explicitly # in the handler signature + inject_response = getattr(handler, "_inject_response", False) if call_unique_id_required: - response = handler(**snake_case_payload, call_unique_id=msg.unique_id) + if inject_response: + response = handler( + **snake_case_payload, + call_unique_id=msg.unique_id, + on_response=response_payload, + ) + else: + response = handler( + **snake_case_payload, call_unique_id=msg.unique_id + ) else: - response = handler(**snake_case_payload) + if inject_response: + response = handler( + **snake_case_payload, on_response=response_payload + ) + else: + response = handler(**snake_case_payload) # Create task to avoid blocking when making a call inside the # after handler if inspect.isawaitable(response): diff --git a/ocpp/routing.py b/ocpp/routing.py index dbdc641cf..8c9406fb1 100644 --- a/ocpp/routing.py +++ b/ocpp/routing.py @@ -56,7 +56,7 @@ def inner(*args, **kwargs): return decorator -def after(action): +def after(action, inject_response=False): """Function decorator to mark function as hook to post-request hook. This hook's arguments are the data that is in the payload for the specific @@ -76,6 +76,7 @@ def inner(*args, **kwargs): return func(*args, **kwargs) inner._after_action = action + inner._inject_response = inject_response if func.__name__ not in routables: routables.append(func.__name__) return inner diff --git a/tests/test_charge_point.py b/tests/test_charge_point.py index 00571f9fd..36b4a9eb9 100644 --- a/tests/test_charge_point.py +++ b/tests/test_charge_point.py @@ -472,3 +472,33 @@ def after_boot_notification(self, *args, **kwargs): assert ChargerA.after_boot_notification_call_count == 1 assert ChargerB.on_boot_notification_call_count == 1 assert ChargerB.after_boot_notification_call_count == 1 + + +@pytest.mark.asyncio +async def test_response_injected_to_after_handler(connection): + """ + This test ensures that the response is injected to the `after` handler + when `inject_response` is set to True. + """ + + class TestChargePoint(cp_16): + @on(Action.BootNotification) + def on_boot_notification(self, **kwargs): + return BootNotificationResult( + current_time="2024-11-01T00:00:00Z", + interval=300, + status=RegistrationStatus.accepted, + ) + + @after(Action.BootNotification, inject_response=True) + def after_boot_notification(self, on_response, **kwargs): + # print args and kwargs to check if response is injected + + assert on_response["current_time"] == "2024-11-01T00:00:00Z" + assert on_response["interval"] == 300 + assert on_response["status"] == RegistrationStatus.accepted + + charge_point = TestChargePoint("test_cp", connection) + payload = {"chargePointVendor": "vendor", "chargePointModel": "model"} + msg = Call(unique_id="1234", action=Action.BootNotification.value, payload=payload) + await charge_point._handle_call(msg) From 7bbace26f3e0e955cca5c8d21b08b5f0a3e24143 Mon Sep 17 00:00:00 2001 From: OSkrk <63248433+OSkrk@users.noreply.github.com> Date: Tue, 26 Nov 2024 01:57:39 +0100 Subject: [PATCH 3/4] Update tests/test_charge_point.py --- tests/test_charge_point.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_charge_point.py b/tests/test_charge_point.py index 36b4a9eb9..cedc3ddb6 100644 --- a/tests/test_charge_point.py +++ b/tests/test_charge_point.py @@ -492,7 +492,6 @@ def on_boot_notification(self, **kwargs): @after(Action.BootNotification, inject_response=True) def after_boot_notification(self, on_response, **kwargs): - # print args and kwargs to check if response is injected assert on_response["current_time"] == "2024-11-01T00:00:00Z" assert on_response["interval"] == 300 From dc3978deb14c9bea462e61e151dd7f087668d7e2 Mon Sep 17 00:00:00 2001 From: Oscar Rodriguez Date: Fri, 5 Jun 2026 13:39:11 +0200 Subject: [PATCH 4/4] Address review: single handler() call, rename on_response->call_response, fix enum - Refactor _handle_call to build kwargs dict and call handler() once (astrand/tropxy) - Rename injected kwarg on_response -> call_response (tropxy) - Document inject_response argument in after() docstring (tropxy) - Fix test to use snake_case Action.boot_notification (fixes failing CI) --- ocpp/charge_point.py | 24 ++++++------------------ ocpp/routing.py | 11 +++++++++++ tests/test_charge_point.py | 22 ++++++++++++++-------- 3 files changed, 31 insertions(+), 26 deletions(-) diff --git a/ocpp/charge_point.py b/ocpp/charge_point.py index 7ebe948a9..e64de984f 100644 --- a/ocpp/charge_point.py +++ b/ocpp/charge_point.py @@ -355,25 +355,13 @@ async def _handle_call(self, msg): call_unique_id_required = "call_unique_id" in handler_signature.parameters # call_unique_id should be passed as kwarg only if is defined explicitly # in the handler signature - inject_response = getattr(handler, "_inject_response", False) if call_unique_id_required: - if inject_response: - response = handler( - **snake_case_payload, - call_unique_id=msg.unique_id, - on_response=response_payload, - ) - else: - response = handler( - **snake_case_payload, call_unique_id=msg.unique_id - ) - else: - if inject_response: - response = handler( - **snake_case_payload, on_response=response_payload - ) - else: - response = handler(**snake_case_payload) + snake_case_payload["call_unique_id"] = msg.unique_id + # call_response should be passed as kwarg only if the after handler + # was decorated with inject_response=True + if getattr(handler, "_inject_response", False): + snake_case_payload["call_response"] = response_payload + response = handler(**snake_case_payload) # Create task to avoid blocking when making a call inside the # after handler if inspect.isawaitable(response): diff --git a/ocpp/routing.py b/ocpp/routing.py index ffe951481..d9b4c311f 100644 --- a/ocpp/routing.py +++ b/ocpp/routing.py @@ -68,6 +68,17 @@ def after(action, inject_response=False): def after_boot_notification(): pass + When ``inject_response`` is set to ``True``, the response that was returned + by the matching ``@on`` handler (and sent back to the counterparty) is + passed to the hook as the ``call_response`` keyword argument. This avoids + having to store the response in a temporary variable to make it available + in the ``@after`` hook. It defaults to ``False`` to preserve backwards + compatibility: + + @after(Action.boot_notification, inject_response=True): + def after_boot_notification(self, call_response, **kwargs): + ... + """ def decorator(func): diff --git a/tests/test_charge_point.py b/tests/test_charge_point.py index 80de7183a..eee073e1a 100644 --- a/tests/test_charge_point.py +++ b/tests/test_charge_point.py @@ -486,7 +486,9 @@ async def test_response_injected_to_after_handler(connection): """ class TestChargePoint(cp_16): - @on(Action.BootNotification) + after_boot_notification_call_count = 0 + + @on(Action.boot_notification) def on_boot_notification(self, **kwargs): return BootNotificationResult( current_time="2024-11-01T00:00:00Z", @@ -494,14 +496,18 @@ def on_boot_notification(self, **kwargs): status=RegistrationStatus.accepted, ) - @after(Action.BootNotification, inject_response=True) - def after_boot_notification(self, on_response, **kwargs): - - assert on_response["current_time"] == "2024-11-01T00:00:00Z" - assert on_response["interval"] == 300 - assert on_response["status"] == RegistrationStatus.accepted + @after(Action.boot_notification, inject_response=True) + def after_boot_notification(self, call_response, **kwargs): + assert call_response["current_time"] == "2024-11-01T00:00:00Z" + assert call_response["interval"] == 300 + assert call_response["status"] == RegistrationStatus.accepted + TestChargePoint.after_boot_notification_call_count += 1 charge_point = TestChargePoint("test_cp", connection) payload = {"chargePointVendor": "vendor", "chargePointModel": "model"} - msg = Call(unique_id="1234", action=Action.BootNotification.value, payload=payload) + msg = Call(unique_id="1234", action=Action.boot_notification.value, payload=payload) await charge_point._handle_call(msg) + + # Ensure the after handler actually ran, so the assertions above are not + # silently skipped. + assert TestChargePoint.after_boot_notification_call_count == 1