An open API service indexing awesome lists of open source software.

https://github.com/databus23/go-sml

pure-Go library for decoding Smart Message Language (SML) data
https://github.com/databus23/go-sml

Last synced: 14 days ago
JSON representation

pure-Go library for decoding Smart Message Language (SML) data

Awesome Lists containing this project

README

          

# go-sml

A pure-Go library for decoding [Smart Message Language (SML)](https://de.wikipedia.org/wiki/Smart_Message_Language) data from German smart electricity meters. Zero external dependencies.

```go
go get github.com/databus23/go-sml
```

## Quick Start

### CLI tool

A small command-line tool is included that prints readings in the same format
as the C `sml_server`:

```bash
go install github.com/databus23/go-sml/cmd/smlreader@latest
smlreader capture.bin
# or from stdin:
smlreader -
```

Output:

```
1-0:1.8.0*255#8391648.8#Wh
1-0:15.7.0*255#163.5#W
```

### As a library

Decode a raw SML payload and print all meter readings:

```go
file, err := sml.Decode(payload)
if err != nil {
log.Fatal(err)
}
for _, entry := range file.Readings() {
if v, ok := entry.ScaledValue(); ok {
fmt.Printf("%s = %f %s\n", entry.OBISString(), v, entry.UnitString())
}
}
// Output:
// 1-0:1.8.0*255 = 8391648.800000 Wh
// 1-0:16.7.0*255 = 163.500000 W
```

## Streaming from a Serial Port

For continuous reading from a serial device (or any `io.Reader`), use `Listen`.
It handles transport framing, escape sequences, and CRC validation automatically:

```go
port, _ := os.Open("/dev/ttyUSB0")
defer port.Close()

err := sml.Listen(ctx, port, func(file *sml.File) error {
for _, entry := range file.Readings() {
if v, ok := entry.ScaledValue(); ok {
fmt.Printf("%s = %.1f %s\n", entry.OBISString(), v, entry.UnitString())
}
}
return nil
})
```

Use `ListenMessages` instead if you want per-message callbacks rather than
per-file.

## API Overview

### Root package `sml`

| Symbol | Description |
|--------|-------------|
| `Decode(data) (*File, error)` | Decode a complete SML payload (strict mode) |
| `DecodeWithOptions(data, opts)` | Decode with configurable strictness |
| `Listen(ctx, r, handler)` | Stream-decode from an `io.Reader` |
| `ListenMessages(ctx, r, handler)` | Like `Listen`, but calls handler per message |
| `File.Readings()` | Collect all `ListEntry` values from `GetListResponse` messages |
| `ListEntry.ScaledValue()` | Apply scaler, return `float64` |
| `ListEntry.OBISString()` | Format OBIS code as `"A-B:C.D.E*F"` |
| `ListEntry.UnitString()` | DLMS unit name (e.g. `"Wh"`, `"W"`, `"V"`) |
| `UnitName(code)` | Look up DLMS unit code directly |

### Sub-package `transport`

| Symbol | Description |
|--------|-------------|
| `NewReader(r) *Reader` | Wrap an `io.Reader` to extract SML transport frames |
| `Reader.Next() ([]byte, error)` | Return next validated frame payload |

The transport layer auto-detects CRC-16/X.25 (standard) and CRC-16/Kermit
(Holley DTZ541 meters).

### Value Types

SML values preserve their wire encoding width. Use a type switch on the
`Value` interface:

```go
switch v := entry.Value.(type) {
case sml.Uint32:
fmt.Println("unsigned 32-bit:", uint32(v))
case sml.Int16:
fmt.Println("signed 16-bit:", int16(v))
case sml.OctetString:
fmt.Printf("bytes: %x\n", []byte(v))
}
```

Available types: `OctetString`, `Bool`, `Int8`, `Int16`, `Int32`, `Int64`,
`Uint8`, `Uint16`, `Uint32`, `Uint64`.

### Message Body Types

`Message.Body` implements `MessageBody` and is one of:

- `*OpenResponse`
- `*CloseResponse`
- `*GetListResponse` (contains meter readings)
- `*AttentionResponse`
- `*GetProcParameterResponse`
- `*GetProfileListResponse`
- `*GetProfilePackResponse`

## Testing

### Run all tests

```bash
go test ./...
```

### Integration tests only

The integration tests decode 37 real meter captures and compare the output
against golden files generated by the C reference implementation
([libsml](https://github.com/volkszaehler/libsml)):

```bash
go test -run TestGoldenFiles -v
```

Test data comes from [devZer0/libsml-testing](https://github.com/devZer0/libsml-testing)
and covers 15+ meter models from manufacturers including EMH, DZG, Holley,
ISKRA, EasyMeter, eBZ, ITRON, and DrNeuhaus.

### Fuzz tests

```bash
go test -fuzz FuzzDecode -fuzztime 30s
go test -fuzz FuzzTransportReader -fuzztime 30s
```

## Regenerating Golden Files

The golden files in `testdata/golden/` are generated by piping each binary
capture through the C `sml_server` example program from
[volkszaehler/libsml](https://github.com/volkszaehler/libsml).

### Build sml_server from source

```bash
git clone https://github.com/volkszaehler/libsml /tmp/libsml
cd /tmp/libsml
make

# On macOS the shared library target may fail — only the static library
# is needed. Link sml_server manually:
cc -o sml_server examples/sml_server.c -Isml/include sml/lib/libsml.a -lm
```

### Generate golden files

```bash
for f in testdata/*.bin; do
name=$(basename "$f" .bin)
/tmp/libsml/sml_server - < "$f" > "testdata/golden/${name}.txt" 2>&1
done
```

The golden file format is one line per `GetListResponse` list entry:

```
OBIS#value#unit
```

where OBIS is `A-B:C.D.E*F`, value is the scaled numeric reading (or hex-formatted octet string), and unit is the DLMS unit name.

### Using Docker (no local C toolchain)

```bash
docker run --rm -v "$(pwd)":/work -w /work gcc:latest bash -c '
apt-get update -qq && apt-get install -y -qq uuid-dev > /dev/null 2>&1
git clone --quiet https://github.com/volkszaehler/libsml /tmp/libsml
cd /tmp/libsml && make > /dev/null 2>&1
for f in /work/testdata/*.bin; do
name=$(basename "$f" .bin)
./examples/sml_server - < "$f" > "/work/testdata/golden/${name}.txt" 2>&1
done
'
```

## License

Apache License 2.0 — see [LICENSE](LICENSE).

## Acknowledgements

- [volkszaehler/libsml](https://github.com/volkszaehler/libsml) — C reference implementation
- [devZer0/libsml-testing](https://github.com/devZer0/libsml-testing) — Real meter captures used for integration testing