2021년 4월 14일 수요일

Python OCPP 패키지 - Server 소스 해설

파이썬용 OCPP 패키지 서버 해설 - 원문 링크

 OCPP(Open Charge Point Protocol)은 두가지 역할 이 있다 - 중앙 서버(서버) 와 충전포인트(클라이언트). OCPP Python 패키지는 양쪽 연결을 모델링하는데 사용 가능하다.

이 문서는 중앙서버를 만드는 방법과 서버측에서 충전포인트를 모델링하는방법을 설명한다.  이 페이지 대부분의 예제는 websockets 계층 구현을위하여 websockets 라이브러리를 사용한다. 꼭 이걸 사용하지 않고, 다른 웹소켓을 적용해도 동작은 잘 될것이다.

Create a websocket server

아래 아주 간단한 예제는 접속 포트 9000을 사용하는 웹소켓 서버이고, 새로운 모든 웹소켓 연결에 대해 'Charge point connected' 를 인쇄한다.

import asyncio
import websockets


async def on_connect(websocket, path):
   await websocket.send('Connection made succesfully.')
   print(f'Charge point {path} connected')


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())

설명이 필요한 두가지가 있다.

  ㅇ on_connect()에 첫번째 인자로 websockets.server() 의 핸들러가 전달된다. 이 핸들러는 모든 새 연결에서 실행된다.

핸들러는 2개의 인자를 전달 하는데, websockets.server.WebSocketServerProtocol과 요청 URI 이다.  요청 URI는 연결을 요청한 충전포인트의 식별자로 사용된다. 아래에 OCCP-J 사양의 3.1.1절을 인용한다.

  "충전포인트의 연결 URI는 충전점의 식별자가 포함되어, 중앙 서버가 어떤 충전점에 해당 websocket이 속해있는지 알게 한다."

  이 예제의 핸들러는 클라이언트에 메시지를 보내고 콘솔에 메시지를 출력한다.

  ㅇ websockets.server()의 subprotocol 인자는 OCPP 1.6을 지원하는 서버를 구성하는데 사용된다.


서버를 시작한 후, 웹소켓 대화형 클라이언트를 사용하여 클라이언트를 서버에 연결할 수 있다.

$ python -m websockets ws://localhost:9000/test_charge_point
Connected to ws://localhost:9000/test_charge_point.
< Connection made successfully.
Connection closed: code = 1000 (OK), no reason.


OCPP compliant handler 

(OCPP 준수(규정을 따르는) 핸들러)

위의 웹소켓 서버 예제는 OCPP 규정을 따르지 않기 때문에, 그닥 좋지 않다.

위의 코드에서 on_connect() 핸들러를 제거하고 다음 예제대로 바꾼다.

from datetime import datetime

from ocpp.routing import on
from ocpp.v16 import ChargePoint as cp
from ocpp.v16.enums import Action, RegistrationStatus
from ocpp.v16 import call_result


class MyChargePoint(cp):
    @on(Action.BootNotification)
    def on_boot_notitication(self, charge_point_vendor, charge_point_model, **kwargs):
        return call_result.BootNotificationPayload(
            current_time=datetime.utcnow().isoformat(),
            interval=10,
            status=RegistrationStatus.accepted
        )


async def on_connect(websocket, path):
    """ For every new charge point that connects, create a ChargePoint instance
    and start listening for messages.

    """
    charge_point_id = path.strip('/')
    cp = MyChargePoint(charge_point_id, websocket)

    await cp.start()

on_connect() 핸들러가 갱신되면, 이제 MyChargePoint 인스턴스를 생성하고 start() 코(부속)루틴을 호출한다. 

MyChargePoint는 ocpp.v16.ChargePoint의 Subclass 이다. ocpp.v16.ChargePoint는 OCPP 패키지의 핵심이다.  이 클래스는 클라이언트에서 올라오는 메시지를 올바른 핸들러(처리기)에 할당한다.  또한, 송/수신중인 모든 메시지의 유효성을 검사하고 흐름제어를 구현한다.

MyChargePoint 클래스는 'BootNotification' 요청 구현을 위해서 @On() 데코레이터를 사용한다. @On() 은 단일 인자로 문자열로된 액션의 이름을 사용한다.  비록 이 예제에서는 사용하지 않았지만, ocpp 패키지는 후처리 요청 핸들러로 사용 가능한 @after() 데코레이터도 제공한다. 

OCPP 스펙(사양)에 따르면, BootNotification 요청의 내용(페이로드)에 반드시 필요한 2가지는 'chargePointModel' 과 'chargePointVendor' 이고, 부가적으로 7가지의 옵션 인자가 있다.  핸들러(처리기)는 두개의 필수인자 charge_point_vendor 와 charge_point_model을 사용해서 이를 적용한다.  핸들러는 옵션 인자로 **kwargs 를 사용한다.

핸들러(처리기)는 ocpp.v16.call_result.BootNotificationPayload의 인스턴스를 반환한다.  이 객체는 클라이언트로 돌려보내는 응답을 만들 때 사용한다.

노트:

OCPP는 페이로드 키값에 낙타형 명명방식을 사용하는데, 파이썬은 snake_case 방식을 사용한다.  그러므로, 이 ocpp 패키지는 메시지의 모든 키를 낙타형에서 snake_case 로 또는 그 반대로 변환하여 파이썬 코드를 작성하도록 유의하라.


이제 websocket 서버를 다시 시작하고, 이전과 같이 클라이언트를 연결한다. 클라이언트가 연결되면, BootNotification을 중앙시스템(서버)로 전송한다.

`[2, "12345", "BootNotification", {"chargePointVendor": "The Mobility House", "chargePointModel": "Optimus"}]`

서버가 응답하고, 아래와 같이 표시되어야 한다.

$ python -m websockets ws://localhost:9000/test_charge_point
Connected to ws://localhost:9000/test_charge_point.
> [2, "12345", "BootNotification", {"chargePointVendor": "The Mobility House", "chargePointModel": "Optimus"}]
< [3, "12345", {"currentTime": "2019-06-16T11:18:09.591716", "interval": 10, "status": "Accepted"}]`

축하!!. 중앙시스템(서버)를 만들었다.

이 문서에서 만든 서버 소스코드는 examples 디렉토리에서 찾을 수 있다.