Exmbus.Parser.DataType (Exmbus v0.4.0)

View Source

Contains functions for working with data encoded according to EN 13757-3:2018 section 6.3.3 (Table 4) and specified in Annex A of same document.

It also implements working with variable-length data (LVAR) according to same document same section (Table 5)

Summary

Functions

Decode an LVAR value from binary. This is a variable-length-style encoding, but with a twist (as with most things in mbus), so it requires a bit more work than <<length, data::binary-size(length), rest::binary>>.

Type A BCD integer. if MSB 0xF then the remanining digits are interpreted as a negative number

Type B Signed little-endian integer

Type C Unsigned little-endian integer

Type D Bool list from bits.

Type F Convert 32 bits to a NaiveDateTime with seconds truncated to 0. (Type F is a date+time without seconds specified, but we just truncate. Good Enough :tm:)

Type G Convert 16 bits to a Date

Type I Convert 48 bits to a NaiveDateTime (This isn't 1:1 with spec but without creating a new type for DateTime this is the closest we get)

Type J Convert 24 bits to a Time

Encode a Date to type G (16 bit)

Functions

decode_lvar(bin, mode \\ :container)

@spec decode_lvar(binary(), :container | :latin1) ::
  {:ok, binary() | integer(), binary()}
  | {:error, {:not_enough_bytes_for_lvar, non_neg_integer(), binary()},
     binary()}

Decode an LVAR value from binary. This is a variable-length-style encoding, but with a twist (as with most things in mbus), so it requires a bit more work than <<length, data::binary-size(length), rest::binary>>.

It takes an optional argument mode which is an atom indicating what decode method to apply in case of LVAR 0x00–0xBF, since that can be both ISO 8859-1 encoded text, or it can be raw bytes in case of VIFs that need a container type.

Currently the following are container types: # If VIF is from the FD extension table and is: # - 0bE0111011 then it's wmbus # - 0bE1110110 then it's manifacturer specific bytes

The default is currently :container mode which just returns the bytes as a binary. This is what the old parser does as well, but we could a latin1 decoder in here if someone actually sends non-ascii text to us, which is the only case where it would be needed (because in elixir Strings are just binary data in UTF-8, and both Latin-1 and UTF-8 are extensions to ASCII, so by coincedence, if you send ASCII-only characters it will be the same in both UTF-8 and Latin-1, and elixir binary data and elixir strings are the same, so we don't need to do any conversion.)

# raw bytes container
iex> decode_lvar(<<5, "hello", 0xFF>>, :container)
{:ok, <<"hello">>, <<0xFF>>}

# empty container
iex> decode_lvar(<<0, 0xFF>>, :container)
{:ok, <<>>, <<0xFF>>}

# empty positive BCD number
iex> decode_lvar(<<0xC0, 0xFF>>)
{:ok, 0, <<0xFF>>}

# positive BCD number
iex> decode_lvar(<<0xC1, 0x10, 0xFF>>)
{:ok, 10, <<0xFF>>}

# empty negative BCD number
iex> decode_lvar(<<0xD0, 0xFF>>)
{:ok, 0, <<0xFF>>}

# negative BCD number
iex> decode_lvar(<<0xD1, 0x10, 0xFF>>)
{:ok, -10, <<0xFF>>}

# negative multi-byte BCD number
iex> decode_lvar(<<0xD2, 0x34, 0x12, 0xFF>>)
{:ok, -1234, <<0xFF>>}

# LVAR binary number
iex> decode_lvar(<<0xE1, 123::signed-little-size(8), 0xFF>>)
{:ok, 123, <<0xFF>>}

# LVAR binary number
iex> decode_lvar(<<0xE4, -123456789::signed-little-size(32), 0xFF>>)
{:ok, -123456789, <<0xFF>>}

iex> decode_lvar(<<5, "hel">>, :container)
{:error, {:not_enough_bytes_for_lvar, 5, "hel"}, <<>>}

decode_type_a(bin, bitsize)

@spec decode_type_a(binary(), integer()) ::
  {:ok, integer() | {:invalid, any()}, binary()}

Type A BCD integer. if MSB 0xF then the remanining digits are interpreted as a negative number

iex> decode_type_a(<<0x34, 0x12, 0xFF>>, 16)
{:ok, 1234, <<0xFF>>}

iex> decode_type_a(<<0x23, 0xF1, 0xFF>>, 16)
{:ok, -123, <<0xFF>>}

iex> decode_type_a(<<0x23, 0xC1, 0xFF>>, 16)
{:ok, {:invalid, {:type_a, 0xC123}}, <<0xFF>>}

decode_type_b(bin, bitsize)

@spec decode_type_b(binary(), integer()) :: {:ok, integer(), binary()}

Type B Signed little-endian integer

iex> decode_type_b(<< -1234::signed-little-size(16), 0xFF>>, 16)
{:ok, -1234, <<0xFF>>}

decode_type_c(bin, bitsize)

@spec decode_type_c(binary(), integer()) :: {:ok, non_neg_integer(), binary()}

Type C Unsigned little-endian integer

iex> decode_type_c(<<1234::unsigned-little-size(16), 0xFF>>, 16)
{:ok, 1234, <<0xFF>>}

decode_type_d(bin, bitsize)

@spec decode_type_d(binary(), integer()) :: {:ok, [boolean()], binary()}

Type D Bool list from bits.

iex> decode_type_d(<<0b0000111111110000::unsigned-little-size(16), 0xFF>>, 16)
{:ok, [false, false, false, false, true, true, true, true, true, true, true, true, false, false, false, false], <<0xFF>>}

decode_type_f(arg)

@spec decode_type_f(binary()) ::
  {:ok, NaiveDateTime.t() | :invalid, rest :: binary()}
  | {:error, {:unsupported_feature, :data_type_f_with_periodicity},
     rest :: binary()}
  | {:error, naive_datetime_new_error_reason :: atom(), rest :: binary()}

Type F Convert 32 bits to a NaiveDateTime with seconds truncated to 0. (Type F is a date+time without seconds specified, but we just truncate. Good Enough :tm:)

iex> decode_type_f(<<0b00::2, 10::6, 0::1, 0::2, 20::5, 0b101::3, 1::5, 0b0010::4, 2::4, 0xFF>>)
{:ok, ~N[2021-02-01 20:10:00], <<0xFF>>}

decode_type_g(arg)

@spec decode_type_g(binary()) ::
  {:ok, Date.t() | Exmbus.Parser.DataType.PeriodicDate.t() | :invalid,
   rest :: binary()}

Type G Convert 16 bits to a Date

iex> decode_type_g(<<0b101::3, 1::5, 0b0010::4, 2::4, 0xFF>>)
{:ok, ~D[2021-02-01], <<0xFF>>}

iex> decode_type_g(<<0xFF, 0xFF, 0xFF>>)
{:ok, :invalid, <<0xFF>>}

iex> decode_type_g(<<0b111_00001, 0b1111_0001, 0xFF>>)
{:ok, %PeriodicDate{year: nil, month: 1, day: 1}, <<0xFF>>}

decode_type_h(arg)

@spec decode_type_h(binary()) ::
  {:ok, float() | :nan | :positive_infinity | :negative_infinity,
   rest :: binary()}

decode_type_i(arg)

@spec decode_type_i(binary()) ::
  {:ok, NaiveDateTime.t(), rest :: binary()}
  | {:error, naive_datetime_new_error_reason :: atom(), rest :: binary()}

Type I Convert 48 bits to a NaiveDateTime (This isn't 1:1 with spec but without creating a new type for DateTime this is the closest we get)

decode_type_j(arg)

@spec decode_type_j(binary()) ::
  {:ok, Time.t(), rest :: binary()}
  | {:error, time_new_error_reason :: atom(), rest :: binary()}

Type J Convert 24 bits to a Time

iex> decode_type_j(<<59, 48, 11, 0xFF>>)
{:ok, ~T[11:48:59], <<0xFF>>}

decode_type_k(bin)

decode_type_l(bin)

decode_type_m(bin)

encode_lvar(b)

encode_type_a(value, bitsize)

encode_type_b(data, bitsize)

encode_type_c(data, bitsize)

encode_type_d(data, bitsize)

encode_type_f(ndt)

encode_type_g(date)

Encode a Date to type G (16 bit)

# <<161, 34>> = <<5::3, 1::5, 2::4, 2::4>> iex> {:ok, <<161, 34>>} = encode_type_g(~D[2021-02-01])

encode_type_h(value)

encode_type_i(naive_date_time)

encode_type_j(time)

encode_type_k(_)

encode_type_l(_)

encode_type_m(_)