Skip to content

Instantly share code, notes, and snippets.

@neomanic
Forked from Tugzrida/Prometheus Water Meter.md
Created December 2, 2019 23:45
Show Gist options
  • Save neomanic/353727073989c5bacd9a11223ba405c3 to your computer and use it in GitHub Desktop.
Save neomanic/353727073989c5bacd9a11223ba405c3 to your computer and use it in GitHub Desktop.
Prometheus Smart Water Meter project in MicroPython for the ESP32
print('booting')
######### Water Options #########
volPerPulse = 0.5
pulseDebounce = 250
flowRateTimeout = 20000
flowRatePerMs = 60000
#################################
lastPulse = -flowRateTimeout - 100
pulses = secondlastPulse = lastSync = 0
from network import WLAN, STA_IF
import time, socket
from struct import unpack
from machine import RTC, Pin
led = Pin(2, Pin.OUT)
led.off()
# WiFi
wlan = WLAN(STA_IF)
wlan.active(True)
wlan.connect("SSID", "PASSWORD")
while not wlan.isconnected():
time.sleep(1)
print("WiFi connected")
# RTC
standardTimeOffset = 60 * 60 * 10 # AEST(UTC+10)
summerTimeDifference = 60 * 60 * 1 # AEDT(UTC+11), 1 hour ahead
rtc = RTC()
def isDST(now):
month = now[1]
if (month >= 11 or month <= 3): return True # Nov, Dec, Jan, Feb, Mar are always summer time
if (month >= 5 and month <= 9): return False # May, Jun, Jul, Aug, Sep are always standard time
date = now[2]
hour = now[3]
dow = now[6]
lastSun = date - (dow + 1) # Representing the previous Sunday, even if today is Sunday
if (month == 10):
# DST if lastSun was this month or today is changing sunday and after 2am
return ((lastSun > 0) or (dow == 6 and hour >= 2))
if (month == 4):
# DST if lastSun was last month and today isn't changing sunday after 2am
return ((lastSun <= 0) and (dow != 6 or hour < 2))
def ntpSync():
global lastSync
NTP_QUERY = bytearray(48)
NTP_QUERY[0] = 0x1b
addr = socket.getaddrinfo("au.pool.ntp.org", 123)[0][-1]
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.settimeout(1)
res = s.sendto(NTP_QUERY, addr)
msg = s.recv(48)
tm = time.localtime(unpack("!I", msg[40:44])[0] - 3155673600 + standardTimeOffset)
rtc.datetime((tm[0], tm[1], tm[2], tm[6] + 1, tm[3], tm[4], tm[5], 0))
lastSync = time.ticks_ms()
print("Time synced with NTP")
except OSError as e:
if e.args[0] == 110:
print('NTP timeout!')
else:
raise e
finally:
s.close()
def getTime():
if time.ticks_diff(time.ticks_ms(), lastSync) > 1800000:
ntpSync()
now = time.localtime()
dst = isDST(now)
if dst:
now = time.localtime(time.mktime(now) + summerTimeDifference)
return now, dst
ntpSync()
lastResetDate = getTime()[0][2]
# Web server setup
def runHTTP():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(("", 80))
s.listen(3)
print("HTTP server running")
led.on()
while True:
conn, addr = s.accept()
request = conn.recv(1024)
currentTime, dst = getTime()
global pulses, lastResetDate
if (currentTime[2] != lastResetDate):
pulses = 0
lastResetDate = currentTime[2]
conn.sendall(
"""HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8; version=0.0.4
Access-Control-Allow-Origin: *
X-DateTime: {}
X-DST: {}
X-WiFi: {}
Connection: close
# HELP water_today_litres Total litres of water used since midnight.
# TYPE water_today_litres counter
water_today_litres {}
# HELP water_flowrate_lpm Current flowrate in litres per minute.
# TYPE water_flowrate_lpm gauge
water_flowrate_lpm {}
""".format(
str(currentTime),
str(dst),
wlan.config("essid") + " RSSI" + str(wlan.status("rssi")) + "dBm",
str(round(pulses * volPerPulse, 1)),
(str(round(flowRatePerMs * volPerPulse / time.ticks_diff(lastPulse, secondLastPulse), 2)) if time.ticks_diff(time.ticks_ms(), lastPulse) < flowRateTimeout else "0.00")
))
conn.close()
# Pulse counting
def waterPulse(e):
global lastPulse, secondLastPulse, pulses
if time.ticks_diff(time.ticks_ms(), lastPulse) > pulseDebounce:
pulses += 1
secondLastPulse = lastPulse
lastPulse = time.ticks_ms()
waterPin = Pin(13, mode=Pin.IN, pull=Pin.PULL_UP)
waterPin.irq(trigger=Pin.IRQ_FALLING, handler=waterPulse)
print("Pulse interrupt set")
runHTTP()

Prometheus Water Meter

This is a fairly involved process as it seems that no one currently makes a pre-made product that measures water usage from a household meter.

It took quite a while to set up due to WiFi issues and the flow rate code still needs some work as the output is semi-random at the moment.

If you get stuck anywhere I'm happy to answer questions provided this doesn't go viral 😂

Components:

Hardware:

Basically all that needs to happen is that the output of the pulse probe is connected between A GND and D7 on the ESP32.

I did this by cutting off the probe wire from the plug as supplied by Aquatrip, then soldering on an extension wire and sealing the joint in heatshrink.

The wire can then be run from the water meter to inside and into the ESP32.

Software:

Firstly the ESP32 needs to be flashed with the MicroPython firmware https://docs.micropython.org/en/latest/esp32/tutorial/intro.html

The below boot.py and main.py can then be uploaded to the board with a program such as uPyCraft after entering your WiFi details and changing the timezone details if necessary.

Upon powering up, the board will briefly turn on the green led, then turn it back off until a WiFi connection is made. This specific board seems to have fairly poor reception so close proximity to an access point is necessary.

Once working, the board will be waiting for http connections on port 80 and will serve the litres used today and current flowrate in Prometheus text exposition format, along with a couple of debug headers.

Depending on how your water meter sends pulses, you may also need to change the volPerPulse variable to ensure an accurate reading. You can do this by checking the change in the reported water usage before and after filling up a bucket with a known capacity.

Once it's working, you can add a job to prometheus with something like this:

scrape_configs:
  - job_name: 'water'
    scrape_interval: 30s
    static_configs:
      - targets: ['ESP32_IP_ADDRESS']

Then onto Grafana dashboard design.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment