DSMR

Build StatusHex.pmHexdocs.pm

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

DSMR is the standardized protocol used by smart energy meters in the Netherlands, Belgium, and Luxembourg. These smart meters are installed in homes and businesses to measure electricity and gas consumption in real-time.

Smart meters continuously broadcast "telegrams" - structured data packets containing:

This library parses these telegrams into Elixir structs, making it easy to build energy monitoring applications, home automation systems, or analytics dashboards.

Installation

Add dsmr to your list of dependencies in mix.exs:

def deps do
  [
    {:dsmr, "~> 1.0"},
    {:decimal, "~> 2.0"} # Optional: Required only if you want to use floats: :decimals option for arbitrary precision
  ]
end

By default, measurement values are returned as native floats. To use high-precision %Decimal{} structs instead, add the Decimal dependency and pass the floats: :decimals option to DSMR.parse/2.

Supported DSMR Versions

This library supports DSMR 4.x and 5.x protocols:

The parser automatically handles version differences. The version field in the telegram indicates which protocol version the meter uses.

Usage

telegram =
  # String is formatted in separate lines for readability.
  Enum.join([
    "/KFM5KAIFA-METER\r\n",
    "\r\n",
    "1-3:0.2.8(42)\r\n",
    "0-0:1.0.0(161113205757W)\r\n",
    "0-0:96.1.1(3960221976967177082151037881335713)\r\n",
    "1-0:1.8.1(001581.123*kWh)\r\n",
    "1-0:1.8.2(001435.706*kWh)\r\n",
    "1-0:2.8.1(000000.000*kWh)\r\n",
    "1-0:2.8.2(000000.000*kWh)\r\n",
    "0-0:96.14.0(0002)\r\n",
    "1-0:1.7.0(02.027*kW)\r\n",
    "1-0:2.7.0(00.000*kW)\r\n",
    "0-0:96.7.21(00015)\r\n",
    "0-0:96.7.9(00007)\r\n",
    "1-0:99.97.0(3)(0-0:96.7.19)(000104180320W)(0000237126*s)(000101000001W)",
    "(2147583646*s)(000102000003W)(2317482647*s)\r\n",
    "1-0:32.32.0(00000)\r\n",
    "1-0:52.32.0(00000)\r\n",
    "1-0:72.32.0(00000)\r\n",
    "1-0:32.36.0(00000)\r\n",
    "1-0:52.36.0(00000)\r\n",
    "1-0:72.36.0(00000)\r\n",
    "0-0:96.13.1()\r\n",
    "0-0:96.13.0()\r\n",
    "1-0:31.7.0(000*A)\r\n",
    "1-0:51.7.0(006*A)\r\n",
    "1-0:71.7.0(002*A)\r\n",
    "1-0:21.7.0(00.170*kW)\r\n",
    "1-0:22.7.0(00.000*kW)\r\n",
    "1-0:41.7.0(01.247*kW)\r\n",
    "1-0:42.7.0(00.000*kW)\r\n",
    "1-0:61.7.0(00.209*kW)\r\n",
    "1-0:62.7.0(00.000*kW)\r\n",
    "0-1:24.1.0(003)\r\n",
    "0-1:96.1.0(4819243993373755377509728609491464)\r\n",
    "0-1:24.2.1(161129200000W)(00981.443*m3)\r\n",
    "!6796\r\n"
  ])

DSMR.parse(telegram)
#=> {:ok, %DSMR.Telegram{header: "KFM5KAIFA-METER", version: "42", electricity_delivered_1: %Measurement{unit: "kWh",value: Decimal.new("1581.123")}, ...]}

Parser Options

DSMR.parse/2 accepts an optional keyword list of options:

Option Values Default Description
:checksumtrue / falsetrue When false, skips CRC16 checksum validation. Useful for testing or when processing telegrams from trusted sources.
:floats:native / :decimals:native Controls numeric precision:<br>• :native - Uses Erlang's native float conversion (faster, may have rounding)<br>• :decimals - Returns Decimal structs for arbitrary precision (requires the decimal package)

Examples:

# Skip checksum validation
DSMR.parse(telegram, checksum: false)

# Use Decimal for precise calculations
DSMR.parse(telegram, floats: :decimals)

# Combine options
DSMR.parse(telegram, checksum: false, floats: :decimals)

Available Telegram Fields

The parsed %DSMR.Telegram{} struct contains the following fields:

Header & Metadata

Electricity Measurements

Per-Phase Measurements (3-phase connections)

Power Quality

M-Bus Devices (gas, water, thermal meters)

When the parser encounters OBIS codes that aren't in its mapping table, they're collected in unknown_fields as {obis_tuple, value} pairs instead of causing a crash. This allows the library to handle:

See full documentation for detailed field descriptions and types.

Serialization

You can convert a Telegram struct back to its string representation:

telegram = %DSMR.Telegram{
  header: "KFM5KAIFA-METER",
  checksum: "6796",
  version: "42",
  measured_at: %DSMR.Timestamp{
    value: ~N[2016-11-13 20:57:57],
    dst: "W"
  },
  electricity_delivered_1: %DSMR.Measurement{value: Decimal.new("1581.123"), unit: "kWh"}
}

DSMR.Telegram.to_string(telegram)
#=> "/KFM5KAIFA-METER\r\n\r\n1-3:0.2.8(42)\r\n0-0:1.0.0(161113205757W)\r\n1-0:1.8.1(001581.123*kWh)\r\n!6796\r\n"

Error Handling

The parser returns {:error, reason} tuples for invalid data:

DSMR.parse("invalid data")
#=> {:error, {1, :dsmr_parser, [&#39;syntax error before: &#39;, []]}}

DSMR.parse("/HEADER\r\n!FFFF\r\n")  # Bad checksum
#=> {:error, :invalid_checksum}

Common errors:

Troubleshooting:

Getting Real Telegram Data

Smart meters typically expose data via:

This library only handles parsing - you'll need to handle data acquisition separately.

Example: Reading from a networked meter

See the included Livebook example for a complete GenServer implementation that:

For serial port connections, use libraries like Circuits.UART.

Internals

This library uses a two-stage parsing architecture built on Erlang's leex (lexical analyzer) and yecc (parser generator):

Stage 1: Lexical Analysis (leex)

The lexer (src/dsmr_lexer.xrl) tokenizes raw DSMR telegram data into structured tokens:

The lexer also extracts the MBus channel number from OBIS codes (second position) for single-pass processing of multi-device telegrams.

Stage 2: Parsing (yecc)

The parser (src/dsmr_parser.yrl) uses grammar rules to transform tokens into the DSMR.Telegram struct:

object -> obis attributes : map_obis_to_field(&#39;$1&#39;, &#39;$2&#39;).
attribute -> &#39;(&#39; value &#39;)&#39; : &#39;$2&#39;.
value -> float &#39;*&#39; string : extract_measurement(&#39;$1&#39;, &#39;$3&#39;).

OBIS code mapping is centralized in the DSMR.OBIS Elixir module (lib/dsmr/obis.ex), which serves as the single source of truth for all field mappings. The parser calls this module at runtime to map OBIS codes like [1,0,1,8,1] to field names like :electricity_delivered_1.

Special cases are handled directly in the parser:

The final DSMR.Parser module coordinates both stages and constructs the final struct with proper type conversions (Decimal, NaiveDateTime, etc.).

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Everyone is encouraged to help improve this project. Here are a few ways you can help:

License

Copyright (C) 2020 Robin van der Vleuten

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.