Build log: Detecting if the main water pipe breaks

This summer I had a problem… the water pipe that runs underground the garden broke, and we didn’t notice for several days. How many days? I don’t know, but the bill was close to 1000€.

It’d be so simple (let’s quote-unquote simple) to have an RPi connected to a smart meter and send an alert before it gets too bad. For example, if the water is continuously running for more than an hour, send me a text or an email… or anything!

I found this post by Alan Boris that I encourage everyone to read. It’s an easy-to-build solution that detects water leaks, which is what I’m looking for, but I want to build something using an industrial protocol to get more information out of the meter now that I’m onto it.

So looking for some solutions, I found this meter that looks great, with a long-lasting battery, a display, and a serial interface to expose data through Modbus.

smartmeter

The manufacturer has been very kind and responsive, sharing with me the details of the protocol, so I’ve ordered two of them: one for me and one for my mom, with different pipe sizes just in case :slight_smile:

I could have chosen an external sensor with ultrasonic probes. Still, this one caught my attention with all its possibilities and parameters, and I’m not scared of cutting the pipe open to install the meter.

So, while I wait for the meter to arrive, I’ll start with some balenaBlocks to store and show me statistics, maybe some ad-hoc containers to read modbus and notify me, and who knows what I will decide in the future.

The journey begins!

6 Likes

Yes! This is going to be great Rafa! And the project clinic will be there to help you. :wink:

Those meters look really professional, can’t wait to see more!

The meter is going to arrive in more than a month from now, so I’ve already started building the application. Hopefully, when the meter comes, I will “only” have to do small changes like data formatting, and move to a serial port.

My idea of the application is something like this, where the blue box is the only thing I code, and the rest are modules or balenaBlocks as I’ll explain later

In order to start testing, I’m use node-red on my laptop, to create a slave, simulating the smart meter.


(Note: Funny that there are no log traces in any of those debug boxes)

The meter presents tons of parameters though the modbus protocol. I can see in the documentation that the interesting ones (for me) are the instant flow, hourly, daily and monthly flow, and a test parameter to check that the communication is correct.

But to make things a bit more complex, each of them comes in a different format (IEEE754 and float) and in different number of registers each. So I will start with the Instant Flow and once I get it I’ll see how to add the rest.

An extract of the protocol description here:

To simulate this, my first approach is to create different node flows, with data injectors and timers, so I can get different readings. These were the flows:

  • Flow 1, the server, or the smartmeter sim itself serving the data
  • Flow 2, to inject two integers, join them in an array and use modbus-flex-writer to write them in the simulator
  • Flow 3, to read two registers, convert them into an integer, add 1, and write again to the simulator

Which altogether turned up this ugly… and it didn’t work as expected because I couldn’t control the way the numbers were coded (or rather I didn’t know how to) and I needed flexibility to code, decode and learn how everything works.

So I ended up creating a humble python script to inject random values to the simulator, that would allow me to test the whole data flow up to the device’s UI.

wow! this is super cool @rmorillo24 what node-RED did you use?

@mpous , I’m using node-red v2.0.6 on Mac, with node-red-contrib-modbus and node-red-node-random pagkages

The modbus package gives me all these nodes, which are much more than I need for this project. But they offer a lot of functionality that I may use in the future…

I have just finalized my first application, waiting for the smart meter to arrive.
My initial attempt was to use all the balena features to configure the slave registers as Device Variables, so that they would be easily configured in the balena dashboard.

serverparams

So the code would be pretty simple, since I´d only have to load the variables…

    modbus_host = os.environ['MODBUS_HOST_IP']
    modbus_port = os.environ['MODBUS_HOST_PORT']
    address = os.environ['INST_FLOW_ADDRESS']
    size = os.environ['INST_FLOW_SIZE']
    pollingtime = os.environ[INST_FLOW_POLLINGTIME]

And then configure the server connection, and loop to request to the meter.

    modbusc.connect()
    while (not error):
        rr = modbusc.read_holding_registers(address,
                                            size,
                                            unit=1)
        decoder = BinaryPayloadDecoder.fromRegisters(rr.registers,
                                                     Endian.Little,
                                                     Endian.Little)
        value = decoder.decode_32bit_float()
        sleep(pollingtime)

But this became a problem at the very beginning. Very useful for testing, but not very flexible, with many parameters each of them with different configurations. These would include the format, the polling time, etc. So I saw immediately the lack of scalability of this approach.

Following @rahul-thakoor ‘s hint (Thanks!!!), I looked at the modbus2mqtt project, which is using a data model description based on modbus-herdsman-converters. The idea is simple: define a json file with the data model, and abstract all the reading loop using the model.

I noticed that there were some things missing in the description that wouldn´t suit me totally:

  • each parameter was set to 1 register (this smart meter has many using 2 registers)
  • every parameter would share the same polling time (not useful for reasons explained below)
  • and all the data formats are decoded in the same way (At least I have IEEE754 and float)

I preferred to continue my own way using this idea. Maybe in the future, I will merge the projects if I can, but at the moment let´s see where this takes me.

    "instant_flow": {
                "address": 1,
                "length": 2,
                "polling_secs": 25,
                "format": "ieee754"
    }

So now I have defined the Smart Meter’s data model in a datamodel.json file that allows me to define the parameters I want to read from my modbus slave. The idea is that anyone can use this to add any other parameter with my initial smart meter, or with any other device using modbus.

There’s another design decision I still have to validate, which is the polling time. Different parameters need different polling times. For instance, I would like to read the instant_flow every hour to check if there’s a continuous flow for some time. But the daily accumulated flow and the monthly accumulate flow parameters would be better served with once a day and once a month pollings.

That’s the reason why I’ve included in the data model the polling time. And what I’ve ended up doing is spawning a process for each parameter:

for key, value in datamodel['fromModbus']['input'].items():
    try:
        log.info("Launching process to read %s from address %s, length %s, polling time:%s, format: %s" %(key, value['address'], value['length'], value['polling_secs'], value['format']))
        _thread.start_new_thread( 
            loop_read_register, (key, 
                                 value['address'],
                                 value['length'],
                                 value['polling_secs'],
                                 value['format']) )
    except Exception as e:
        log.exception("Could not start process to read")
        log.exception(e)

where each process will have a specific polling period, or sleep, according to what they need.

def loop_read_register(name, address, size, polling_secs, format):
    error = False
    while( not error ):
        try:
            value = readInputRegisters(address, size, format)
            log.info("Read: " + name +  ":" + str(value))
            msg = mqttc.publish("sensors",
                                json.dumps( { name: value } )) 
            if not msg.is_published():
                raise(Exception("Message not published in mqtt broker"))
        except Exception as e:
            log.log (logging.CRITICAL, e)
            error = True #TO-REVIEW if we want to stop the loop only because one reading fails. Could it happen?
        time.sleep(polling_secs) # Waiting for next poll,

And that’s where I am now. I will next clean the code, make some comments, and publish the Github repo here. Since there´s still time until the meter arrives, I might spend some time improving this generic way of treating with modbus.

1 Like