We connect the water meter to the smart home

Once upon a time, home automation systems, or “smart home” as they are often called, were terribly expensive and only the rich could afford them. Today on the market you can find fairly budget kits with sensors, buttons / switches and actuators for controlling lighting, sockets, ventilation, water supply and other consumers. And even the most crooked DIY-shnik can join the beauty and assemble devices for a smart home for an inexpensive price.

We connect the water meter to the smart home

As a rule, the proposed devices are either sensors or actuators. They make it easy to implement scenarios like “when the motion sensor is triggered, turn on the light” or “the switch near the exit turns off the light in the entire apartment.” But somehow it didn't work out with telemetry. At best, this is a graph of temperature and humidity, or instantaneous power in a particular outlet.

I recently installed water meters with a pulse output. Through each liter that has run through the counter, the reed switch is activated and closes the contact. The only thing left to do is to cling to the wires and try to get some benefit out of it. For example, analyze water consumption by hours and days of the week. Well, if there are several risers for water in the apartment, then it is more convenient to see all the current indicators on one screen than to climb hard-to-reach niches with a flashlight.

Under the cut, my version of a device based on ESP8266, which counts pulses from water meters and sends readings to the smart home server via MQTT. We will program in micropython using the uasyncio library. When creating the firmware, I came across several interesting difficulties, which I will also discuss in this article. Go!

scheme

We connect the water meter to the smart home

The heart of the whole circuit is a module on the ESP8266 microcontroller. ESP-12 was originally planned, but mine turned out to be defective. I had to be content with the ESP-07 module, which was available. Fortunately, they are the same both in terms of conclusions and functionality, the only difference is in the antenna - the ESP-12 has it built-in, while the ESP-07 has an external one. However, even without a WiFi antenna, the signal in my bathroom is caught normally.

The binding of the module is standard:

  • reset button with a pull-up and a capacitor (although both are already inside the module)
  • The enable signal (CH_PD) is pulled up to power
  • GPIO15 pulled to ground. This is only needed at the start, but I still don’t need to cling to this leg anymore

To transfer the module to the firmware mode, you need to close GPIO2 to the ground, and to make it more convenient, I provided the Boot button. In the normal state, this pin is pulled up to power.

The state of the GPIO2 line is checked only at the beginning of operation - when power is applied or immediately after a reset. So the module either boots as usual, or goes into firmware mode. Once loaded, this pin can be used as a regular GPIO. Well, since there is already a button there, you can hang some useful function on it.

For programming and debugging, I will use the UART, which I brought to the comb. When necessary, I simply connect a USB-UART adapter there. You just need to remember that the module is powered by 3.3V. If you forget to switch the adapter to this voltage and apply 5V, then the module will most likely burn out.

I have no problems with electricity in the bathroom - the outlet is located about a meter from the meters, so I will power it from 220V. As a power source, I will have a small block HLK-PM03 by Tenstar Robot. Personally, I have a hard time with analog and power electronics, and here is a ready-made power supply in a small case.

To signal the operating modes, I provided an LED connected to GPIO2. However, I did not solder it, because. the ESP-07 module already has an LED connected to the same GPIO2. But let it be on the board - all of a sudden I want to bring this LED to the case.

Let's move on to the most interesting. Water meters have no logic, they cannot be asked for current readings. The only thing that is available to us is impulses - closing the contacts of the reed switch every liter. I have the reed switch outputs in GPIO12 / GPIO13. I will turn on the pull-up resistor programmatically inside the module.

Initially, I forgot to provide resistors R8 and R9 and they are not in my version of the board. But since I'm already laying out the scheme for everyone to see, it's worth correcting this oversight. Resistors are needed so as not to burn the port if the firmware is buggy and sets a unit on the pin, and the reed switch shorts this line to ground (with a resistor, a maximum of 3.3V / 1000Ω = 3.3mA will flow).

It's time to think about what to do if the electricity goes out. The first option is to ask the server for the initial values ​​of the counters at the start. But this would require a significant complication of the exchange protocol. Moreover, the performance of the device in this case depends on the state of the server. If after turning off the light the server did not start (or started later), then the water meter would not be able to request the initial values ​​​​and would work incorrectly.

Therefore, I decided to implement the storage of counter values ​​in a memory chip connected via I2C. I don’t have any special requirements for the size of flash memory - you need to save only 2 numbers (the number of liters according to hot and cold water meters). Even the smallest module will do. But you need to pay attention to the number of write cycles. For most modules, this is 100 thousand cycles, for some up to a million.

It would seem that a million is a lot. But for 4 years of living in my apartment, I consumed a little more than 500 cubic meters of water, this is 500 thousand liters! And 500 thousand records in flash. And that's just cold water. You can, of course, re-solder the chip every couple of years, but it turned out there are FRAM chips. From a programming point of view, this is the same I2C EEPROM, only with a very large number of rewrite cycles (hundreds of millions). That's just until I still can't get to a store with such microcircuits, so for now the usual 24LC512 will stand.

Printed circuit board

Initially, I planned to make a board at home. Therefore, the board was designed as one-sided. But after spending an hour with a laser iron and a solder mask (it’s somehow not comme il faut without it), I nevertheless decided to order boards from the Chinese.

We connect the water meter to the smart home

Almost before ordering the board, I realized that in addition to the flash memory chip, you can hook up something else useful to the I2C bus, for example, a display. What exactly to output to it is still a question, but you need to breed it on the board. Well, since I was going to order boards at the factory, there was no point in limiting myself to a one-sided board, so the I2C lines are the only ones on the back of the board.

One large jamb was also connected with the one-way wiring. Because the board was drawn one-sided, then the tracks and SMD components were planned to be placed on one side, and the output components, connectors and power supply on the other. When I received the boards a month later, I forgot about the original plan and soldered all the components on the front side. And only when it came to soldering the power supply it turned out that the plus and minus were divorced vice versa. I had to farm with jumpers. In the picture above, I have already changed the wiring, but the ground is transferred from one part of the board to another through the pins of the Boot button (although it would be possible to draw a track on the second layer).

It turned out like this

We connect the water meter to the smart home

Chassis

The next step is the body. If you have a 3D printer, this is not a problem. I didn’t bother much - I just drew a box of the right size and made cutouts in the right places. The cover is attached to the body with small self-tapping screws.

We connect the water meter to the smart home

I already mentioned that the Boot button can be used as a general purpose button - so let's bring it to the front panel. To do this, I drew a special “well” where the button lives.

We connect the water meter to the smart home

There are also stubs inside the case, on which the board is installed and fixed with a single M3 screw (there was no more space on the board)

The display was selected already when I printed the first fitting version of the case. A standard two-line printer did not fit into this case, but in the bottom of the barrel was an OLED display SSD1306 128 × 32. It’s small, but I don’t stare at him every day - it will roll.

Estimating this way and that, how the wires will be laid from it, I decided to stick the display in the middle of the case. Ergonomics, of course, below the plinth - the button is on top, the display is on the bottom. But I already said that the idea to screw the display came too late and I was too lazy to re-wire the board to move the button.

Assembled device. The display module is glued to the snot with hot glue

We connect the water meter to the smart home

We connect the water meter to the smart home

The end result can be seen on KDPV

Firmware

Let's move on to the software part. For such small crafts, I really like to use the Python language (micropython) - the code is very compact and understandable. Fortunately, there is no need to go down to the level of registers in order to squeeze out microseconds - everything can be done from python.

It seems that everything is simple, but not very - the device has several independent functions:

  • The user taps a button and looks at the display
  • Liters tick and update values ​​in flash memory
  • The module monitors the WiFi signal and reconnects if necessary
  • Well, without a blinking light bulb, you can’t at all

It is impossible to admit that one function did not work if the other, for some reason, is stupid. I have already eaten cacti in other projects and now I still see glitches like “missed another liter because the display was updating at that moment” or “the user cannot do anything while the module connects to WiFi”. Of course, some things can be done through interrupts, but you can run into a limitation on duration, nesting of calls, or non-atomic change of variables. Well, the code that does everything and immediately quickly turns into a mess.

В more serious project I used classic preemptive multitasking and FreeRTOS, but in this case, the model turned out to be much more suitable coroutines and uasync libraries . Moreover, the Python implementation of coroutines is just a bomb - everything is done simply and conveniently for the programmer. Just write your own logic, just tell me where you can switch between threads.

I propose to study the differences between preemptive and competitive multitasking as optional. Now let's finally get to the code.

#####################################
# Counter class - implements a single water counter on specified pin
#####################################
class Counter():
    debounce_ms = const(25)
    
    def __init__(self, pin_num, value_storage):
        self._value_storage = value_storage
        
        self._value = self._value_storage.read()
        self._value_changed = False

        self._pin = Pin(pin_num, Pin.IN, Pin.PULL_UP)

        loop = asyncio.get_event_loop()
        loop.create_task(self._switchcheck())  # Thread runs forever

Each counter is handled by an instance of the Counter class. First of all, the initial value of the counter is subtracted from the EEPROM (value_storage) - this is how recovery after a power failure is implemented.

The pin is initialized with a built-in pull-up to the power supply: if the reed switch is closed, the line is zero, if the line is open, it is pulled up to the power supply and the controller reads one.

Also, a separate task is launched here, which will poll the pin. Each counter will run its own task. Here is her code

    """ Poll pin and advance value when another litre passed """
    async def _switchcheck(self):
        last_checked_pin_state = self._pin.value()  # Get initial state

        # Poll for a pin change
        while True:
            state = self._pin.value()
            if state != last_checked_pin_state:
                # State has changed: act on it now.
                last_checked_pin_state = state
                if state == 0:
                    self._another_litre_passed()

            # Ignore further state changes until switch has settled
            await asyncio.sleep_ms(Counter.debounce_ms)

A delay of 25ms is needed to filter the bounce of contacts, and at the same time it regulates how often the task wakes up (while this task is sleeping, other tasks are working). Every 25ms, the function wakes up, checks the pin, and if the reed switch contacts are closed, then another liter has passed through the counter and this needs to be processed.

    def _another_litre_passed(self):
        self._value += 1
        self._value_changed = True

        self._value_storage.write(self._value)

Processing the next liter is trivial - the counter just increases. Well, it would be nice to write a new value to a USB flash drive.

For ease of use, "accessors" are provided.

    def value(self):
        self._value_changed = False
        return self._value

    def set_value(self, value):
        self._value = value
        self._value_changed = False

Well, now let's use the delights of python and the uasync library and make the counter object waitable (how can I translate it into Russian? The one that can be expected?)

    def __await__(self):
        while not self._value_changed:
            yield from asyncio.sleep(0)

        return self.value()

    __iter__ = __await__  

This is such a handy function that waits until the counter value is updated - the function wakes up from time to time and checks the _value_changed flag. The trick of this function is that the calling code can fall asleep on a call to this function and sleep until a new value is received.

But what about interrupts?Yes, at this point you can troll me, saying that he himself said about interruptions, but in fact he arranged a stupid pin poll. Actually interrupts are the first thing I tried. In the ESP8266, you can organize an interrupt on the front, and even write an interrupt handler for this interrupt in python. In this interrupt, you can update the value of a variable. Probably, this would be enough if the counter was a slave device - one that waits until it is asked for this value.

Unfortunately (or fortunately?), my device is active, it should itself send messages via the MQTT protocol and write data to EEPROM. And here restrictions already come in - you can’t allocate memory in interrupts and use a large stack, which means you can forget about sending messages over the network. There are buns like micropython.schedule () that allow you to run some kind of function “as soon as and immediately”, but the question arises “what's the point?”. Suddenly, we are sending some kind of message right now, and then an interrupt breaks in and spoils the values ​​​​of variables. Or, for example, a new counter value has arrived from the server while we have not recorded the old one yet. In general, you need to block synchronization or get out somehow differently.

And from time to time RuntimeError: schedule stack full crashes and who knows why?

With explicit polling and uasync, in this case, it somehow turns out to be more beautiful and more reliable.

I took out work with EEPROM in a small class

class EEPROM():
    i2c_addr = const(80)

    def __init__(self, i2c):
        self.i2c = i2c
        self.i2c_buf = bytearray(4) # Avoid creation/destruction of the buffer on each call


    def read(self, eeprom_addr):
        self.i2c.readfrom_mem_into(self.i2c_addr, eeprom_addr, self.i2c_buf, addrsize=16)
        return ustruct.unpack_from("<I", self.i2c_buf)[0]    
        
    
    def write(self, eeprom_addr, value):
        ustruct.pack_into("<I", self.i2c_buf, 0, value)
        self.i2c.writeto_mem(self.i2c_addr, eeprom_addr, self.i2c_buf, addrsize=16)

It is difficult to work with bytes directly in python, and it is bytes that are written to memory. I had to fence the conversion between an integer and bytes using the ustruct library.

In order not to transmit the I2C object and the address of the memory cell each time, I wrapped it all up in a small and convenient classic

class EEPROMValue():
    def __init__(self, i2c, eeprom_addr):
        self._eeprom = EEPROM(i2c)
        self._eeprom_addr = eeprom_addr
        

    def read(self):
        return self._eeprom.read(self._eeprom_addr)


    def write(self, value):
        self._eeprom.write(self._eeprom_addr, value)

The I2C object itself is created with these parameters

i2c = I2C(freq=400000, scl=Pin(5), sda=Pin(4))

We approach the most interesting - the implementation of communication with the server via MQTT. Well, you don’t need to implement the protocol itself - I found it on the Internet ready-made asynchronous implementation. Here we will use it.

All the most interesting is collected in the CounterMQTTClient class, which is based on the library MQTTClient. Let's start with the periphery

#####################################
# Class handles both counters and sends their status to MQTT
#####################################
class CounterMQTTClient(MQTTClient):

    blue_led = Pin(2, Pin.OUT, value = 1)
    button = Pin(0, Pin.IN)

    hot_counter = Counter(12, EEPROMValue(i2c, EEPROM_ADDR_HOT_VALUE))
    cold_counter = Counter(13, EEPROMValue(i2c, EEPROM_ADDR_COLD_VALUE))

Here, light bulb and button pins are created and configured, as well as cold and hot water meter objects.

With initialization, not everything is so trivial

    def __init__(self):
        self.internet_outage = True
        self.internet_outages = 0
        self.internet_outage_start = ticks_ms()

        with open("config.txt") as config_file:
            config['ssid'] = config_file.readline().rstrip()
            config['wifi_pw'] = config_file.readline().rstrip()
            config['server'] = config_file.readline().rstrip()
            config['client_id'] = config_file.readline().rstrip()
            self._mqtt_cold_water_theme = config_file.readline().rstrip()
            self._mqtt_hot_water_theme = config_file.readline().rstrip()
            self._mqtt_debug_water_theme = config_file.readline().rstrip()

        config['subs_cb'] = self.mqtt_msg_handler
        config['wifi_coro'] = self.wifi_connection_handler
        config['connect_coro'] = self.mqtt_connection_handler
        config['clean'] = False
        config['clean_init'] = False
        super().__init__(config)

        loop = asyncio.get_event_loop()
        loop.create_task(self._heartbeat())
        loop.create_task(self._counter_coro(self.cold_counter, self._mqtt_cold_water_theme))
        loop.create_task(self._counter_coro(self.hot_counter, self._mqtt_hot_water_theme))
        loop.create_task(self._display_coro())

To set the parameters of the mqtt_as library, a large dictionary of different settings is used - config. Most of the default settings work for us, but a lot of settings need to be set explicitly. In order not to prescribe the settings directly in the code, I store them in a text file config.txt. This allows you to change the code regardless of the settings, as well as rivet several identical devices with different parameters.

The last block of code starts several coroutines to serve various system functions. Here is an example of a coroutine that serves counters

    async def _counter_coro(self, counter, topic):
        # Publish initial value
        value = counter.value()
        await self.publish(topic, str(value))

        # Publish each new value
        while True:
            value = await counter
            await self.publish_msg(topic, str(value))

The coroutine waits in a loop for a new counter value, and as soon as it appears, it sends a message via the MQTT protocol. The first piece of code sends the initial value even if there is no water flowing through the counter.

The base class MQTTClient serves itself, initiates a WiFi connection and reconnects when the connection is lost. When the state of the WiFi connection changes, the library informs us by calling wifi_connection_handler

    async def wifi_connection_handler(self, state):
        self.internet_outage = not state
        if state:
            self.dprint('WiFi is up.')
            duration = ticks_diff(ticks_ms(), self.internet_outage_start) // 1000
            await self.publish_debug_msg('ReconnectedAfter', duration)
        else:
            self.internet_outages += 1
            self.internet_outage_start = ticks_ms()
            self.dprint('WiFi is down.')
            
        await asyncio.sleep(0)

The function is honestly licked from the examples. In this case, it counts the number of outages (internet_outages) and their duration. When the connection is restored, an idle time is sent to the server.

By the way, the last sleep is needed only for the function to become asynchronous - in the library it is called through await, and only functions in the body of which there is another await can be called.

In addition to connecting to WiFi, you also need to establish a connection with the MQTT broker (server). This is also done by the library, and we get the opportunity to do something useful when the connection is established

    async def mqtt_connection_handler(self, client):
        await client.subscribe(self._mqtt_cold_water_theme)
        await client.subscribe(self._mqtt_hot_water_theme)

Here we subscribe to several messages - the server now has the ability to set the current values ​​​​of the counters by sending the appropriate message.

    def mqtt_msg_handler(self, topic, msg):
        topicstr = str(topic, 'utf8')
        self.dprint("Received MQTT message topic={}, msg={}".format(topicstr, msg))

        if topicstr == self._mqtt_cold_water_theme:
            self.cold_counter.set_value(int(msg))

        if topicstr == self._mqtt_hot_water_theme:
            self.hot_counter.set_value(int(msg))

This function processes incoming messages, and depending on the topic (message name), the values ​​​​of one of the counters are updated

A couple of helper functions

    # Publish a message if WiFi and broker is up, else discard
    async def publish_msg(self, topic, msg):
        self.dprint("Publishing message on topic {}: {}".format(topic, msg))
        if not self.internet_outage:
            await self.publish(topic, msg)
        else:
            self.dprint("Message was not published - no internet connection")

This function is responsible for sending a message if the connection is established. If there is no connection, the message is ignored.

And this is just a convenient function that generates and sends debug messages.

    async def publish_debug_msg(self, subtopic, msg):
        await self.publish_msg("{}/{}".format(self._mqtt_debug_water_theme, subtopic), str(msg))

So much text and we haven't blinked the LED yet. Here

    # Blink flash LED if WiFi down
    async def _heartbeat(self):
        while True:
            if self.internet_outage:
                self.blue_led(not self.blue_led()) # Fast blinking if no connection
                await asyncio.sleep_ms(200) 
            else:
                self.blue_led(0) # Rare blinking when connected
                await asyncio.sleep_ms(50)
                self.blue_led(1)
                await asyncio.sleep_ms(5000)

I provided 2 modes of blinking. If the connection is lost (or it is just being established), then the device will blink quickly. If the connection is established, the device blinks every 5 seconds. If necessary, other modes of blinking can be implemented here.

But the LED is so, pampering. We also swung at the display.

    async def _display_coro(self):
        display = SSD1306_I2C(128,32, i2c)
    
        while True:
            display.poweron()
            display.fill(0)
            display.text("COLD: {:.3f}".format(self.cold_counter.value() / 1000), 16, 4)
            display.text("HOT:  {:.3f}".format(self.hot_counter.value() / 1000), 16, 20)
            display.show()
            await asyncio.sleep(3)
            display.poweroff()

            while self.button():
                await asyncio.sleep_ms(20)

This is what I was talking about - how simple and convenient it is with coroutines. This little function describes ALL user interaction. The coroutine just waits for the button to be pressed and turns on the display for 3 seconds. The display shows the current meter readings.

There are still a couple of little things left. Here is the function that (re)starts this whole economy. The main loop is only concerned with sending various debugging information once a minute. In general, I give it as it is - I don’t need to comment specifically, I think

   async def main(self):
        while True:
            try:
                await self._connect_to_WiFi()
                await self._run_main_loop()
                    
            except Exception as e:
                self.dprint('Global communication failure: ', e)
                await asyncio.sleep(20)

    async def _connect_to_WiFi(self):
        self.dprint('Connecting to WiFi and MQTT')
        sta_if = network.WLAN(network.STA_IF)
        sta_if.connect(config['ssid'], config['wifi_pw'])
        
        conn = False
        while not conn:
            await self.connect()
            conn = True

        self.dprint('Connected!')
        self.internet_outage = False

    async def _run_main_loop(self):
        # Loop forever
        mins = 0
        while True:
            gc.collect()  # For RAM stats.
            mem_free = gc.mem_free()
            mem_alloc = gc.mem_alloc()

            try:
                await self.publish_debug_msg("Uptime", mins)
                await self.publish_debug_msg("Repubs", self.REPUB_COUNT)
                await self.publish_debug_msg("Outages", self.internet_outages)
                await self.publish_debug_msg("MemFree", mem_free)
                await self.publish_debug_msg("MemAlloc", mem_alloc)
            except Exception as e:
                self.dprint("Exception occurred: ", e)
            mins += 1

            await asyncio.sleep(60)

Well, a couple more settings and constants for completeness of description

#####################################
# Constants and configuration
#####################################


config['keepalive'] = 60
config['clean'] = False
config['will'] = ('/ESP/Wemos/Water/LastWill', 'Goodbye cruel world!', False, 0)

MQTTClient.DEBUG = True

EEPROM_ADDR_HOT_VALUE = const(0)
EEPROM_ADDR_COLD_VALUE = const(4)

It all starts like this

client = CounterMQTTClient()
loop = asyncio.get_event_loop()
loop.run_until_complete(client.main())

Something with my memory has become

So, all the code is there. I uploaded the files using the ampy utility - it allows you to upload them to the internal (the one in the ESP-07 itself) flash drive and then access it from the program as normal files. There I also uploaded the mqtt_as, uasyncio, ssd1306 and collections libraries I used (used inside mqtt_as).

We start and... We receive MemoryError. Moreover, the more I tried to understand exactly where the memory was leaking, the more I debugged the prints, the earlier this error occurred. A short google led me to understand that in the microcontroller, in principle, there are only 30 kb of memory in which 65 kb of code (together with libraries) do not fit in any way.

But there is a way out. It turns out that micropython does not execute code directly from a .py file - this file is first compiled. Moreover, it is compiled directly on the microcontroller, turns into bytecode, which is then stored in memory. Well, the compiler also needs a certain amount of RAM to work.

The trick is to save the microcontroller from resource-intensive compilation. You can compile files on a large computer, and upload ready-made bytecode to the microcontroller. To do this, you need to download the micropython firmware and build mpy-cross utility.

I did not write a Makefile, but manually went through and compiled all the necessary files (including libraries) like this

mpy-cross water_counter.py

It remains only to fill in the files with the .mpy extension, remembering to first remove the corresponding .py files from the device's file system.

I did all the development in the program (IDE?) ESPlorer. It allows you to upload scripts to the microcontroller and immediately execute them. In my case, all the logic and the creation of all objects are located in the water_counter.py (.mpy) file. But for all this to start automatically at the start, there must also be a file called main.py. Moreover, it must be exactly .py, and not pre-compiled .mpy. Here is its trivial content

import water_counter

We start - everything works. But free memory is threateningly small - about 1kb. I still have plans to expand the functionality of the device, and this kilobyte will obviously not be enough for me. But it turned out that there is a way out.

The point is this. Even though the files are compiled into bytecode and reside on the internal file system, they are actually loaded into RAM and executed from there anyway. But it turns out that micropython can execute bytecode directly from flash memory, but for this you need to build it directly into the firmware. It's not difficult, although it took a decent amount of time on my netbook (only there I had Linux).

The algorithm is as follows:

  • Download and install ESP Open SDK. This thing builds a compiler and libraries for programs under the ESP8266. It is assembled according to the instructions on the main page of the project (I chose the STANDALONE=yes setting)
  • Download micropython sorts
  • Throw the necessary libraries into ports/esp8266/modules inside the micropython tree
  • We collect the firmware according to the instructions in the file ports/esp8266/README.md
  • Upload the firmware to the microcontroller (I do it on Windows using the ESP8266Flasher programs or Python's esptool)

Everything, now 'import ssd1306' will raise the code directly from the firmware and RAM will not be spent for this. With this trick, I uploaded only the library code to the firmware, while the main program code is executed from the file system. This makes it easy to modify the program without recompiling the firmware. At the moment, I have about 8.5kb of RAM free. This will allow us to implement quite a lot of different useful functionality in the future. Well, if there is not enough memory at all, then you can push the main program into the firmware.

And what to do with it now?

Ok, the piece of iron is soldered, the firmware is written, the box is printed, the device is stuck on the wall and the light blinks happily. But so far this is all a black box (literally and figuratively) and there is still little sense from it. It's time to do something with the MQTT messages that are sent to the server.

My "smart home" is spinning on Majordomo system. The MQTT module is either out of the box, or easily installed from the add-on market - I don’t remember where it came from. MQTT is not a self-sufficient thing - you need a so-called. broker - a server that accepts, sorts and forwards messages to MQTT clients. I use mosquitto, which (like majordomo) runs on the same netbook.

After the device sends a message at least once, the value will immediately appear in the list.

We connect the water meter to the smart home

These values ​​can now be associated with system objects, they can be used in automation scripts and subjected to various analysis - all this is out of the scope of this article. Who is interested in the majordomo system, I can recommend Channel Electronics In Lens - a friend is also building a smart home and intelligibly talks about setting up the system.

I'll just show you a couple of graphs. This is a simple graph of values ​​per day

We connect the water meter to the smart home
It can be seen that almost no one used the water at night. A couple of times someone went to the toilet, and it looks like the reverse osmosis filter sucks a couple of liters a night. In the morning consumption increases significantly. I usually use water from the boiler, but then I wanted to take a bath and temporarily switched to city hot water - this is also clearly visible in the lower graph.

From this chart, I learned that going to the toilet is 6-7 liters of water, taking a shower is 20-30 liters, washing dishes is about 20 liters, and taking a bath requires 160 liters. During the day, my family consumes somewhere around 500-600l.

For those who are especially curious, you can look into the records for each individual value.

We connect the water meter to the smart home

From here I learned that when the tap is open, water flows at a speed of about 1 liter in 5 seconds.

But in this form, the statistics are probably not very convenient to look at. majordomo also has the ability to view consumption charts by day, week and month. Here, for example, is a graph of consumption in columns

We connect the water meter to the smart home

So far I have only one week of data. In a month, this graph will be more revealing - a separate column will correspond to each day. The picture is slightly spoiled by the adjustments of the values ​​​​that I enter manually (the largest column). And it is not yet clear whether I incorrectly set the very first values ​​\uXNUMXb\uXNUMXbalmost a cube less, or whether this is a bug in the firmware and not all liters were taken into account. Need more time.

Above the graphs themselves, you still need to conjure, whiten, paint. Perhaps I will also build a graph of memory consumption for debugging purposes - all of a sudden something is leaking there. Perhaps I will somehow display the periods when there was no Internet. While all this is spinning at the level of the idea.

Conclusion

Today my apartment has become a little smarter. With such a small device, it will be more convenient for me to monitor the water consumption in the house. If earlier I was indignant “again a lot of water was consumed in a month”, now I can find the source of this consumption.

It will seem strange to someone to look at the readings on the screen if it is a meter from the meter itself. But in the not too distant future, I plan to move to another apartment, where there will be several water risers, and the meters themselves, most likely, will be located on the landing. So a remote reading device would be very handy.

I also plan to expand the functionality of the device. I'm already looking at motorized valves. Now, to switch the boiler-city water, I need to turn 3 taps in a hard-to-reach niche. It would be much more convenient to do this with one button with the corresponding indication. Well, of course, it is worth implementing protection against leaks.

In the article, I told my version of the device based on ESP8266. In my opinion, I got a very interesting version of the micropython firmware using coroutines - simple and pretty. I tried to describe the many nuances and jambs that I encountered during the campaign. Perhaps I described everything in too much detail, for me personally, as a reader, it is easier to squander the excess than to think out what was left unsaid later.

As always, I'm open to constructive criticism.

Source
Schematic and board
Case model

Source: habr.com

Add a comment