Exmbus.Parser.DataType (Exmbus v0.3.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)

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)

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

# empty positive BCD number iex> decode_lvar(<<0xC0, 0xFF>>)

# positive BCD number iex> decode_lvar(<<0xC1, 0x10, 0xFF>>)

# empty negative BCD number iex> decode_lvar(<<0xD0, 0xFF>>)

# negative BCD number iex> decode_lvar(<<0xD1, 0x10, 0xFF>>)

# negative multi-byte BCD number iex> decode_lvar(<<0xD2, 0x34, 0x12, 0xFF>>)

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

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

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

decode_type_a(bin, bitsize)

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)

iex> decode_type_a(<<0x23, 0xF1, 0xFF>>, 16)

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

decode_type_b(bin, bitsize)

Type B Signed little-endian integer

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

decode_type_c(bin, bitsize)

Type C Unsigned little-endian integer

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

decode_type_d(bin, bitsize)

Type D Bool list from bits.

iex> decode_type_d(<<0b0000111111110000::unsigned-little-size(16), 0xFF>>, 16)

decode_type_f(arg)

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

decode_type_g(arg)

Type G Convert 16 bits to a Date

iex> decode_type_g(<<0b101::3, 1::5, 0b0010::4, 2::4, 0xFF>>)

iex> decode_type_g(<<0xFF, 0xFF, 0xFF>>)

decode_type_h(arg)

decode_type_i(arg)

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)

Type J Convert 24 bits to a Time

iex> decode_type_j(<<59, 48, 11, 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(_)