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.Messages 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)

Summary

Types

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

A raw telegram binary or a decoded Sml.File.

Functions

Decodes a telegram into an Sml.File.

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

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

Returns the list entry for key.

Returns the scaled value and the unit code for key.

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

Types

error()

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

key()

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

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

source()

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

A raw telegram binary or a decoded Sml.File.

Functions

decode(data, opts \\ [])

@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!(data, opts \\ [])

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

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

entries(source)

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

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

get(source, key)

@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(source, key)

@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(source)

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

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