Reading modbus kWh meters using EmonHub (SDM120 and SDM630)

I would like to meter my household, heat pump and solar panels. Our household has a 3 phase connection to the grid. With a single emonTx I would only be able to use an approximate method to estimate the power. Could be ‘good enough’, but I still went for modbus kWh meters, DIN rail mounted. I share my steps: you never know someone wants a similar setup or that someone can improve my steps.

The hardware I use:

  • 2x Eastron SDM120M, direct connect, modbus version, one for our heat pump and one for our solar panels.
  • 1x Eastron SDM630-Modbus V2, direct connec,t modbus version, for our 3 phase household.
  • A computer to read register values. During testing I used a regular laptop with Lubuntu 20.04 LTS. In the future this should be a Raspberry Pi, Odroid C4, … or any other smaller and less energy hungry computer.
  • RS485-USB adapter, to be able to read the values through modbus. There are different models, although most have the same chipset (eg. ch341, CP2102, FTDI232).

Some remarks:

  • The SDM120 and SDM630 is a series with each 7 models. I use the ‘direct connected’ model: the disadvantage is that the meters must be installed in-line and so they are invasive (in contrast with Current Transformers). Handle with care if you disconnect, rewire and reconnect all the cables!
  • DIN rail mounted, so that I can integrate them in my fuse board. So you need some space to put them, which I have.
  • Modbus is a data communications protocol to read or store values in a device. You can read every meter on its own communications cable, but it also supports communication to and from multiple devices connected to the same cable. Then you must connect the meters in a daisy-chain fashion, so that you can use one serial link to read the bus. As electrical interface RS-485 is used.

The steps I did

  • Connect one SDM120 to the grid (not in the fuse board yet) and read it on a regular Lubuntu 20.04 laptop.
  • Make a daisy chain with more kWh meters and read them.
  • Configure emonhub and emoncms so that an interfacer can be used to communicate over RS485 with emonhub and store and plot the values in emoncms.
  • Mount everything in the fuse board (on my 2DO list).
  • Install everything on a much smaller computer than a laptop (on my 2DO list).

The details follow. As you can see on my 2DO list: this is only a try-out and a work in progress. Some basic knowledge of an Ubuntu base operating system, emoncms and emonhub will be necessary.

Preparing computer for modbus reading

Open a terminal (ctrl + alt + t) and execute ls -l /dev/ttyU*. Connect the RS485 USB adapter and use ls -l /dev/ttyU* again: the one that is extra is the USB port assignment. Changes are big it is /dev/ttyUSB0. Problems? The commands dmesg or lsusb provide extra information.

To avoid having to use sudo to execute the script, add the current user to the dialout group, so that that user can read /dev/ttyUSB0.

sudo adduser $USER dialout

The new permission is only effective after logging out and back in again!

Python3 is already installed by default on my setup, but I still have to install minimalmodbus, a Modbus RTU (and Modbus ASCII) implementation for Python. And for that Python module to install, I need pip3.

sudo apt install python3-pip
sudo pip3 install minimalmodbus

SDM120

Documentation:

Wiring

The official wiring diagram is on the left, on the right two possible wirings to try out. As it is a try out, I just used some plugs and sockets I had lying around. The RS-485 cabling is also with some simple cabling.

To connect my first SDM120, I used the wiring scheme in the middle. See my picture. The black cable goes to the plug. You can ignore the small gray cable at the moment: that’s already my daisy chain for the next SDM120M. You can also ignore the yellow cable: it’s not connected on any end. I could have use it to connect ground for modbus, but it works fine without it.

Reading values

Connect the plug to the grid: the display will light up and cycle through some values (eg. the voltage on the grid).

Connect the RS485 adapter to the computer. Time to test the reading with the following python script you can execute in a terminal:

#!/usr/bin/env python3
import minimalmodbus

addr = 1
instrument = minimalmodbus.Instrument('/dev/ttyUSB0', addr)  # port name, slave address (in decimal)

instrument.serial.baudrate = 9600         # Baud
instrument.serial.bytesize = 8
instrument.serial.parity   = minimalmodbus.serial.PARITY_NONE
instrument.serial.stopbits = 1
instrument.serial.timeout  = 1          # seconds
instrument.mode = minimalmodbus.MODE_RTU   # rtu or ascii mode

registers = [ 0,  6, 12, 18,    24,  30, 70,   72,   74,     76,     78,   84,   86,  88,    90,   92,   94, 258,  264,  342,    344]
names =     ["V","I","P","S",   "Q","PF","f","IAE","EAE",  "IRE",  "ERE","TSP","MSP","ISP","MIP","ESP","MEP","ID","MID","TAE",  "TRE"]
units =     ["V","A","W","VA","var", "","Hz","kWh","kWh","kvarh","kvarh",  "W",  "W",  "W",  "W",  "W",  "W", "A",  "A","kWh","kvarh"]
info = [
"(V for Voltage in volt)",
"(I for Current in ampere)",
"(P for Active Power in watt)",
"(S for Apparent power in volt-ampere)",
"(Q for Reactive power in volt-ampere reactive)",
"(PF for Power Factor)",
"(f for Frequency in hertz)",
"(IAE for Import active energy in kilowatt-hour)",
"(EAE for Export active energy in kilowatt-hour)",
"(IRE for Import reactive energy in kilovolt-ampere reactive hours)",
"(ERE for Export reactive energy in kilovolt-ampere reactive hours)",
"(TSP for Total system power demand in watt)",
"(MSP for Maximum total system power demand in watt)",
"(ISP for Import system power demand in watt)",
"(MIP for Maximum import system power demand in watt)",
"(ESP for Export system power demand in watt)",
"(MEP for MaximumExport system power demand in watt)",
"(ID for current demand in ampere)",
"(MID for Maximum current demand in ampere)",
"(TAE for Total active energy in kilowatt-hour)",
"(TRE for Total reactive energy in kilovolt-ampere reactive hours)",
]

print ("=== General info about address", addr, "===")
print (instrument)

print ("=== The registers for address", addr, "===")
for i in range(len(registers)):
    value = instrument.read_float(registers[i], 4, 2)
    print (str(registers[i]).rjust(3), str(value).rjust(20), units[i].ljust(5), info[i])

print ("")

An example of an output, with a toaster in the plug.

$ python3 ./sdm120-basic.py 
=== General info about address 1 ===
minimalmodbus.Instrument<id=0x7fdb1dcdfdf0, address=1, mode=rtu, close_port_after_each_call=False, precalculate_read_size=True, clear_buffers_before_each_transaction=True, handle_local_echo=False, debug=False, serial=Serial<id=0x7fdb1dcf8370, open=True>(port='/dev/ttyUSB0', baudrate=9600, bytesize=8, parity='N', stopbits=1, timeout=1, xonxoff=False, rtscts=False, dsrdtr=False)>
=== The registers for address 1 ===
  0    242.6999969482422 V     (V for Voltage in volt)
  6    4.927000045776367 A     (I for Current in ampere)
 12   1198.0999755859375 W     (P for Active Power in watt)
 18      1198.1591796875 VA    (S for Apparent power in volt-ampere)
 24                  0.0 var   (Q for Reactive power in volt-ampere reactive)
 30   0.9999989867210388       (PF for Power Factor)
 70   50.040000915527344 Hz    (f for Frequency in hertz)
 72    8.428999900817871 kWh   (IAE for Import active energy in kilowatt-hour)
 74                  0.0 kWh   (EAE for Export active energy in kilowatt-hour)
 76     2.99399995803833 kvarh (IRE for Import reactive energy in kilovolt-ampere reactive hours)
 78   1.0479999780654907 kvarh (ERE for Export reactive energy in kilovolt-ampere reactive hours)
 84    8.363028526306152 W     (TSP for Total system power demand in watt)
 86     1321.80126953125 W     (MSP for Maximum total system power demand in watt)
 88    8.341917037963867 W     (ISP for Import system power demand in watt)
 90     1321.80126953125 W     (MIP for Maximum import system power demand in watt)
 92 0.021111104637384415 W     (ESP for Export system power demand in watt)
 94  0.02305554784834385 W     (MEP for MaximumExport system power demand in watt)
258  0.03573137894272804 A     (ID for current demand in ampere)
264    6.728886127471924 A     (MID for Maximum current demand in ampere)
342    8.428999900817871 kWh   (TAE for Total active energy in kilowatt-hour)
344    4.041999816894531 kvarh (TRE for Total reactive energy in kilovolt-ampere reactive hours)

Problems? Check:

  • Do the settings of SDM120 (like baud, …) match those in the script? You can cycle through them with the button on the menu of the meter.
  • Is the RS485 adapter available at /dev/ttyUSB0? Unplug, replug and check dmesg.
  • Is the A and B cabling for RS485 okay? Even then: some manufacturers unfortunately mix them up, so switch them at one end and retry the script.

Add a SDM120

Wiring

This time I used the other way of wiring (see scheme I posted earlier) with an old distribution plug where I cut the wiring in half.

I daisy chained my extra SDM120 on the left to the existing on the right. Twisted pair cabling and a termination resistor would be better, but at the moment the distance is so short that it should work out just fine.

SDM120_daisy_chain

In the picture, the daisy chain to the SDM630 is already attached by using another gray cable, but you can ignore it at the moment.

Reading values

The extra SDM120 also has 1 as default address, but every device on the daisy chain has to have a unique one. The series manual tells me I can keep pressing the button for 3 seconds to enter set-up mode. I changed it to address 2 and kept the other settings.

I can change the address in the already posted script and execute to test. To really test the daisy chain, I wrote the script sdm120-daisy.py below. The differences compared to the first script:

  • An extra array addresses = [1,2] to hold the addresses that I want to read.
  • An extra loop for addr in addresses:, where I change the address I want to read with instrument.address = addr.
  • A time.sleep(0.1) so that the script doesn’t try to read the next device too fast otherwise I would get a minimalmodbus.NoResponseError. That’s why I also needed import time at the start of the script.
    After some trial and error, the value of 0.1 seemed fine. Your setup might need another value.
#!/usr/bin/env python3
import minimalmodbus
import time

addr = 1
instrument = minimalmodbus.Instrument('/dev/ttyUSB0', addr)  # port name, slave address (in decimal)

instrument.serial.baudrate = 9600         # Baud
instrument.serial.bytesize = 8
instrument.serial.parity   = minimalmodbus.serial.PARITY_NONE
instrument.serial.stopbits = 1
instrument.serial.timeout  = 1          # seconds
instrument.mode = minimalmodbus.MODE_RTU   # rtu or ascii mode

addresses = [1,2]
registers = [ 0,  6, 12, 18,    24,  30, 70,   72,   74,     76,     78,   84,   86,  88,    90,   92,   94, 258,  264,  342,    344]
names =     ["V","I","P","S",   "Q","PF","f","IAE","EAE",  "IRE",  "ERE","TSP","MSP","ISP","MIP","ESP","MEP","ID","MID","TAE",  "TRE"]
units =     ["V","A","W","VA","var", "","Hz","kWh","kWh","kvarh","kvarh",  "W",  "W",  "W",  "W",  "W",  "W", "A",  "A","kWh","kvarh"]
info = [
"(V for Voltage in volt)",
"(I for Current in ampere)",
"(P for Active Power in watt)",
"(S for Apparent power in volt-ampere)",
"(Q for Reactive power in volt-ampere reactive)",
"(PF for Power Factor)",
"(f for Frequency in hertz)",
"(IAE for Import active energy in kilowatt-hour)",
"(EAE for Export active energy in kilowatt-hour)",
"(IRE for Import reactive energy in kilovolt-ampere reactive hours)",
"(ERE for Export reactive energy in kilovolt-ampere reactive hours)",
"(TSP for Total system power demand in watt)",
"(MSP for Maximum total system power demand in watt)",
"(ISP for Import system power demand in watt)",
"(MIP for Maximum import system power demand in watt)",
"(ESP for Export system power demand in watt)",
"(MEP for MaximumExport system power demand in watt)",
"(ID for current demand in ampere)",
"(MID for Maximum current demand in ampere)",
"(TAE for Total active energy in kilowatt-hour)",
"(TRE for Total reactive energy in kilovolt-ampere reactive hours)",
]
for addr in addresses:
    instrument.address = addr
    print ("=== General info about address", addr, "===")
    print (instrument)
    print ("=== The registers for address", addr, "===")
    for i in range(len(registers)):
        value = instrument.read_float(registers[i], 4, 2)
        print (str(registers[i]).rjust(3), str(value).rjust(20), units[i].ljust(5), info[i])
    time.sleep(0.1) # To avoid minimalmodbus.NoResponseError
    print ("")

You can avoid the time.sleepby using try-except-else. I have included an alternative (but more complex) script as an attachment.

sdm120_daisychain_alt.txt (2.6 KB)

Add a SDM630

Another type, so other documentation:

Wiring

The household is 3 phase, but my regular sockets only have 1 phase, so to test the meter during my try-out I used the ‘single phase two wires’ setup. And I added the SDM630 to the daisy chain. On the bottom I only use brown and blue (yellow and black are not connected). On the top the smaller white cable goes to a plug, so that I can plug it into a regular socket.

Reading values

Again all addresses need to be unique. The series manual has a chapter ‘Set Up’ and a subchapter ‘RS485 Address’ to help me change the address to 3.

Luckily the registers that are used in the SDM120 are also present in the SDM630. So after changing the address in the code (or add it to the array), I can use the same script to test. You could extend the script to read more registers by reading the SDM630_modbus manual (eg. ‘Phase 2 line to neutral volts’ is at register 2).

emoncms and emonhub

I continue to use my laptop to test, before moving on to a smaller computer and the fuse board. This sections assumes you have emoncms (to read, store and plot the values) and emonhub (sits in between the meters and emonhub) up and running on that laptop. If not, you can follow these steps.

EmonHubMinimalModbusInterfacer.py

Emonhub includes some interfacers by default, eg. “Reading from a SDM120 single-phase meter”. This solution doesn’t allow you to do much settings as eg. choosing registers, applying it to a different modbus meter, … So I’ll use another solution.

Work is being done on a branch “minimalmodbus” to include a EmonHubMinimalModbusInterfacer.py so that also other modbus devices can be read. At the time of writing that new interfacer:

  • … allows you to change device (eg. /dev/ttyUSB0) and baud (eg. 9600) in emonhub.conf.
  • … allows you to change the address of the modbus device in EmonHubMinimalModbusInterfacer.py (eg. 1 or 2 or 3).
  • … can read registers that store a float, with functioncode 4 and number_of_registers 2.
  • … doesn’t support multiple devices on the same serial link (daisy chaining)… yet, but discussion is going on.

It looks the way to go, so after you have got a default emoncms and emonhub running, we will get the code for EmonHubMinimalModbusInterfacer.py:

cd /opt/openenergymonitor/emonhub
git fetch --all
git checkout minimalmodbus
git pull
sudo service emonhub restart

The emonhub restart on my setup always gives the error below, but it always works on reboot.

Failed to restart emonhub.service: Unit var-log.mount not found.

Add the following to /etc/emonhub/emonhub.conf:

[[SDM120_1]]
    Type = EmonHubMinimalModbusInterfacer
    [[[init_settings]]]
        device = /dev/ttyUSB0
        baud = 9600
    [[[runtimesettings]]]
        pubchannels = ToEmonCMS,
        read_interval = 10
        nodename = heatpump
        registers = 0,6,12,18,30,70,72,74,76
        names = V,I,P,VA,PF,FR,EI,EE,RI
        precision = 2,3,1,1,3,3,3,3,3

Reboot and go to http://localhost/ where you should see the register values on the input page. From there on you can configure your feeds and graphs. Interesting values for a ‘Log to feed’ seem to be the registers ‘Import active energy’ (heat pump), ‘Export active energy’ (PV) and a combination of those (3 phase household on SDM630). In that way the consumption or production in kWh can be measured. Also ‘Active Power’ in watt seems interesting.

Multiple RS-485 adapters

I hope emonhub can soon read other meters on a daisy chain. As I wrote, discussion is going on :-). If you already want to use 3 kWh meters now, you could use 3 RS-485 USB adapters, so each meter has it own serial link. Then you avoid using a daisy chained serial link which EmonHubMinimalModbusInterfacer.py can’t handle at the moment.

What you don’t want is eg. /dev/ttyUSB0 and /dev/ttyUSB1 mixing up after a reboot. Then the monitoring of the heatpump could be seen at the PV (and vice versa). The stackexchange question “How to bind USB device under a static name?” explains how to avoid this. Unfortunately some batches of adapters don’t have a unique idVendor, idProduct or serial. As a workaround I’ll use the usb port they are connected to (see How to distinguish between identical USB-to-serial adapters?). The disadvantage is that I have to always connect an adapter to the same USB port. In short for my own setup (please read the guides to better understand what is going on):

$ udevadm info -a -n /dev/ttyUSB0 | grep -i kernels
    KERNELS=="ttyUSB0"
    KERNELS=="5-1:1.0"
    KERNELS=="5-1"
    KERNELS=="usb5"
    KERNELS=="0000:00:1d.0"
    KERNELS=="pci0000:00"

Having a udev-rule at /etc/udev/rules.d/99-usb-serial.rules. For my situation:

KERNEL=="ttyUSB*", KERNELS=="7-1:1.0", SYMLINK+="ttyUSB_heatpump"
KERNEL=="ttyUSB*", KERNELS=="6-2:1.0", SYMLINK+="ttyUSB_PV"
KERNEL=="ttyUSB*", KERNELS=="6-1:1.0", SYMLINK+="ttyUSB_household"

Reload the udev-rules and do a check:

$ sudo udevadm trigger
$ ls -l /dev/ttyUSB*
$ ls -l /dev/ttyUSB*
crw-rw---- 1 root dialout 188, 0 mei  1 12:11 /dev/ttyUSB0
crw-rw---- 1 root dialout 188, 1 mei  1 12:11 /dev/ttyUSB1
crw-rw---- 1 root dialout 188, 2 mei  1 12:11 /dev/ttyUSB2
lrwxrwxrwx 1 root root         7 mei  1 12:11 /dev/ttyUSB_heatpump -> ttyUSB0
lrwxrwxrwx 1 root root         7 mei  1 12:11 /dev/ttyUSB_household -> ttyUSB2
lrwxrwxrwx 1 root root         7 mei  1 12:11 /dev/ttyUSB_PV -> ttyUSB1

Two things left to do:

  1. EmonHubMinimalModbusInterfacer.py doesn’t support setting a different modbus address than 1 (yet). So all kWh meters should have 1 as address.
  2. Edit /etc/emonhub/emonhub.conf accordingly:
[interfacers]
### From https://community.openenergymonitor.org/t/reading-from-a-sdm120-meter-using-emonhub/16475/90
[[SDM120_1]]
    Type = EmonHubMinimalModbusInterfacer
    [[[init_settings]]]
        device = /dev/ttyUSB_heatpump
        baud = 9600
    [[[runtimesettings]]]
        pubchannels = ToEmonCMS,
        read_interval = 10
        nodename = heatpump
        registers = 0,6,12,18,30,70,72,74,76
        names = V,I,P,VA,PF,FR,EI,EE,RI
        precision = 2,3,1,1,3,3,3,3,3

[[SDM120_2]]
    Type = EmonHubMinimalModbusInterfacer
    [[[init_settings]]]
        device = /dev/ttyUSB_PV
        baud = 9600
    [[[runtimesettings]]]
        pubchannels = ToEmonCMS,
        read_interval = 10
        nodename = pv 
        registers = 0,6,12,18,30,70,72,74,76
        names = V,I,P,VA,PF,FR,EI,EE,RI
        precision = 2,3,1,1,3,3,3,3,3

[[SDM630]]
    Type = EmonHubMinimalModbusInterfacer
    [[[init_settings]]]
        device = /dev/ttyUSB_household
        baud = 9600
    [[[runtimesettings]]]
        pubchannels = ToEmonCMS,
        read_interval = 10
        nodename = household
        registers = 0,6,12,18,30,70,72,74,76
        names = V,I,P,VA,PF,FR,EI,EE,RI
        precision = 2,3,1,1,3,3,3,3,3
2 Likes

A big thumbsup to you, sir. Nice work!

Eastron does manufacture an SDM120 (as well as an SDM630) that uses Current Transformers.

The downside here is the use of 5 Amp CTs. They present hazards of their own, but at least they
don’t require any re-wiring as the in-line models do.


The modbus spec says instruments must be installed in daisy-chain fashion.
Stubs/spurs can be used if the stub/spur length is kept short, but they’re not recommended.

RS485 to USB adapters with CP2102 and FTDI232 chipsets are widely available too. :wink:

lsusb will show if the adapter has been recognized.
ls -l /dev/ttyU* will show USB port assignments, although not necessarily which port the USB adapter is on if more than one USB adapter is connected.
(the only advantage here is eliminating the need to peruse the dmesg file)

Unless I’m mistaken (and I could well be) I think you don’t need Modbus ASCII.
(Modbus ASCII and Modbus RTU are not compatible with each other)

Thank you for reading my post and providing feedback! I have edited my post accordingly.

The series versus models should be more clear now, as well as the correct usage of the daisy chain. I have given more chipset examples as well.

I have changed my steps as follows:

I don’t need the Modbus ASCII part of the implementation indeed, but it was just an explanation/definiton of what minimalmodbus is :-).

Thanks again for your feedback!

Looks good.

Between Modbus RTU and Modbus ASCII, RTU sees much more use.
And as you’re not using the ACSII variant, I’d say eliminate the mention of it to avoid any confusion.

@Bill.Thomson, I wanted to edit my first message (to eliminate the ASCII mention and provide some sort of TOC), but the pencil icon seems to be gone. Maybe it is removed after some time or after some replies?

TBH, I don’t know. Let’s see what Gwil says…

@Gwil, shouldn’t he be able to edit his own posts regardless of their age, or is there a limit?

By default, Discourse sets a time limit for how long new users can edit their posts.

I think it is done to prevent someone from changing a good, relevant and harmless looking post into spam or malicious content without the moderators noticing.

1 Like

@Gwil, I guess that will be the reason. Apparently the post edit time limit defaults to 86400 minutes (2 months). It would be nice if an admin could change a specific post to be editable by the original author on any time. Maybe this Wiki Post feature could be something that solves this issue? I’ve only just read about it and so I have never used it.

Excellent stuff, thank you for capturing all this. It helped me resolve my own modbus SDM120M issue (MID ID set to different values vs. 1 hardcoded)

1 Like

Excellent write up. Just what I needed. It worked instantly. Before this I used PyModBus, but could not get that to work.
I wanted to raise the baudrate to 38400. According to the manual that must work. See here: https://www.etteam.com/productDIN/SDM120-MODBUS/SDM120-PROTOCOL.pdf
I will fiddle around with the code: first to see if I can read the baudrate modbus register and then try to modify it.

1 Like

Just like to say thanks as I used your script to test a data link I created using 433Mhz to RS485 over 200mtrs and it worked really well at 9600 which is all I need