Exmbus.Parser.DataType (Exmbus v0.4.0)
View SourceContains 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
@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"}, <<>>}
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>>}
Type B Signed little-endian integer
iex> decode_type_b(<< -1234::signed-little-size(16), 0xFF>>, 16)
{:ok, -1234, <<0xFF>>}
@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>>}
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>>}
@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>>}
@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>>}
@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)
@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>>}
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])