An Ocpp Model

codecov gitlab Hex pm

Currently I'm doing 2 experiments

  • An Ocpp Backend in Elixir
  • A Nerves based Charger on a Rasberry Pi Zero

This library contains the OCPP 2.0.1 Model / Protocol that is needed for both those projects

It will be populated on a 'need to have' basis starting with basic charger functionality

Implemented Messages

OCPP VersionStateDone-ness
2.0.1messages_2.0.1.md81%
1.6messages_1.6.md0%

Add Dependency

def deps do
  [
    {:ocpp_model, "~> 0.3.0"}
  ]
end

Usage

Using the library is by having your modules assume either the of the following behaviours:

BehaviourSummary
OcppModel.V20.Behaviours.BasicChargerSupports callbacke for only the basic messages
OcppModel.V20.Behaviours.ChargerSupports callbacks for all messages implemented in this library
OcppModel.V20.Behaviours.BasicChargeSystemSupports callbacks for only the basic messages
OcppModel.V20.Behaviours.ChargeSystemSupports callbacks for all messages implemented in this library

This library does not make any decisions on transport, you can do the json over websockets thing, or protobuf over http long-polling or an IoT solution as long as it supports bi-directional communication

+-------------------+                    +------------+                    +------------------------+
| Charger Behaviour |                    | OCPP Model |                    | ChargeSystem Behaviour |
+-------------------+                    +------------+                    +------------------------+
    |                                       |      |                                          |
    |      +--------------------------------+      +--------------------------------+         |
    |      |                                                                        |         |
    V      V                                                                        V         V
+---------------+     +----------------+    Internet    +----------------+     +--------------------+
| MyTestCharger | <-> | json/websocket | <- Lora     -> | websocket/json | <-> | MyTestChargeSystem | 
+---------------+     +----------------+    IoT         +----------------+     +--------------------+ 

An example Charger

defmodule Implementations.MyTestBasicCharger do
  @moduledoc """
    Basic charger implementation, only supports the minimum required to do a chargesession.
  """

  alias OcppModel.V20.Behaviours, as: B
  alias OcppModel.V20.DataTypes, as: DT
  alias OcppModel.V20.EnumTypes, as: ET
  alias OcppModel.V20.Messages, as: M

  @behaviour B.BasicCharger

  def handle([2, id, action, payload], state) do
    case B.BasicCharger.handle(__MODULE__, action, payload, state) do
      {{:ok, response_payload}, new_state} -> {[3, id, response_payload], new_state}
      {{:error, error, desc}, new_state} -> {[4, id, Atom.to_string(error), desc, {}], new_state}
    end
  end

  def handle({[3, id, payload], _state}),
    do: IO.puts("Received answer for id #{id}: #{inspect(payload)}")

  def handle({[4, id, err, desc, det], _state}),
    do: IO.puts("Received error for id #{id}: #{err}, #{desc}, #{det}")

  @impl B.BasicCharger
  def reset(req, state) do
    if ET.validate?(:reset, req.type) do
      case req.type do
        "Immediate" -> {{:ok, %M.ResetResponse{status: "Accepted"}}, state}
        "OnIdle" -> {{:ok, %M.ResetResponse{status: "Scheduled"}}, state}
      end
    else
      {{:error, "Unknown Reset Type #{req.type}"}, state}
    end
  end

  @impl B.BasicCharger
  def unlock_connector(_req, state),
    do:
      {{:ok,
        %M.UnlockConnectorResponse{
          status: "Unlocked",
          statusInfo: %DT.StatusInfo{reasonCode: "cable unlocked"}
        }}, state}
end

An Example ChargeSystem

defmodule Implementations.MyTestBasicChargeSystem do
  @moduledoc """
    Basic chargesystem implementation, only supports the minimum required to do a chargesession.
  """

  alias OcppModel.V20.Behaviours, as: B
  alias OcppModel.V20.DataTypes, as: DT
  alias OcppModel.V20.EnumTypes, as: ET
  alias OcppModel.V20.Messages, as: M

  @behaviour B.BasicChargeSystem

  def handle([2, id, action, payload], state) do
    case B.BasicChargeSystem.handle(__MODULE__, action, payload, state) do
      {{:ok, response_payload}, new_state} -> {[3, id, response_payload], new_state}
      {{:error, error, desc}, state} -> {[4, id, Atom.to_string(error), desc, {}], state}
    end
  end

  def handle({[3, id, payload], _state}),
    do: IO.puts("Received answer for id #{id}: #{inspect(payload)}")

  def handle({[4, id, err, desc, det], _state}),
    do: IO.puts("Received error for id #{id}: #{err}, #{desc}, #{det}")

  @impl B.BasicChargeSystem
  def authorize(_req, state) do
    {{:ok, %M.AuthorizeResponse{idTokenInfo: %DT.IdTokenInfo{status: "Accepted"}}}, state}
  end

  @impl B.BasicChargeSystem
  def boot_notification(req, state) do
    if ET.validate?(:bootReason, req.reason) do
      {{:ok,
        %M.BootNotificationResponse{
          currentTime: current_time(),
          interval: 900,
          status: %DT.StatusInfo{reasonCode: ""}
        }}, state}
    else
      {{:error, :boot_notification, "#{req.reason} is not a valid BootReason"}, state}
    end
  end

  @impl B.BasicChargeSystem
  def heartbeat(_req, state) do
    {{:ok, %M.HeartbeatResponse{currentTime: current_time()}}, state}
  end

  @impl B.BasicChargeSystem
  def status_notification(_req, state) do
    {{:ok, %M.StatusNotificationResponse{}}, state}
  end

  @impl B.BasicChargeSystem
  def transaction_event(_req, state) do
    {{:ok, %M.TransactionEventResponse{}}, state}
  end

  def current_time, do: DateTime.now!("Etc/UTC") |> DateTime.to_iso8601()
end