From 2e09f6ceb09ca0e917d46e3f3fd1001442db3104 Mon Sep 17 00:00:00 2001 From: snabo1988 Date: Sun, 11 Jan 2026 19:01:20 +0100 Subject: [PATCH 1/2] feat: treat Preparing->Available without Charging as failed session --- csds/uec_csds/ocpp/charge_point_v16.py | 53 +++++++++++++++++--------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/csds/uec_csds/ocpp/charge_point_v16.py b/csds/uec_csds/ocpp/charge_point_v16.py index 07078fc..65fbda0 100644 --- a/csds/uec_csds/ocpp/charge_point_v16.py +++ b/csds/uec_csds/ocpp/charge_point_v16.py @@ -55,7 +55,7 @@ def on_heartbeat(self, **kwargs): @on(Action.authorize) def on_authorize(self, **kwargs): - id_tag_info = IdTagInfo(status=AuthorizationStatusEnumType.accepted) # type: ignore + id_tag_info = IdTagInfo(status=AuthorizationStatusEnumType.accepted) return call_result.Authorize(id_tag_info=id_tag_info) @on(Action.meter_values) @@ -176,13 +176,13 @@ async def set_charging_profile_req(self, payload: call.SetChargingProfile): async def get_composite_schedule( self, payload: call.GetCompositeSchedule ) -> call_result.GetCompositeSchedule: - return await self.call(payload) # type: ignore + return await self.call(payload) async def get_composite_schedule_req( self, **kwargs ) -> call_result.GetCompositeSchedule: payload = call.GetCompositeSchedule(**kwargs) - return await self.call(payload) # type: ignore + return await self.call(payload) async def clear_charging_profile_req(self, **kwargs): payload = call.ClearChargingProfile(**kwargs) @@ -234,18 +234,37 @@ async def get_diagnostic(self, start_time: str) -> str: ) ), ) - await self._diagnostics_upload_future if result.file_name: return self._ftp.get_pathname(result.file_name) raise Exception("No diagnostic file name received") async def handle_status_notification(self, sn: call.StatusNotification): + if sn.status == ChargePointStatus.available: + session = self._sessions.get(sn.connector_id) + + if session and not session.has_entered_charging(): + session.ended_silent( + sn.timestamp or datetime.now(timezone.utc).isoformat(), + + "PREPARING_ABORTED_BEFORE_CHARGING", + ) + await self._engine.handle_failed_session( + cast(Session, session), + ) + self._sessions.pop(sn.connector_id, None) + if sn.status == ChargePointStatus.preparing: self._sessions[sn.connector_id] = Session( self, - or_now(sn.timestamp), + sn.timestamp or datetime.now(timezone.utc).isoformat(), + ) + if sn.status == ChargePointStatus.charging: + session = self._sessions.get(sn.connector_id) + if session: + session.mark_entered_charging() + if sn.status == ChargePointStatus.faulted: session = self._sessions.get(sn.connector_id) if not session: @@ -254,6 +273,7 @@ async def handle_status_notification(self, sn: call.StatusNotification): session.ended(or_now(sn.timestamp), get_error_code(sn)) await self._engine.handle_failed_session( cast(Session, session), + self._sessions.pop(sn.connector_id, None) ) @@ -263,11 +283,22 @@ def __init__(self, charger: ChargePoint16, timestamp: str): self._end_timestamp = "" self._start_timestamp = timestamp self._error_code = "" + self._has_entered_charging = False + + def mark_entered_charging(self): + self._has_entered_charging = True + + def has_entered_charging(self) -> bool: + return self._has_entered_charging def ended(self, timestamp: str, error_code: str): self._end_timestamp = timestamp self._error_code = error_code + def ended_silent(self, timestamp: str, reason: str): + self._end_timestamp = timestamp + self._error_code = reason + async def get_diagnostic(self) -> str: pathname = await self._charger.get_diagnostic(self._start_timestamp) return read_diagnostic_file(pathname) @@ -280,15 +311,3 @@ def timestamp(self) -> str: def error_code(self) -> str: return self._error_code - - -def or_now(ts: Optional[str]) -> str: - return ts or datetime.now(timezone.utc).isoformat() - - -def get_error_code(sn: call.StatusNotification) -> str: - return ( - sn.error_code - if sn.error_code != ChargePointErrorCode.other_error - else sn.vendor_error_code or "Unknown Error" - ) From 7f91137d9f3534af1352ac32daf961ee50401c1e Mon Sep 17 00:00:00 2001 From: snabo1988 Date: Tue, 13 Jan 2026 21:22:31 +0100 Subject: [PATCH 2/2] fix: address review comments for silent failure detection --- csds/uec_csds/ocpp/charge_point_v16.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/csds/uec_csds/ocpp/charge_point_v16.py b/csds/uec_csds/ocpp/charge_point_v16.py index 65fbda0..25a83b9 100644 --- a/csds/uec_csds/ocpp/charge_point_v16.py +++ b/csds/uec_csds/ocpp/charge_point_v16.py @@ -27,6 +27,17 @@ from ..engine import Engine from typing import cast +def get_error_code(sn: call.StatusNotification) -> str: + return ( + sn.error_code + if sn.error_code != ChargePointErrorCode.other_error + else sn.vendor_error_code or "Unknown Error" + ) + + +def or_now(ts: Optional[str]) -> str: + return ts or datetime.now(timezone.utc).isoformat() + class ChargePoint16(cp): def __init__(self, ftp, engine, *args, **kwargs): @@ -245,7 +256,7 @@ async def handle_status_notification(self, sn: call.StatusNotification): if session and not session.has_entered_charging(): session.ended_silent( - sn.timestamp or datetime.now(timezone.utc).isoformat(), + or_now(sn.timestamp), "PREPARING_ABORTED_BEFORE_CHARGING", ) @@ -257,7 +268,7 @@ async def handle_status_notification(self, sn: call.StatusNotification): if sn.status == ChargePointStatus.preparing: self._sessions[sn.connector_id] = Session( self, - sn.timestamp or datetime.now(timezone.utc).isoformat(), + or_now(sn.timestamp), ) if sn.status == ChargePointStatus.charging: @@ -272,9 +283,9 @@ async def handle_status_notification(self, sn: call.StatusNotification): self._sessions[sn.connector_id] = session session.ended(or_now(sn.timestamp), get_error_code(sn)) await self._engine.handle_failed_session( - cast(Session, session), - self._sessions.pop(sn.connector_id, None) + cast(Session, session) ) + self._sessions.pop(sn.connector_id, None) class Session: