Matik.Org

Watermate

Just a quick note on an existing project. Do you need to keep track of you water usage? I wanted to , especially the garden sprinkler system.

I suspected that the sprinkler system had developed some nasty leaks, and the only way to measure flow would be to jog out to our driveway and look at the spider-infested meter next to the bike path while turning on various sprinkler circuits. Not a great use of time, especially if i want periodic measurements, to catch new leaks etc.

Luckily, I could just add an inline hall flow sensor directly into the separate PVC line we have for the garden watering system. These sensors are nifty and cheap (and have worked flawlessly for 6 months now). Here are a few links:

These flow meters all seem very similar, perhaps just white-label versions, but I have not compared them. While they are inexpensive, it was not trivial to install them without leaking - I’m also not certain about their long-term reliability yet.

The flow-meter is provided with 5v and ground, and a digital pin that generates pulses as the little turbine inside spins.

My flow-meter says that the pulse frequency is 5.5 Hz * Q, where Q is the flow in liters/minute. That means one liter translates into 330 pulses, or about 3.3ml per pulse. I let the sprinkler run for a few minutes, checked our official utility water meter before and after, and compared with the number of pulses on the flow meter. As far as I can tell, the flow meter is fairly accurate (less than 10% deviation).

I also used an elderly esp8266 wemos D1 with micropython. I just connected GND and 5V to the flow meter and the data pin to D4.

I run the following code (in main.py) on the Wemos D1.

import time
from machine import Pin
import gc

pin = Pin(2, machine.Pin.IN, machine.Pin.PULL_UP)
count = 0
frequency = 0

def total():
    global frequency
    global count
    frequency = count
    count = 0
    
# this Interrupt handler just increases the count of pulses seen
def counterfn(x):
    global count
    count+=1

def runme():
    global total
    global frequency
    count = 0
    while True:
		total()
	    if frequency != 0:
			#do something with frequency, e.g. update a log or a server
		gc.collect()
		time.sleep(1)

pin.irq(counterfn, Pin.IRQ_FALLING)
runme()

This is pretty straightforward. THe code first sets up a IRQ handler for pulses arriving at D4. If one is received, it will increase the global ‘count’ variable. The code then calls the main loop, runme().

Then, in the main loop, I call total once a second, which copies the current value of count into frequency and resets count. Side note: This is NOT a great way to learn about proper parallel computing - this is just sufficient to be ‘good enough’, even if I lose a second of watering every once in a while. Use mutexes if you need more precision.

Since I don’t expect the esp8266 to be up all the time, I do report the current frequency periodically to a server (my home raspberry pi in this case). This allows me to keep all the data without fear of loss, and run some analysis over them.

This is done like so:

import time
import urequests as requests
import ujson
import network

ALIVE_PING_INTERVAL = 360 # send empty packet every that many seconds
                          # otherwise only send when water is flowing
REQUEST_URL = '192.168.1.2:7777/waterworld' # server to save log to

sta_if=network.WLAN(network.STA_IF)


def sendUpdate(total):
    post_data = ujson.dumps({ 'Pulses': total})
    try:
		requests.post(REQUEST_URL, headers = {
			'content-type': 'application/json'}, 
			data = post_data)
    except:
	print("could not connect?")
	
	... and in the loop ...
	
		    if frequency != 0:
              sendUpdate(frequency)
            if count % ALIVE_PING_INTERVAL == 0:
              sendUpdate(0)
              gc.collect()

Note that I will send an empty packet every 5 minutes even if there is no water flowing. This way, I can get an idea of how reliable wifi or the esp8366 are.

Here is the go server running on my raspberry pi:

package main

import (
  "net/http"
  "os"
  "time"
  "fmt"
  "encoding/json"
  "bufio"
)
var f os.File

func check(e error) {
    if e != nil {
        panic(e)
    }
}

type Payload struct {
    Pulses int
    Random int
    Checksum int
}

func handler(w http.ResponseWriter, r *http.Request){
  var p Payload
  fmt.Println("time: %s", time.Now().Format(time.UnixDate))

  err := json.NewDecoder(r.Body).Decode(&p)
  if err != nil {
    http.Error(w, err.Error(), http.StatusBadRequest)
    return
  }
  fmt.Println("Payload: %+v", p)

  w2 := bufio.NewWriter(f)
  
  _, err = fmt.Fprintf(w2, "time: %s", time.Now().Format(time.UnixDate))
  check(err)

  _, err = fmt.Fprintf(w2, "Payload: %+v", p)
  check(err)

  w2.Flush()
}

func main() {

  f, err := os.Create("/home/USER/water")
  check(err)
  defer f.Close()

  http.HandleFunc("/", handler)
  http.ListenAndServe("0.0.0.0:7777", nil)
}

Note that logging here is extremely naughty - on restart the server wipes the old log, which is why I don’t have a historical graph here … This should be quickly changed to do some log rotation.

Finally, a small python script to plot the data (I run that on my laptop after downloading the data file from the raspberry pi):

# read log file from water server and show a graph of watering over time as 
# well as a cumulative plot
# sample command line:
#
# scp user@waterserver:water/water.log /dev/stdout|python plot_water.py


from dateutil import parser
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import sys
import time


plt.close('all')

# stdin is one line per entry in this format:
#------------------------------------------------------------------------------
#time: Tue Jun 16 20:12:24 BST 2020	Payload: {Pulses:10 Nonce:0 Checksum:3}
#------------------------------------------------------------------------------
# parse out timestamp and pulses:
ts = []
ps = []
for line in sys.stdin:
  tuple = line.strip().split()
  date=parser.parse(" ".join(tuple[1:7]))
  pulses = int(tuple[8].split(':')[1])
  ts.append(date)
  ps.append(pulses)

data = pd.DataFrame(data={'t': ts, 'p': ps})
data['date']=pd.to_datetime(data["t"], unit='s')
data.plot(x='date',y='p')
plt.show()

# 'c' column to show cumulative water use
data['c'] = data['p'].cumsum()
data.plot(x='date',y='c')
plt.show()

Note that this script will open a new window to show the first plot. Pressing ‘q’ does close that window and shows the second plot, cumulative water usage. Another ‘q’ will then quit the program. Also note that you can zoom into plots, move the curser around to select subsets of the data and so on.

The code is on github

Written on August 10, 2020