ocpp Python Library

repository·master·Indexed 21 days ago

https://github.com/mobilityhouse/ocpp

A Python implementation of the JSON-based Open Charge Point Protocol (OCPP) supporting versions 1.6 and 2.0.1. The library provides foundational building blocks for developing both Charging Stations (clients) and Charging Station Management Systems (servers), featuring a ChargePoint class for message routing, payload translation between snake_case and camelCase, and support for WebSocket connections.

Tokens
8.4K
Snippets
24
Records
32
Agent score
77%

What's inside ocpp

  1. Overview of ocpp support

    master

    The ocpp package is a Python implementation of the JSON version of the Open Charge Point Protocol (OCPP).

    Supported versions include:

    • OCPP 1.6 (including errata v4)
    • OCPP 2.0.1 (Edition 2 FINAL, 2022-12-15 and Edition 3 errata 2024-11)
  2. Use the experimental OCPP 2.1 module

    master
    The ocpp/experimental/v21 module is intended for testing and development of the OCPP 2.1 standard. Because the standard is not yet finalized, this module does not follow the traditional release cycle and may introduce breaking changes without notice. Use this module only for experimental purposes and do not rely on it for production environments.
  3. Understand the roles of the ocpp library

    master

    The ocpp Python library provides foundational components to model both sides of an Open Charge Point Protocol (OCPP) connection. It is designed to help you develop either a Charging Station (Charge Point), which acts as the client, or a Charging Station Management System (CSMS)/Central System, which acts as the server.

    Note that this library is not a complete, out-of-the-box solution; it provides the building blocks that you must implement and tailor to your specific use case.

  4. How OCPP message routing works

    master

    The ocpp package uses a decorator-based routing system to map incoming OCPP messages to specific Python methods.

    • @on(Action.NAME): This decorator registers a method as a handler for a specific OCPP action. When a message with that action arrives, the ChargePoint class routes it to this method.
    • @after(Action.NAME): This decorator can be used to register a post-request handler that executes after a specific action has been processed.
    • Data Transformation: The library handles the translation between the OCPP JSON standard (camelCase) and Pythonic naming conventions (snake_case). For example, a JSON key chargePointVendor is passed to your handler as charge_point_vendor.
    • Validation: The ChargePoint class ensures that incoming messages conform to the expected schema before they reach your handler.
  5. Create a basic WebSocket server for OCPP

    master

    To host an OCPP central system, you must first set up a WebSocket server. The server listens for incoming connections, where the request URI (path) is used to identify the Charge Point. You must specify the supported OCPP version using the subprotocols parameter in websockets.serve (e.g., ['ocpp1.6'] or ['ocpp1.6', 'ocpp2.0.1']).

    Example of a minimal WebSocket server:

    import asyncio
    import websockets
    
    async def on_connect(connection: websockets.ServerConnection):
        await connection.send('Connection made successfully')
        print(f'Charge point connected: {connection.request.path}')
    
    async def main():
        server = await websockets.serve(
            on_connect,
            '0.0.0.0',
            9000,
            subprotocols=['ocpp1.6'],
        )
        await server.wait_closed()
    
    if __name__ == '__main__':
        asyncio.run(main())
    import asyncio
    import websockets
    
    async def on_connect(connection: websockets.ServerConnection):
        await connection.send('Connection made successfully')
        print(f'Charge point connected: {connection.request.path}')
    
    async def main():
        server = await websockets.serve(
            on_connect,
            '0.0.0.0',
            9000,
            subprotocols=['ocpp1.6'],
        )
        await server.wait_closed()
    
    if __name__ == '__main__':
        asyncio.run(main())
  6. Implement an OCPP compliant handler

    master

    To implement the actual OCPP protocol, you must subclass ocpp.v16.ChargePoint (or the version appropriate for your needs). This class handles message routing and validation.

    Key Implementation Steps:

    1. Subclass ChargePoint: Create a class that inherits from ocpp.v16.ChargePoint.
    2. Register Handlers: Use the @on(Action.ACTION_NAME) decorator to register methods that handle specific OCPP actions (e.g., Action.boot_notification).
    3. Handle Arguments: The library automatically converts OCPP camelCase JSON keys to Pythonic snake_case. Required arguments from the OCPP payload should be defined as named parameters in your handler, while optional arguments can be captured using **kwargs.
    4. Return Results: Handlers must return an instance of a call_result class (e.g., call_result.BootNotification) to send a valid response back to the client.
    5. Connection Lifecycle: In your WebSocket on_connect handler, extract the charge_point_id from the connection path, instantiate your custom ChargePoint class, and call await charge_point.start().

    Example Implementation:

    from datetime import datetime, timezone
    import websockets
    
    from ocpp.routing import on
    from ocpp.v16 import ChargePoint as cp
    from ocpp.v16 import call_result
    from ocpp.v16.enums import Action, RegistrationStatus
    
    
    class MyChargePoint(cp):
        @on(Action.boot_notification)
        async def on_boot_notification(
            self, charge_point_vendor, charge_point_model, **kwargs
        ):
            return call_result.BootNotification(
                current_time=datetime.now(tz=timezone.utc).isoformat(),
                interval=10,
                status=RegistrationStatus.accepted,
            )
    
    
    async def on_connect(connection: websockets.ServerConnection):
        # Extract ID from path, e.g., /cp_id_1 -> cp_id_1
        charge_point_id = connection.request.path.split("/")[-1]
        charge_point = MyChargePoint(charge_point_id, connection)
    
        await charge_point.start()
    from datetime import datetime, timezone
    import websockets
    
    from ocpp.routing import on
    from ocpp.v16 import ChargePoint as cp
    from ocpp.v16 import call_result
    from ocpp.v16.enums import Action, RegistrationStatus
    
    
    class MyChargePoint(cp):
        @on(Action.boot_notification)
        async def on_boot_notification(
            self, charge_point_vendor, charge_point_model, **kwargs
        ):
            return call_result.BootNotification(
                current_time=datetime.now(tz=timezone.utc).isoformat(),
                interval=10,
                status=RegistrationStatus.accepted,
            )
    
    
    async def on_connect(connection: websockets.ServerConnection):
        charge_point_id = connection.request.path.split("/")[-1]
        charge_point = MyChargePoint(charge_point_id, connection)
    
        await charge_point.start()
  7. Implement a Charge Point client

    master

    To create a charge point that connects to a central system, you must subclass the version-specific ChargePoint class (e.g., ocpp.v16.ChargePoint) and provide a websocket connection.

    Connection Steps:

    1. Establish a WebSocket connection: Use websockets.connect() with a URL that includes the charge point identifier in the path (e.g., ws://localhost:9000/CP_1).
    2. Specify the Subprotocol: You must pass the OCPP version as a subprotocol in the websockets.connect call (e.g., subprotocols=['ocpp1.6'] or subprotocols=['ocpp2.0.1']).
    3. Initialize the ChargePoint: Pass the charge point ID and the websocket object to the ChargePoint constructor.
    4. Run the Client: Use asyncio.gather() to run the charge_point.start() coroutine (which listens for incoming messages) concurrently with your business logic coroutines.

    Sending Requests:

    Use the call() method on your ChargePoint instance. This method accepts a request object from the ocpp.v16.call module, serializes it, and returns a corresponding call_result dataclass.

    import asyncio
    import websockets
    from ocpp.v16 import ChargePoint as cp
    from ocpp.v16 import call, call_result
    from ocpp.v16.enums import RegistrationStatus
    
    class ChargePoint(cp):
        async def send_boot_notification(self):
            request = call.BootNotification(
                charge_point_model="Wallbox XYZ",
                charge_point_vendor="acme",
            )
            response: call_result.BootNotification = await self.call(request)
    
            if response.status == RegistrationStatus.accepted:
                print("Connected to central system.")
    
    async def main():
        async with websockets.connect(
            "ws://localhost:9000/CP_1", subprotocols=["ocpp1.6"]
        ) as ws:
            charge_point = ChargePoint("CP_1", ws)
    
            await asyncio.gather(
                charge_point.start(),
                charge_point.send_boot_notification(),
            )
    
    if __name__ == "__main__":
        asyncio.run(main())
  8. How message routing and hooks work in ChargePoint

    master

    The ChargePoint class uses a routing mechanism to map incoming OCPP Action names to specific handler functions (hooks).

    The Hook Lifecycle:

    1. _on_action: The primary handler for the incoming CALL. It receives the payload as snake_case keyword arguments. If this handler returns a value, it is used to construct the CALLRESULT.
    2. _after_action: An optional hook executed after the response has been sent. It can be used for side effects. If decorated with inject_response=True, it also receives the call_response payload.

    Key Features:

    • Automatic Translation: The class automatically converts incoming camelCase payloads to snake_case for your handlers, and converts your returned snake_case dicts back to camelCase for the wire.
    • Schema Validation: By default, incoming CALL payloads and outgoing CALLRESULT payloads are validated against the OCPP schema unless _skip_schema_validation is set in the route map.