# `Sml`
[🔗](https://github.com/emischorr/sml_ex/blob/v0.1.0/lib/sml.ex#L1)

Decoder for SML (Smart Message Language) telegrams.

SML (BSI TR-03109-1) is the protocol German electricity meters ("moderne
Messeinrichtung", eHZ) use on their optical interface. A meter typically
pushes a telegram every one to four seconds that contains the current
readings, e.g. the energy counter (`1-0:1.8.0`) and the current power
(`1-0:16.7.0`).

## Reading values

    {:ok, bezug_wh, 30} = Sml.value(telegram, :bezug)
    {:ok, leistung_w, 27} = Sml.value(telegram, :leistung)

The third element is the DLMS unit code (`30` = Wh, `27` = W), see
`Sml.Units`. Values are scaled with the scaler sent by the meter: the result
is an integer if the scaler is not negative and a float otherwise.

Keys can be an alias atom (see `Sml.Obis` for the list), an OBIS string like
`"1-0:1.8.0"` or `"1-0:1.8.0*255"`, an `Sml.Obis` struct or the raw 6 byte
OBIS binary.

All functions accept either the raw telegram binary or an already decoded
`Sml.File`. When reading several values from one telegram, decode it once:

    {:ok, file} = Sml.decode(telegram)
    {:ok, bezug, _unit} = Sml.value(file, :bezug)
    {:ok, einspeisung, _unit} = Sml.value(file, :einspeisung)

## Inspecting telegrams

`decode/2` returns the full structure: an `Sml.File` with a list of
`Sml.Message`s whose bodies are the structs in `Sml.Messages`. Since
everything is a plain struct, pattern matching works as expected:

    {:ok, %Sml.File{messages: messages}} = Sml.decode(telegram)

    for %Sml.Message{body: %Sml.Messages.GetListResponse{val_list: entries}} <- messages,
        %Sml.ListEntry{obis: obis, unit: 27} = entry <- entries do
      {to_string(obis), Sml.ListEntry.scaled_value(entry)}
    end

## Reading from a serial port

Telegrams arrive in arbitrary chunks. `Sml.Transport.extract/1` splits a
buffer into complete telegrams and returns the rest for the next read:

    {telegrams, buffer} = Sml.Transport.extract(buffer <> chunk)

# `error`

```elixir
@type error() ::
  Sml.Transport.unwrap_error()
  | Sml.Decoder.error()
  | :not_found
  | :invalid_obis
  | {:unknown_alias, atom()}
```

# `key`

```elixir
@type key() :: atom() | String.t() | Sml.Obis.t()
```

An alias atom, OBIS string, `Sml.Obis` struct or 6 byte OBIS binary.

# `source`

```elixir
@type source() :: binary() | Sml.File.t()
```

A raw telegram binary or a decoded `Sml.File`.

# `decode`

```elixir
@spec decode(binary(), keyword()) :: {:ok, Sml.File.t()} | {:error, error()}
```

Decodes a telegram into an `Sml.File`.

`data` may be a complete transport frame (starting with
`1B 1B 1B 1B 01 01 01 01`, leading garbage is skipped) or the bare message
bytes without framing.

## Options

  * `:crc` - checksum algorithm used to verify the transport frame and each
    message: `:auto` (default) accepts CRC-16/X-25 as well as CRC-16/Kermit
    (used by e.g. Holley DTZ541), `:x25` or `:kermit` accept only that one,
    `false` skips verification for meters that send wrong checksums.

## Errors

  * `:no_start_sequence`, `:incomplete`, `:invalid_escape`,
    `:invalid_padding` - broken transport framing
  * `:invalid_crc` - a checksum does not match
  * `{:parse_error, reason, offset}` - invalid SML encoding at byte `offset`
    of the message payload

# `decode!`

```elixir
@spec decode!(binary(), keyword()) :: Sml.File.t()
```

Like `decode/2` but raises `Sml.DecodeError` on failure.

# `entries`

```elixir
@spec entries(source()) :: {:ok, [Sml.ListEntry.t()]} | {:error, error()}
```

Returns all list entries (readings) of all `GetListResponse` messages.

# `get`

```elixir
@spec get(source(), key()) :: {:ok, Sml.ListEntry.t()} | {:error, error()}
```

Returns the list entry for `key`.

If several entries match a pattern without F group (e.g. some meters send
`1-0:1.8.0*255` along with historic values `1-0:1.8.0*96`), the one with
`F = 255` (current value) is preferred, otherwise the first match is
returned.

# `value`

```elixir
@spec value(source(), key()) ::
  {:ok, number() | binary() | boolean() | nil, Sml.Units.code() | nil}
  | {:error, error()}
```

Returns the scaled value and the unit code for `key`.

Non-numeric values (like the manufacturer ID) are returned as binaries; the
unit is `nil` if the meter does not send one.

    {:ok, 12_345.6, 30} = Sml.value(telegram, :bezug)
    {:ok, 230, 27} = Sml.value(telegram, "1-0:16.7.0")
    {:ok, "EMH", nil} = Sml.value(telegram, :hersteller)

# `values`

```elixir
@spec values(source()) ::
  {:ok, %{required(Sml.Obis.t()) =&gt; {term(), Sml.Units.code() | nil}}}
  | {:error, error()}
```

Returns all readings as a map of `Sml.Obis` to `{scaled_value, unit}`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
