Packages

A library for parsing Dutch Smart Meter Requirements (DSMR) telegram data

Current section

Files

Jump to
dsmr lib dsmr.ex
Raw

lib/dsmr.ex

defmodule DSMR do
@moduledoc "README.md"
|> File.read!()
|> String.split("<!-- MDOC !-->")
|> Enum.fetch!(1)
defmodule ChecksumError do
@type t :: %__MODULE__{checksum: binary()}
defexception [:checksum]
@impl true
def message(_exception), do: "checksum mismatch"
end
defmodule ParseError do
@type t :: %__MODULE__{message: binary()}
defexception [:message]
end
alias DSMR.Telegram
@doc """
Parses telegram data from a string and returns a `DSMR.Telegram` struct.
## Options
* `:checksum` - when true, the checksum will be validated, defaults to `true`.
* `:floats` - controls how floats are parsed. Possible values are:
* `:native` (default) - Native conversion from binary to float using `:erlang.binary_to_float/1`,
* `:decimals` - uses `Decimal.new/1` to parse the binary into a Decimal struct with arbitrary precision.
"""
@spec parse(binary(), keyword()) ::
{:ok, Telegram.t()} | {:error, ParseError.t() | ChecksumError.t()}
def parse(string, options \\ []) when is_binary(string) and is_list(options) do
validate_checksum = Keyword.get(options, :checksum, true)
with {:ok, telegram} <- do_parse(string, options),
:ok <- valid_checksum?(telegram, string, validate_checksum) do
{:ok, telegram}
end
end
defp do_parse(string, options) do
try do
case DSMR.Lexer.tokenize(string, options) do
{:ok, tokens} ->
case :dsmr_parser.parse(tokens) do
{:ok, telegram} ->
{:ok, telegram}
{:error, raw_error} ->
{:error, format_parse_error(raw_error)}
end
{:error, reason, rest} ->
{:error, format_parse_error({:lexer, reason, rest})}
end
rescue
error ->
{:error, format_parse_error(error)}
end
end
defp format_parse_error({_token, :dsmr_parser, msgs}) do
message = msgs |> Enum.map(&to_string/1) |> Enum.join("")
%ParseError{message: message}
end
defp format_parse_error({:lexer, _reason, rest}) do
sample_slice = String.slice(rest, 0, 10)
sample = if String.valid?(sample_slice), do: sample_slice, else: inspect(sample_slice)
message = "Parsing failed at `#{sample}`"
%ParseError{message: message}
end
defp format_parse_error(%{} = error) do
detail =
if is_exception(error) do
": " <> Exception.message(error)
else
""
end
message = "An unknown error occurred while parsing" <> detail
%ParseError{message: message}
end
defp valid_checksum?(_telegram, _string, false), do: :ok
# @TODO Only skip empty checksums when telegram version does not require it.
defp valid_checksum?(%DSMR.Telegram{checksum: ""}, _string, _), do: :ok
defp valid_checksum?(%DSMR.Telegram{} = telegram, string, _) do
[raw, _rest] = String.split(string, "!")
checksum = DSMR.CRC16.checksum(raw <> "!")
if checksum === telegram.checksum do
:ok
else
{:error, %ChecksumError{checksum: checksum}}
end
end
end