mobilityhouse / ocpp

Python implementation of the Open Charge Point Protocol (OCPP).
MIT License
790 stars 315 forks source link

Charger throws "BootNotificationReq error: OccurenceConstraintViolation, Parameter 'currentTime' is missing" when connecting using Django Channels #474

Closed Sadeeed closed 1 year ago

Sadeeed commented 1 year ago

Hi, I get this error in my chargers event logs when replying to a BootNotification with my Django channels-based OCPP application BootNotificationReq error: OccurenceConstraintViolation, Parameter 'currentTime' is missing and I'm sending the current time already. This code was working with Flask and WebSockets but for some reason, the same code doesn't want to work on Django with channels.

Here is my Consumer all other actions aren't performed yet since it fails at sending the correct boot notification

class ChargePointConsumer(AsyncJsonWebsocketConsumer):
    async def connect(self):
        if "ocpp1.6" in self.scope["subprotocols"]:
            # Check for protocol
            self.id = self.scope["url_route"]["kwargs"]["charger_id"]
            print("ID: ", self.id)
            # Verify evcharger against database and add to private channel layer
            try:
                charge_point = ChargePointModel.objects.get(charge_point_id=self.id)
                print("Charge Point: ", charge_point)
                await self.accept("ocpp1.6")
                charge_point.status = "active"
                charge_point.save()
                # Add to CP Group
                await self.channel_layer.group_add(
                    self.id,
                    self.channel_name,
                )
            except Exception as e:
                logger.info(f"ocpp1.6 device attempted to connect but was not in registered database: {e}")
                await self.close()
            logger.info("Connected device: %s", self.scope["url_route"]["kwargs"]["charger_id"])
        else:
            await self.close(1002)

        # self.accept("ocpp1.6")

    async def disconnect(self, close_code):
        # Remove from CP Group
        await self.channel_layer.group_discard(
            self.id,
            self.channel_name,
        )
        # Update Charge Point Status
        charge_point = ChargePointModel.objects.get(charge_point_id=self.id)
        charge_point.status = "inactive"
        charge_point.save()
        logger.info("Disconnected device: %s", self.scope["url_route"]["kwargs"]["charger_id"])
        print("Close Code: ", close_code)

    async def decode_json(self, text_data):
        return unpack(text_data)

    async def encode_json(self, content):
        return pack(content)

    async def receive_json(self, content, **kwargs):
        # For related "Call", create proper "CallResult".
        print("Content: ", content)
        print("Kwargs: ", kwargs)
        if content.action == "BootNotification":
            # Handle received payload.
            if check_evse_in_db(self.id):
                payload = call_result.BootNotificationPayload(
                    current_time=datetime.utcnow().isoformat(), interval=10, status=RegistrationStatus.accepted
                )
            else:
                logger.warning(f"Unknown EVSE (ID: {self.id}) trying to connect.")
                payload = call_result.BootNotificationPayload(
                    current_time=datetime.utcnow().isoformat(), interval=10, status=RegistrationStatus.rejected
                )
        elif content.action == "Heartbeat":
            # Create result payload.
            payload = call_result.HeartbeatPayload(
                # current_time=datetime.utcnow().isoformat()
                current_time=datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
                + "Z"
            )
        elif content.action == "StatusNotification":
            error_code = content.error_code
            status = content.status
            timestamp = content.timestamp
            connector_id = content.connector_id

            logger.info(
                f"Received StatusNotification from {self.id}. Connector ID: {connector_id}, Error Code: {error_code}, "
                f"Status: {status}, Timestamp: {timestamp}. Additional Data: {content}"
            )

            # Handling for each ChargePointErrorCode
            if error_code == ChargePointErrorCode.no_error:
                logger.info(f"No error reported by {self.id}")
            else:
                logger.warning(f"Error code '{error_code}' received from {self.id}")

            # Handling for each ChargePointStatus
            if status == ChargePointStatus.available:
                logger.info(f"Charge point {self.id} is available")
            # elif status == ChargePointStatus.occupied:
            #     logger.info(f"Charge point {self.id} is occupied")
            elif status == ChargePointStatus.reserved:
                logger.info(f"Charge point {self.id} is reserved")
            elif status == ChargePointStatus.unavailable:
                logger.warning(f"Charge point {self.id} is unavailable")
            elif status == ChargePointStatus.faulted:
                logger.error(f"Charge point {self.id} has faulted")
            elif status == ChargePointStatus.charging:
                logger.info(f"Charge point {self.id} is charging")
            elif status == ChargePointStatus.finishing:
                logger.info(f"Charge point {self.id} is finishing charging session")
            else:
                logger.warning(f"Unknown status '{status}' received from {self.id}")

            payload = call_result.StatusNotificationPayload()
        elif content.action == "Authorize":
            logger.info(f"Received Authorize request from {self.id}. Additional Data: {content}")
            payload = call_result.AuthorizePayload(id_tag_info={"status": "Accepted"})
        elif content.action == "StartTransaction":
            logger.info(f"Received StartTransaction from {self.id}. Content: {content}")
            payload = call_result.StartTransactionPayload(id_tag_info={"status": "Accepted"}, transaction_id=int(1))
        elif content.action == "MeterValues":
            logger.info(f"Received MeterValues from {self.id}. Additional Data: {content}")
            payload = call_result.MeterValuesPayload()
        elif content.action == "StopTransaction":
            logger.info(f"Received StopTransaction from {self.id}. Content: {content}")
            # f"Received StopTransaction from {self.id}. Transaction ID: {transaction_id},
            # ID Tag: {id_tag}, Timestamp: {timestamp}, Meter Stop: {meter_stop}. Additional Data: {kwargs}")
            payload = call_result.StopTransactionPayload()
        elif content.action == "DataTransfer":
            logger.info(f"Received DataTransfer from {self.id}. Content: {content}")
            payload = call_result.DataTransferPayload(status=DataTransferStatus.accepted)
        elif content.action == "DiagnosticsStatusNotification":
            """Handle DiagnosticsStatusNotification message"""
            logger.info(f"Received DiagnosticsStatusNotification from {self.id}. Additional Data: {content}")
            status = content.status
            # Handling for each DiagnosticsStatus
            if status == DiagnosticsStatus.idle:
                logger.info(f"Diagnostics status is idle for Charge Point {self.id}")
                # Actions or logic to handle "idle" status can be added here
            elif status == DiagnosticsStatus.uploading:
                logger.info(f"Diagnostics upload in progress for Charge Point {self.id}")
                # Actions or logic to handle "uploading" status can be added here
            elif status == DiagnosticsStatus.uploaded:
                logger.info(f"Diagnostics successfully uploaded for Charge Point {self.id}")
                # Actions or logic to handle successful upload can be added here
            elif status == DiagnosticsStatus.uploadFailed:
                logger.error(f"Diagnostics upload failed for Charge Point {self.id}")
                # Actions or logic to handle upload failure can be added here
            else:
                logger.warning(f"Unknown diagnostics status '{status}' received from Charge Point {self.id}")
            payload = call_result.DiagnosticsStatusNotificationPayload()

        # Send "CallResult"
        print("Payload:", payload)
        print("Payload Dict: ", content.create_call_result(asdict(payload)))
        await self.send_json(content.create_call_result(asdict(payload)))

I very confused at this point and would appreciate any help

Sadeeed commented 1 year ago

Resolved it, when using channels you will have to convert your payload to camelCase instead of the default snake_case just pass your payload like this

from ocpp.charge_point import snake_to_camel_case

self.send_json(content.create_call_result(snake_to_camel_case(asdict(payload))))