GitHub - mijnverbruik/dsmr: A library for parsing Dutch Smart Meter Requirements (DSMR) telegram data

5 min read Original article ↗

Build Status Hex.pm Hexdocs.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:

  • Current and cumulative electricity usage (delivered and returned to grid)
  • Gas consumption readings
  • Voltage and current measurements per phase
  • Power failure logs and quality statistics
  • Additional M-Bus connected devices (water, thermal, etc.)

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, "~> 3.1"} # 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

The parser supports DSMR 2.2, 3.0, 4.x, and 5.x telegrams. Version-specific fields are optional on %DSMR.Telegram{} and remain nil when they are not present in the input.

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

{:ok, parsed} = DSMR.parse(telegram)

parsed.version
#=> "42"

parsed.electricity_delivered_1
#=> %DSMR.Measurement{unit: "kWh", value: 1581.123}

Parser Options

DSMR.parse/2 accepts these options:

Option Values Default Description
:checksum true / false true Validates the CRC16 checksum when enabled.
:floats :native / :decimals :native Returns decimal values as native floats or %Decimal{} structs.

Examples:

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

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

Available Telegram Fields

DSMR.parse/2 returns a %DSMR.Telegram{} struct. Common fields include:

Header and metadata

  • header - Meter manufacturer and model
  • checksum - CRC16 checksum
  • version - DSMR protocol version
  • measured_at - Telegram timestamp
  • equipment_id - Unique meter identifier

Electricity measurements

  • electricity_delivered_1 / electricity_delivered_2 - Cumulative consumption (tariff 1/2)
  • electricity_returned_1 / electricity_returned_2 - Cumulative return to grid (tariff 1/2)
  • electricity_tariff_indicator - Current active tariff
  • electricity_currently_delivered / electricity_currently_returned - Instantaneous power

Per-phase measurements

  • currently_delivered_l1/l2/l3 - Power delivered per phase
  • currently_returned_l1/l2/l3 - Power returned per phase
  • voltage_l1/l2/l3 - Voltage per phase
  • phase_power_current_l1/l2/l3 - Current per phase

Power quality

  • power_failures_count / power_failures_long_count - Failure counters
  • power_failures_log - Timestamped log of power failures
  • voltage_sags_l1/l2/l3_count / voltage_swells_l1/l2/l3_count - Quality events

M-Bus devices

  • mbus_devices - Connected gas, water, heat, or other meters

Unknown OBIS codes are collected in unknown_fields as {obis_tuple, value} pairs. They are preserved instead of rejected.

See full documentation for detailed field descriptions and types.

Serialization

Use DSMR.Telegram.to_string/1 to convert a telegram struct back to its string representation. Fields with nil or empty string values are omitted.

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, %DSMR.ParseError{message: "checksum delimiter '!' not found"}}

{:error, %DSMR.ChecksumError{}} = DSMR.parse("/HEADER\r\n!FFFF\r\n")

Use DSMR.parse!/2 when invalid input should raise instead of returning an error tuple.

Getting Real Telegram Data

Smart meters typically expose telegram data through a serial P1 port or through a P1-to-network adapter. This package only parses telegrams; collecting bytes from the meter is handled separately.

See the included Livebook example for a GenServer that:

  • Connects to a meter via TCP (common with WiFi P1 adapters)
  • Buffers incoming lines and assembles complete telegrams
  • Parses telegrams and visualizes usage

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

Internals

DSMR uses a two-stage parser built on Erlang's leex and yecc.

Stage 1: Lexical Analysis (leex)

The lexer (src/dsmr_lexer.xrl) turns telegram text into tokens:

  • OBIS codes such as 1-0:1.8.1
  • Timestamps such as 161113205757W
  • Measurements such as 001581.123*kWh
  • Headers and checksums

The lexer also extracts the M-Bus channel from OBIS codes.

Stage 2: Parsing (yecc)

The parser (src/dsmr_parser.yrl) maps tokens into a %DSMR.Telegram{}:

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

OBIS mappings live in DSMR.OBIS. The parser calls that module to map known codes to telegram fields.

Special cases are handled directly in the parser:

  • M-Bus fields are grouped by channel.
  • Power failure logs are parsed as variable-length event lists.
  • Unknown OBIS codes are collected in unknown_fields.

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