PZEM-016 single phase modbus energy meter

I’ve seen the same thing. I had placed a second order for a 014 and that arrived yesterday. On it’s own the 016 works fine, when I have the 2 connected one will fail to respond quite frequently. I too checked and played with the cable type, lengths, shield and termination resistors to no avail, what I also noticed using the debug mode in minimalmodbus was that once the device failed to respond immediately, it was never going to respond. That is that no matter how high I set the timeout, it didn’t impact the fails. I noticed there were only 2 conditions, either the device responded and total round trip time was 60 to 80ms, or the timeout limit was reached.

I successfully fixed this by setting the timeout low and puting the read command in a looped try catch to allow up to 5 retries without raising an error

        for i in range(5):
            try:
                data = self.read_registers(0,10,4)
                break
            except IOError as e:
                pass
        if data:
            . . .

what I found is if it doesn’t succeed the first time it always succeeds on the second try, this takes a maximum of 160ms round trip where as even a 1.5s timeout often failed.

I suspect there is a glitch in the FW which causes a stall if a read is tried when something else happens to be going on, a clash of some sort, no matter how long you wait it will never recover, it just times out. Since it only occurs with multiple devices connected, I suspect these might just be a bit too sensitive to noise or certain cable/connection characteristics perhaps.

Totally, I was actually looking to use these more for some per consumer monitoring, eg a ring main of MVHR units, having one of these installed at each MVHR unit rather than having to individually wire each MVHR unit back to the dist board to monitor individually.

For “monitor device per breaker” monitoring I think I would prefer to see them din rail mounted in the dist board, but I’m particularly thinking about new builds here, choice is always limited when retro fitting.

An average DIY’er could (quite) easily add one of these behind a power outlet (in the floor, ceiling or hollow stud/partition wall etc) to measure (for example) the consumption of a washing machine or dishwasher etc. It costs not much more than a CT and needs no power supply, It’s not RF or WiFi it just needs a simple signal cable.

BUT - It needs to be said since these are mains powered, they are not as “DIY” as the usual low voltage OEM kit. Caution and confidence in what you are doing is very important when using these devices, especially the pzem014 that sits in-line with the circuit being monitored.

That raises the question: is it possible to test the installation with the energy meter permanently wired in place - both so that the test is valid and maybe to prevent damage to the meter during the standard insulation tests? If not, then it needs to be “accessible” so that it can be removed to allow the rest of the installation to be tested.

I’ve read that for short lengths of wiring, the terminators aren’t required.
But it seems to make no difference, as I tried it with and without terminators.

Good to know it wasn’t just me, though :wink:

I’ll give your loop construct a shot. It’d be nice to have them all on one bus.
Any possibility of getting a look at your script?

Sure, but I’ve only spent a couple of scattered hours on it over the last couple of days and that has been a bit of a learning curve so let me check it over a bit before I share it.

One thing I’ve noticed about the -016 is the output is smoother at low power levels.

More weirdness…
The 16 did this today.
The small gaps are dropouts caused by reboots.
After I rebooted the RPi the third time, no further changes were made.

Despite the gap, the end-of-day numbers between the 16 and the WattsOn are relatively close.

That is interesting. The shape looks to be right and even the divot at about 15:00 looks to be about the right scale (given the different vertical scales of the graphs), just with a big offset during the hiccup. Using the max value from the WattsOn of 7444W means the -016 should have been reporting 74440 or 0x000122c8. That comes across the serial bus as two 16 bit numbers, but if the high order 16-bit word read as a 0 instead of a 1, it would be the equivalent of subtracting 6.5536kW from the answer. Subtract that from 7444W gives you 890.4W which looks to be pretty close to the -016’s max during the hiccup.

The glitch looks to start and end right as it crosses the 6.5536kW boundary so that register might be permanently stuck at 0, that wouldn’t impact the morning or late afternoon readings. It’s only when the power reading carried over into the high 16-bits in the middle of the day that you’d notice. Is this using their software to read and display, or some script you’ve crafted?

Since the energy totals come across in separate registers, I guess their reading wasn’t impacted by the glitch.

I clipped and zoomed (hence the blurriness) the two sections of the graph. Looks like your analysis is spot on.
graph-comparison

The code is a python script I wrote, and I’m no coder…
It’ll be interesting to see what it does later today.
The device is in my garage, and the mid-day temps have been 42-43C for the last several days. :sweat:

I think @dBC might have sussed it. That middle section does just look as if it’s “sunk” as in those values are reduced by a fixed amount (65335).

I had noticed your original test script only reads one 16 bit register per value using the read_register() function but assumed that was just some experimenting not a final script.

I opted for using the read_registers() function that reads all the registers in one shot and then parse the values, later I added the ability to read individual registers (working towards a lib) that I used read_long() for the current, power and energy as the read_register() is fixed at 16bit. This does mean that you either have to do some bit shifting with the 1st approach ((msb*65536)+lsb) or scaling with the latter approach as the read_long() doesn’t take decimal places as an arg like the read_register() function.

Far from finished, maybe a pre-alpha version, but this is what I have so far, this is pzem.py

#!/usr/bin/env python

"""

Driver for the PZEM-014 and PZEM-016 Energy monitors, for communication using the Modbus RTU protocol.

"""

__author__  = "Paul Burnell"
__license__ = "TBC"

try:
    import minimalmodbus
except ImportError:
    print("Cannot import minimalmodbus, is it installed?\nExiting...")
    quit()

class pzem(minimalmodbus.Instrument):

    def __init__(self, serialPort, slaveAddress=1):
        """
        Create monitor device instance
        """
        minimalmodbus.Instrument.__init__(self, serialPort, slaveAddress)
        self.serial.timeout = 0.1
        self.serial.baudrate = 9600

    def getData(self):
        """
        Return array of [V, A, W, Wh, Hz, PF, alarm] values.
        """
        for i in range(5):
            try:
                data = self.read_registers(0,10,4)
                break
            except IOError as e:
                pass
            # except Exception as e:
            #     print(e)
        if 'data' in locals():
            return [
                round((data[0]*0.1),1),                    # Voltage(0.1V)
                round((data[1]*65536+data[2]*0.001),3),    # Current(0.001A)
                round((data[3]*65536+data[4]*0.1),1),      # Power(0.1W)
                round((data[5]*65536+data[6]*1),0),        # Energy(1Wh)
                round((data[7]*0.1),1),                    # Frequency(0.1Hz)
                round((data[8]*0.01),2),                   # Power Factor(0.01)
                int(data[9]>0)]                            # Alarm(1)
        else:
            return False

    def getVoltage(self):
        """
        Return the line voltage (0.1V precision).
        """
        return self.read_register(0,1,4)

    def getCurrent(self):
        """
        Return the load current (0.001A precision).
        """
        return round((self.read_long(1,4)*0.001),3)

    def getPower(self):
        """
        Return the real power (0.1W precision).
        """
        return round((self.read_long(3,4)*0.1),1)

    def getEnergy(self):
        """
        Return the real energy (1Wh precision).
        """
        return self.read_long(5,4)

    def getFrequency(self):
        """
        Return the line frequency (0.1Hz precision).
        """
        return self.read_register(7,1,4)

    def getPowerFactor(self):
        """Return the power factor (0.01 precision).
        """
        return self.read_register(8,2,4)

    def getAlarmStatus(self):
        """Return the alarm status (0 or 1 for True or False).
        """
        return 1 if self.read_register(9,0,4) == 65535 else 0

    def getAlarmThreshold(self):
        """
        Return the power alarm threshold (1W precision).
        """
        return self.read_register(1,0,3)

    def setAlarmThreshold(self, watts):
        """
        Set the power alarm threshold (1W precision).
        """
        # alarmMax = 23000        # 2300 max for pzem014 or 23000 max for pzem016
        # if int(watts) > 0 and int(watts) <= alarmMax:
        try:
            self.write_register(1,watts,0,6)
            return True
        except Exception as e:
            pass
        return False

    def setZeroEnergy(self):
        """
        Reset the energy accumulator total to zero.
        """
        try: 
            self._performCommand(66,'')
        except Exception as e:
            return False
        return True

    def setSlaveAddress(self, address):
        """
        Set a new slave address (1 to 247), initially set to 1 from factory.
        Each device must have a unique address, Max of 31 devices per network.
        """
        return self.write_register(2,address,0,6)

    #def setCalibration(self):
    #    return self._performCommand(61,'')

#class pzem014(pzem):
#class pzem016(pzem):

if __name__ == "__main__":
    
    import time

    serialPort = "COM7"

    # # # Using a single devive with factory default address of "1"
    # # pz = pzem(serialPort)

    # Using a slave address other than the factory default of "1".
    slaveAddress = 2
    pz = pzem(serialPort, slaveAddress)

    while True:
        print(' '.join(str(v) for v in [time.time(),pz.address]+pz.getData()))
        time.sleep(10)

    # #pz.setZeroEnergy()

    # #pz.setSlaveAddress(3)

    # print(pz.setAlarmThreshold(22099))

    # print(pz.getAlarmThreshold())

Ignore all the commented stuff at the bottom, that’s just where I have been trying different stuff, this is imported into the script I run, (I’ll post that to, but it needs another import too)

pzem.zip (3.0 KB)
[edit - original zip contained the pzem.pyc file instead of pzem.py in error, corrected now, although the later files have evolved a bit since originally posted]

Currently contains 3 files, pzem.py is the code above, timestamp.py is used to time the intervals and looping, the “read_pzems.py” is what I’m using to read multiple pzems on a 10s loop. It outputs like so as I intend it to send to an emonhub socket interfacer.

C:\Users\paulb\Desktop\wip\pzem>read_pzems.py
1532335960.0 2 249.5 0.0 0.0 0.0 50.0 0.0 0
1532335960.0 3 249.7 0.0 0.0 0.0 50.0 0.0 0
1532335970.0 2 249.6 0.0 0.0 0.0 50.0 0.0 0
1532335970.0 3 249.7 0.0 0.0 0.0 50.1 0.0 0
Exiting...

C:\Users\paulb\Desktop\wip\pzem>
1 Like

Yep. totally agree.
Hence my comment to dBC I’m no coder.
Hardware (and Radar hardware, at that) is my bailiwick. Looks like a some script mods are in order. :grin:

Almost forgot…

Tnx for the script!

After thinking about it for a bit, I figured out why I did what I did.
The data from my WattsOn transducers is in 32-bit FP format.
So, like a dummy, I had single read instruction on the brain, so-to-speak,
as that’s the tack I took with my script to read the WattsOn data.

Looking at your original script, it’s easy enough to alter. Just changing the function names (for AMPS, WATT and WHRS) and moving the decimal place arg out of the function args and instead applying the scaling with some math is all you need, so

AMPS = pz.read_register(1, 3, 4)

becomes

AMPS = pz.read_long(1, 4) *0.001  

using the “3” decimal places arg to define the multiplier, however, that will result in some funky long float rounding errors, so since we know the resolution I just used round() to enforce that resolution eg

AMPS = round(pz.read_long(1, 4) *0.001, 3)

the 2nd arg “3” also matches the “3” in the original args.

1 Like

That’s easy. (I like easy)

Thanks PB!

ARRRGH!

When I plug in your line of code, the result is 1.76 Million Amps.
(I wish! I’d OWN the utility company at THAT rate!)
I copied / pasted the line to keep the typing mistakes to a minimum.

Me too! What’s not so easy for you to fix is the occasional non-responses. They way I grab all 10 registers (20bytes) in one hit means I can easily put the single read into a retry loop as I have done. But when reading all the individual registers you need to either loop/retry each one which means 5 or 6 retry loops that might split the results slightly ie the voltage and power factor might be half a second apart depending on number of retries etc or you still put all the individual reads in a single loop which would mean the successfully read values gets overwritten each time a full compliment of reads isn’t completed. IMO if you are always reporting all the values as a set (regardless of whether it’s as a CVS array or named pairs) it is more efficient to get them all in one hit. I think that would become even more apparent if there were closer to 32 devices connected.

Oh and just for fun I tried lowering the reporting interval, I can successfully read 2 devices at 0.8second intervals, It won’t (currently) go lower even for just one device, not that we would needs sub 1 second readings, but I was just wondering how more devices might impact the loop time.

C:\Users\paulb\Desktop\wip\pzem>read_pzems.py
1532366139.9 2 246.2 0.0 0.0 0.0 50.0 0.0 0
1532366139.9 3 246.3 0.0 0.0 0.0 50.0 0.0 0
1532366140.6 2 246.2 0.0 0.0 0.0 49.9 0.0 0
1532366140.6 3 246.3 0.0 0.0 0.0 50.0 0.0 0
1532366141.4 2 246.2 0.0 0.0 0.0 50.0 0.0 0
1532366141.4 3 246.3 0.0 0.0 0.0 50.0 0.0 0
1532366142.2 2 245.9 0.0 0.0 0.0 49.9 0.0 0
1532366142.2 3 246.0 0.0 0.0 0.0 49.9 0.0 0
1532366143.0 2 245.9 0.0 0.0 0.0 50.0 0.0 0
1532366143.0 3 246.0 0.0 0.0 0.0 49.9 0.0 0
1532366143.7 2 246.3 0.0 0.0 0.0 50.0 0.0 0
1532366143.7 3 246.4 0.0 0.0 0.0 50.0 0.0 0
1532366144.5 2 246.2 0.0 0.0 0.0 49.9 0.0 0
1532366144.5 3 246.3 0.0 0.0 0.0 49.9 0.0 0
1532366145.3 2 246.2 0.0 0.0 0.0 49.9 0.0 0
1532366145.3 3 246.3 0.0 0.0 0.0 50.0 0.0 0
1532366146.1 2 246.1 0.0 0.0 0.0 50.0 0.0 0
1532366146.1 3 246.2 0.0 0.0 0.0 50.0 0.0 0
1532366146.8 2 246.1 0.0 0.0 0.0 50.0 0.0 0
1532366146.8 3 246.3 0.0 0.0 0.0 49.9 0.0 0
1532366147.6 2 246.2 0.0 0.0 0.0 49.9 0.0 0
1532366147.6 3 246.3 0.0 0.0 0.0 49.9 0.0 0
1532366148.4 2 246.1 0.0 0.0 0.0 50.0 0.0 0
1532366148.4 3 246.2 0.0 0.0 0.0 50.0 0.0 0
1532366149.2 2 246.1 0.0 0.0 0.0 49.9 0.0 0
1532366149.2 3 246.2 0.0 0.0 0.0 50.0 0.0 0
1532366149.9 2 246.2 0.0 0.0 0.0 50.0 0.0 0
1532366149.9 3 246.3 0.0 0.0 0.0 50.0 0.0 0
1532366150.7 2 246.2 0.0 0.0 0.0 49.9 0.0 0
1532366150.7 3 246.3 0.0 0.0 0.0 50.0 0.0 0
1532366151.5 2 246.2 0.0 0.0 0.0 49.9 0.0 0
1532366151.5 3 246.3 0.0 0.0 0.0 50.0 0.0 0
1532366152.2 2 246.2 0.0 0.0 0.0 50.0 0.0 0
1532366152.2 3 246.3 0.0 0.0 0.0 50.0 0.0 0
1532366153.0 2 246.2 0.0 0.0 0.0 50.0 0.0 0
1532366153.0 3 246.3 0.0 0.0 0.0 49.9 0.0 0
1532366153.8 2 246.0 0.0 0.0 0.0 49.9 0.0 0
1532366153.8 3 246.1 0.0 0.0 0.0 49.9 0.0 0
1532366154.6 2 246.0 0.0 0.0 0.0 50.0 0.0 0
1532366154.6 3 246.2 0.0 0.0 0.0 50.0 0.0 0
1532366155.4 2 246.1 0.0 0.0 0.0 49.9 0.0 0
1532366155.4 3 246.2 0.0 0.0 0.0 50.0 0.0 0
1532366156.1 2 246.1 0.0 0.0 0.0 50.0 0.0 0
1532366156.1 3 246.2 0.0 0.0 0.0 49.9 0.0 0
Exiting...

I actually started writing this before you posted above and had to pop out for a bit, now I’ve returned and seen your comment, firstly I would question whether you want to do the values individually, for the reasons given above, if you do then some things to try, perhaps another set of parenthesis like so

AMPS = round((pz.read_long(1, 4) * 0.001), 3)

I thought I had tested this before resetting the Wh counter as one of my devices arrived non-zeroed, I have since tested the setZeroEnergy() function and no longer have any values I can test this with. But the docs seem straightforward enough, there is no “endiness” switch, it is unsigned by default and says it reads 2 consecutive 16bit registers starting from the address given and “1” is the address for current so I’m a bit unsure why it should read so high.

https://minimalmodbus.readthedocs.io/en/master/apiminimalmodbus.html#minimalmodbus.Instrument.read_long

You could try adding pz..debug = True after the code to create the connection to see what is being returned in it’s raw form.

If you do not use the read_long() function it would then require at least 2 read_register()s and some external maths eg (((65536 x reg1) + reg2) * 0.001), which just isn’t right, or read the whole lot with read_registers() and then just do the math on the array variables eg round((data[1]*65536+data[2]*0.001),3), this is how I do it, but I have to admit I have not done extensive testing with high values as I have not yet connected them to loads.

I used your code to read the registers / generate the array and I still get an occasional error on script startup.

Here’s the error I get:

Traceback (most recent call last):
  File "./1.py", line 51, in <module>
    print(' '.join(str(v) for v in pz.getData()+[time.time()]))
TypeError: unsupported operand type(s) for +: 'bool' and 'list'

I reversed the order of the time and data, and eliminated the address.
The error only happens occasionally. Otherwise it seems to work fine.

What sort of error? I’m sure there’s loads of room for improvement but I can’t preempt what might cause an issue as I’m new to Modbus and rs485 and only have these pzem devices to play with, so I have to rely on errors occurring to know what to change.

[ edit Ah, ok, you’ve posted whilst I was asking the question :slight_smile: )

Here’s what the output looks like when it’s working:

247.7 2010120192.0 676790272.1 1957101568.0 1532376609
248.0 2010578944.0 681115648.1 1957822464.0 1532376614

This is the correct output. (I ran the time through the int() function)

This is what I’m playing with ATM:

#!/usr/bin/python
import minimalmodbus
class pzem(minimalmodbus.Instrument):

    def __init__(self, serialPort, slaveAddress=1):

        minimalmodbus.Instrument.__init__(self, serialPort, slaveAddress)
        self.serial.timeout = 0.1
        self.serial.baudrate = 9600

    def getData(self):

	for i in range(5):
        try:
            data = self.read_registers(0,7,4)
            break
        except IOError as e:
            pass

    if 'data' in locals():
        return [
            round((data[0]*0.1),1),                    # Voltage(0.1V)
            round((data[1]*65536+data[2]*0.001),3),    # Current(0.001A)
            round((data[3]*65536+data[4]*0.1),1),      # Power(0.1W)
            round((data[5]*65536+data[6]*1),0)         # Energy(1Wh)
        ]
    else:
        return False

if __name__ == "__main__":

import time
serialPort = "/dev/ttyUSB1"
slaveAddress = 4
pz = pzem(serialPort, slaveAddress)

while True:
    print(' '.join(str(v) for v in pz.getData()+[int(time.time())]))
    time.sleep(5)