dsmr

package module
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 22, 2025 License: MIT Imports: 7 Imported by: 0

README

DSMR

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

Latest Release Build Status MIT license PkgGoDev

The package focuses on turning raw telegram strings into strongly typed Go structures, making it easier to work with smart meter measurements such as energy consumption, production, gas readings, and meter metadata. It supports common DSMR versions out of the box and hides the boilerplate of dealing with checksums, optional fields, and multi-line records.

Usage

import "github.com/robinvdvleuten/dsmr"

raw := "" +
    "/ISk5\\2MT382-1000\r\n" +
    "\r\n" +
    "1-3:0.2.8(50)\r\n" +
    "0-0:1.0.0(170102192002W)\r\n" +
    "0-0:96.1.1(4B384547303034303436333935353037)\r\n" +
    "1-0:1.8.1(000004.426*kWh)\r\n" +
    "1-0:1.8.2(000002.399*kWh)\r\n" +
    "1-0:2.8.1(000002.444*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(00.244*kW)\r\n" +
    "1-0:2.7.0(00.000*kW)\r\n" +
    "0-0:96.7.21(00013)\r\n" +
    "0-0:96.7.9(00000)\r\n" +
    "1-0:99.97.0(0)(0-0:96.7.19)\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.0()\r\n" +
    "1-0:32.7.0(0230.0*V)\r\n" +
    "1-0:52.7.0(0230.0*V)\r\n" +
    "1-0:72.7.0(0229.0*V)\r\n" +
    "1-0:31.7.0(0.48*A)\r\n" +
    "1-0:51.7.0(0.44*A)\r\n" +
    "1-0:71.7.0(0.86*A)\r\n" +
    "1-0:21.7.0(00.070*kW)\r\n" +
    "1-0:41.7.0(00.032*kW)\r\n" +
    "1-0:61.7.0(00.142*kW)\r\n" +
    "1-0:22.7.0(00.000*kW)\r\n" +
    "1-0:42.7.0(00.000*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(3232323241424344313233343536373839)\r\n" +
    "0-1:24.2.1(170102161005W)(00000.107*m3)\r\n" +
    "0-2:24.1.0(003)\r\n" +
    "0-2:96.1.0()\r\n" +
    "!6EEE\r\n"

telegram, err := dsmr.Parse(raw)
if err != nil {
    // Handle checksum mismatches or invalid telegrams.
    log.Fatal(err)
}
for _, entry := range telegram.Data {
    switch e := entry.(type) {
    case *dsmr.Object:
        switch e.OBIS.Value {
        case "1-0:1.8.1":
            if measurement, ok := e.Value.(*dsmr.Measurement); ok {
                fmt.Printf("Electricity delivered (tariff 1): %s %s\n", measurement.Value, measurement.Unit.Value)
            }
        case "0-1:24.2.1":
            if capture, ok := e.Value.(*dsmr.LastCapture); ok {
                fmt.Printf("Gas delivered: %s %s\n", capture.Value.Value, capture.Value.Unit.Value)
            }
        }
    case *dsmr.MBusDevice:
        fmt.Printf("M-Bus channel %d has %d objects\n", e.Channel, len(e.Data))
    }
}

The parser also accepts io.Reader inputs via dsmr.Parse, which is helpful when reading from serial connections or files produced by smart meter gateways. See the _examples directory for additional usage patterns, including continuous reading from a P1 port.

Contributing

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

To get started with development:

git clone https://github.com/robinvdvleuten/dsmr.git
cd dsmr
go test ./...

Before submitting a pull request, please make sure to run go fmt on any Go source files you touched so the code stays consistent.

Feel free to open an issue to get feedback on your idea before spending too much time on it.

License

The MIT License (MIT). Please see License File for more information.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ChecksumError

type ChecksumError struct {
	Unexpected string
	Expect     string
}

func (*ChecksumError) Error

func (e *ChecksumError) Error() string

type DSTIndicator

type DSTIndicator int

DSTIndicator represents daylight saving time indicator.

const (
	DSTUnknown DSTIndicator = iota
	DSTWinter
	DSTSummer
)

func (DSTIndicator) Bool

func (d DSTIndicator) Bool() bool

Bool reports whether daylight saving time is active (summer time).

func (*DSTIndicator) Capture

func (d *DSTIndicator) Capture(values []string) error

Capture implements participle capture for DSTIndicator.

type Data

type Data []Entry

Data collects the entries in the body of a telegram.

type Entry

type Entry interface {
	Key() string
	Node
}

Entry represents the top-level items that can appear in telegram data.

type Error

type Error interface {
	error
}

type Event

type Event struct {
	Pos lexer.Position `parser:""`

	Timestamp *Timestamp   `parser:"'(' @@ ')'"`
	Value     *Measurement `parser:"'(' @@ ( ')' (?='(') )?"`
}

Event represents a timestamp+duration pair.

func (*Event) Position

func (e *Event) Position() lexer.Position

type EventLog

type EventLog struct {
	Pos lexer.Position `parser:""`

	Count *Number  `parser:"@@ ')'"`
	OBIS  *OBIS    `parser:"'(' @@ ( ')' (?='(') )?"`
	Value []*Event `parser:"@@*"`
}

EventLog represents a log of events.

func (*EventLog) Position

func (e *EventLog) Position() lexer.Position
type Footer struct {
	Pos lexer.Position `parser:""`

	Value string `parser:"'!' @~EOL? (?=EOL)"`
}

Footer is the closing line of a telegram.

func (*Footer) Key

func (f *Footer) Key() string

func (*Footer) Position

func (f *Footer) Position() lexer.Position
type Header struct {
	Pos lexer.Position `parser:""`

	Value string `parser:"'/' @~EOL+ (?=EOL)"`
}

Header is the opening line of a telegram.

func (*Header) Key

func (h *Header) Key() string

func (*Header) Position

func (h *Header) Position() lexer.Position

type LastCapture

type LastCapture struct {
	Pos lexer.Position `parser:""`

	Timestamp *Timestamp   `parser:"@@ ')'"`
	Value     *Measurement `parser:"'(' @@ ( ?!')' '(' )"`
}

LastCapture represents the last 5-minute capture of a MBus device.

func (*LastCapture) Position

func (l *LastCapture) Position() lexer.Position

type LegacyLastCapture

type LegacyLastCapture struct {
	Pos lexer.Position `parser:""`

	// We ignore any extraneous values between timestamp and OBIS as specs are unclear about their purpose.
	Timestamp *String            `parser:"@@ ')' ( '(' ~(')' | OBIS) ')' (?='(') )+ '('"`
	OBIS      *OBIS              `parser:"@@ ')' '('"`
	Value     *LegacyMeasurement `parser:"@@"`
}

LegacyLastCapture represents the last 5-minute capture of an older MBus device (DSMR v2.2 or v3.0).

func (*LegacyLastCapture) Position

func (l *LegacyLastCapture) Position() lexer.Position

type LegacyMeasurement

type LegacyMeasurement struct {
	Pos lexer.Position `parser:""`

	Unit  *String `parser:"@@ ')' '('"`
	Value *Number `parser:"@@"`
}

LegacyMeasurement represents a number+unit of a LegacyLastCapture.

func (*LegacyMeasurement) Position

func (m *LegacyMeasurement) Position() lexer.Position

type MBusDevice

type MBusDevice struct {
	Pos     lexer.Position
	Channel int
	Data    []*Object
}

MBusDevice groups OBIS objects that belong to the same M-Bus channel.

func (*MBusDevice) Key

func (m *MBusDevice) Key() string

func (*MBusDevice) Position

func (m *MBusDevice) Position() lexer.Position

type Measurement

type Measurement struct {
	Pos lexer.Position `parser:""`

	Value *Number `parser:"@@"`
	Unit  *String `parser:"'*' @@"`
}

Measurement represents a number+unit.

func (*Measurement) Position

func (m *Measurement) Position() lexer.Position

type Node

type Node interface {
	Position() lexer.Position
	// contains filtered or unexported methods
}

Grammar overview (simplified EBNF):

Telegram   = Header Entry* Footer ;
Entry      = Header | Footer | Object | MBusDevice ;
Object     = OBIS "(" Value* ")" ;
MBusDevice = Object { Object }  // same channel, leading OBIS 0-<channel>:*
Value      = EventLog | LastCapture | LegacyLastCapture | Measurement | Timestamp | String ;
EventLog   = Number ")" "(" OBIS ")" Event* ;
Event      = "(" Timestamp ")" "(" Measurement [")"] ;
LastCapture = Timestamp ")" "(" Measurement ;
Measurement = Number "*" String ;
Timestamp  = <12 digit datetime> ["S" | "W"] ;
String     = <any chars except ')' or EOL> ;
Number     = ["+" | "-"] digit+ ["." digit+] ;
Header/Footer tokens reuse String rules for their payloads.

Node expresses the common behaviour for every AST node in a telegram.

type Number

type Number struct {
	Pos lexer.Position `parser:""`

	Value *big.Float `parser:"@Number"`
}

Number wraps a numeric literal optionally containing a decimal point.

func (*Number) Position

func (n *Number) Position() lexer.Position

type OBIS

type OBIS struct {
	Pos lexer.Position `parser:""`

	Value string `parser:"@OBIS"`
}

OBIS holds the identifier of a COSEM object.

func (*OBIS) Position

func (o *OBIS) Position() lexer.Position

type Object

type Object struct {
	Pos lexer.Position `parser:""`

	OBIS  *OBIS `parser:"@@"`
	Value Value `parser:"'(' @@* ')' (?=EOL)"`
	// contains filtered or unexported fields
}

Object represents a COSEM object with one or more values.

func (*Object) Key

func (o *Object) Key() string

func (*Object) Position

func (o *Object) Position() lexer.Position

type Option

type Option func(opts *parseOptions) error

func VerifyChecksum

func VerifyChecksum(v bool) Option

type ParseError

type ParseError struct {
	Pos        lexer.Position
	Message    string
	Unexpected string
	Err        error
}

ParseError wraps errors returned by participle with position information and context.

func (*ParseError) Error

func (e *ParseError) Error() string

func (*ParseError) Unwrap

func (e *ParseError) Unwrap() error

type String

type String struct {
	Pos lexer.Position `parser:""`

	// Also check for `EOL` token so both Header and Footer can use this Value struct as well.
	Value string `parser:"@(~(')' | EOL)+)"`
}

String represents a literal string segment.

func (*String) Position

func (s *String) Position() lexer.Position

type Telegram

type Telegram struct {
	Pos lexer.Position `parser:""`

	Header *Header `parser:"@@"`
	Data   Data    `parser:"@@*"`
	Footer *Footer `parser:"@@"`
}

Telegram is the root node produced for every parsed message.

func Parse

func Parse(str string, options ...Option) (*Telegram, error)

Parse parses telegram from a string.

func (*Telegram) Position

func (t *Telegram) Position() lexer.Position

type Timestamp

type Timestamp struct {
	Pos lexer.Position `parser:""`

	Value string       `parser:"@Timestamp"`
	DST   DSTIndicator `parser:"@('S' | 'W')"`
}

Timestamp represents a timestamp of a date.

func (*Timestamp) IsDST

func (t *Timestamp) IsDST() bool

IsDST reports whether the timestamp is in daylight saving time (summer time).

func (*Timestamp) Position

func (t *Timestamp) Position() lexer.Position

type Value

type Value interface {
	Node
	// contains filtered or unexported methods
}

Value represents the possible values attached to an OBIS object.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL