Sometimes a quick and dirty solution is all we need. At the moment, I’m in need of granular data about the load on my electric cabling.
Voltie load-management Link to heading
I have 3 Voltie electric car chargers at home, which are equipped with a load-sharing feature. This means they automatically calculate the maximum charging current depending on what is available at my electricity meter.1
The load-management is achieved by installing a Smart EVSE Sensorbox with current probes on all 3 phases2, which is then connected to a USR-DR302 Modebus to ethernet gateway3. One of the Voltie chargers periodically polls the Sensorbox over Modbus RTU over TCP/IP. That’s a lot of encapsulation, but it works.
Parse Modbus by packet timing Link to heading
There’s no official way to get the raw current data out of this system, but the USR-DR302 is capable of serving multiple clients over TCP, and forwards any Modbus replies to all connected clients. Since the Voltie system already polls the Sensorbox periodically, it is possible to just listen-in to the replies containing the current-transformer data.
There’s one catch: the Modbus protocol does not seem to have an easy way of identifying the beginning of a modbus packet when sent over TCP. All packets have deterministic lengths, but to parse a stream reliably I’d need a semi-complete modbus implementation. I’d like to avoid that.4
Instead of proper Modbus parsing, I can rely on the fact that there’s a little bit of delay between packets. This is absolutely a hack. The Modbus protocol only guarantees 7 chars of time between packets, around 6ms in my case with 9600 baud. This is well within the range of a possible network latency, and even on a quick network there are no timing guarantees made by the Modbus to Ethernet converter either.
Despite all the reasons it shouldn’t, delay-based packet separation will still work. Voltie only queries the current data approximately every second, all devices are on the same network with less than 10ms latency, and there’s no other traffic on the modbus. With that in mind, I’ve made a few lines of go code to listen-in on the measurements and output a csv.
The code Link to heading
package main
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"net"
"os"
"time"
)
func crc(data []byte) uint16 {
var crc16 uint16 = 0xffff
l := len(data)
for i := 0; i < l; i++ {
crc16 ^= uint16(data[i])
for j := 0; j < 8; j++ {
if crc16&0x0001 > 0 {
crc16 = (crc16 >> 1) ^ 0xA001
} else {
crc16 >>= 1
}
}
}
return crc16
}
const ip = "192.0.2.20:6002"
func main() {
r, err := net.DialTimeout("tcp", ip, time.Second*3)
if err != nil {
panic(err)
}
b := make([]byte, 256)
nextpacket:
for {
derr := r.SetReadDeadline(time.Now().Add(time.Second * 3))
if derr != nil {
panic(derr)
}
l, err := r.Read(b)
if err != nil {
if errors.Is(err, os.ErrDeadlineExceeded) {
fmt.Fprintf(os.Stderr, "Missing modbus packets!\n")
for {
r, err = net.DialTimeout("tcp", ip, time.Second*3)
if err == nil {
fmt.Fprintf(os.Stderr, "Reconnect successful.\n")
break
}
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
time.Sleep(time.Second * 3)
}
} else {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
}
continue
}
for {
derr := r.SetReadDeadline(time.Now().Add(time.Millisecond * 100))
if derr != nil {
panic(derr)
}
a, err := r.Read(b[l:])
l += a
if err != nil {
if errors.Is(err, os.ErrDeadlineExceeded) {
break
}
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
continue nextpacket
}
}
if l != 45 || b[0] != 0x0a || b[1] != 0x04 {
continue
}
var a1, a2, a3 float32
e1 := binary.Read(bytes.NewReader(b[31:35]), binary.BigEndian, &a1)
e2 := binary.Read(bytes.NewReader(b[35:39]), binary.BigEndian, &a2)
e3 := binary.Read(bytes.NewReader(b[39:43]), binary.BigEndian, &a3)
var crcread uint16
e4 := binary.Read(bytes.NewReader(b[43:45]), binary.LittleEndian, &crcread)
crccalc := crc(b[:43])
var e5 error
if crccalc != crcread {
e5 = errors.New("Crc mismatch!\n")
}
if err := errors.Join(e1, e2, e3, e4, e5); err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
continue
}
fmt.Printf("%s;%.2f;%.2f;%.2f\n", time.Now().Format("2006-01-02 15:04:05"), a1, a2, a3)
}
}
Edit the IP-address in the code, and off you go with go run smartevse-sensorbox-reader.go > output.csv
.5 Be careful with nohup mixing error messages into the output though.
Check out the Voltie website for information, or watch this unlisted Voltie Sensorbox installation video peek behind the curtains. ↩︎
The GitHub repository of Sensorbox-2 contains information about the Modbus messages used. ↩︎
Check the manufacturer’s website for USR-DR302. ↩︎
A great 3rd-party resource is the modbus tools documentation which is more concise than the official Modbus specifications. ↩︎
This code is also available as a GitHub gist. The code has been updated to avoid hitting an issue with SetDeadline blocking indefinitely. ↩︎