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)
[![Build Status](https://travis-ci.com/gertjana/ocpp_model.svg?branch=main)](https://travis-ci.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 running 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                      |
| ------------ | -------------------------- |
| 2.0.1        | [messages.md](messages.md) |
| 1.6          | might implement later      |

## Installation

```elixir
def deps do
  [
    {:ocpp_model, "~> 0.2.2"}
  ]
end
```

## Usage

Using the library is by having your modules assume either the `OcppModel.V20.Behaviours.Charger` or the `OcppModel.V20.Behaviours.ChargeSystem` Behaviour.


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
+---------------+  +-------------------+  +------------------------+
| OCPP Model    |  | Charger Behaviour |  | ChargeSystem Behaviour |
+---------------+  +-------------------+  +------------------------+
    |       |                |                         |
    |       +----------------|-------------------------|-------------------------------------+
    |      +-----------------+                         +----------------------------+        |
    |      |                                                                        |        |
+---------------+     +----------------+    Internet    +----------------+     +--------------------+
| MyTestCharger | <-> | json/websocket | <- Lora     -> | websocket/json | <-> | MyTestChargeSystem | 
+---------------+     +----------------+    IoT         +----------------+     +--------------------+ 
```

## An example Charger

```
  defmodule MyTestCharger do

    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.Charger

    @doc """
      Entrypoint for decoded OCPP Messages coming from the ChargeSystem
    """
    def handle([2, id, action, payload]) do
      case B.Charger.handle(MyTestCharger, action, payload) do
        {:ok, response_payload} -> [3, id, response_payload]
        {:error, error} ->         [4, id, Atom.to_string(error), "", {}]
      end
    end

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

    @impl B.Charger
    def change_availability(req) do
      if ET.validate?(:operationalStatus, req.operationalStatus) do
         {:ok, %M.ChangeAvailabilityResponse{status: "Accepted",
                  statusInfo: %DT.StatusInfo{reasonCode: "charger is inoperative"}}}
      else
        {:error, :invalid_operational_status}
      end
    end
    
    @impl B.Charger
    def data_transfer(_req), do: {:ok, %M.DataTransferResponse{status: "Accepted"}}

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

  end
```

## An Example ChargeSystem

```
  defmodule MyTestChargeSystem do

    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.ChargeSystem

    @doc """
      Entrypoint for decoded OCPP Messages coming from the Charger
    """
    def handle([2, id, action, payload]) do
      case B.ChargeSystem.handle(__MODULE__, action, payload) do
        {:ok, response_payload} -> [3, id, response_payload]
        {:error, error} ->         [4, id, Atom.to_string(error), "", {}]
      end
    end
    def handle([3, id, payload]), do: IO.puts "Received answer for id #{id}: #{inspect(payload)}"
    def handle([4, id, err, desc, det]), do: IO.puts "Received error for id #{id}: #{err}, #{desc}, #{det}"

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

    @impl B.ChargeSystem
    def boot_notification(req) do
      if ET.validate?(:bootReason, req.reason) do
        {:ok, %M.BootNotificationResponse{currentTime: current_time(), interval: 900,
                status: %DT.StatusInfo{reasonCode: ""}}}
      else
        {:error, :invalid_bootreason}
      end
    end

    @impl B.ChargeSystem
    def data_transfer(req) do
      case req.vendorId do
        "GA" -> {:ok, %M.DataTransferResponse{status: "Accepted", data: String.reverse(req.data)}}
        _ -> {:ok, %M.DataTransferResponse{status: "UnknownVendorId"}}
      end
    end

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

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

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

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