README.md

# An Ocpp Model
[![codecov](https://codecov.io/gh/gertjana/ocpp_model/branch/main/graph/badge.svg?token=nrXnKllzIA)](https://codecov.io/gh/gertjana/ocpp_model)
[![gitlab](https://gitlab.com/gertjana/ocpp_model/badges/develop/pipeline.svg)](https://gitlab.com/gertjana/ocpp_model)
[![Hex pm](http://img.shields.io/hexpm/v/ocpp_model.svg?style=flat)](https://hex.pm/packages/ocpp_model)

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

## Add Dependency

```elixir
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

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

## An example Charger

```elixir
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

```elixir
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
```