An Ocpp Model

codecovgitlabHex pm

Currently I'm doing 2 experiments

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 Version State Done-ness
2.0.1 messages_2.0.1.md 81%
1.6 messages_1.6.md 0%

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:

Behaviour Summary
OcppModel.V20.Behaviours.BasicCharger Supports callbacke for only the basic messages
OcppModel.V20.Behaviours.Charger Supports callbacks for all messages implemented in this library
OcppModel.V20.Behaviours.BasicChargeSystem Supports callbacks for only the basic messages
OcppModel.V20.Behaviours.ChargeSystem Supports 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