Shower Water Consumption Nudger
Fleetingin micropython, with the trinket M0 and a yf-s201
Inspired by https://www.hydrao.com/fr/boutique/54-pommeau-de-douche-aloe and https://www.instructables.com/Save-Water-With-the-Shower-Water-Monitor/ .
getting familiar with the device

real life test
Following the code from blink the dotstar internal led.
from machine import Pin
import time
import time
from machine import Pin, SoftSPI
spi = SoftSPI(
baudrate=4000000,
polarity=0,
phase=0,
sck=Pin.board.DOTSTAR_CLK,
mosi=Pin.board.DOTSTAR_DATA,
miso=Pin.board.MISO, # not used, but required
)
def dotstar_write(r, g, b, brightness=31):
start_frame = b"\x00\x00\x00\x00"
end_frame = b"\xff"
brightness = 0b11100000 | (brightness & 0b00011111)
led_frame = bytes([brightness, b, g, r])
spi.write(start_frame + led_frame + end_frame)
count = 0
consumption = 0
THRESHOLD = 5
consumption_to_color = {
0 * THRESHOLD: (0, 255, 0),
1 * THRESHOLD: (0, 0, 255),
2 * THRESHOLD: (255, 0, 255),
3 * THRESHOLD: (255, 0, 0),
4 * THRESHOLD: (255, 255, 0),
5 * THRESHOLD: (255, 255, 255),
}
dotstar_write(*consumption_to_color[consumption])
def irq_handler(pin):
global count
global consumption
count += 1
if count == 450:
consumption += 1
count = 0
data = Pin(Pin.board.D1, Pin.IN, Pin.PULL_UP)
data.irq(trigger=Pin.IRQ_FALLING, handler=irq_handler)
while True:
time.sleep(1)
dotstar_write(*consumption_to_color[consumption])
TMP="$(mktemp -d)"
trap "rm -rf '${TMP}'" 0
cat<<EOF>"${TMP}/main.py"
from machine import Pin
import time
import time
from machine import Pin, SoftSPI
spi = SoftSPI(
baudrate=4000000,
polarity=0,
phase=0,
sck=Pin.board.DOTSTAR_CLK,
mosi=Pin.board.DOTSTAR_DATA,
miso=Pin.board.MISO, # not used, but required
)
def dotstar_write(r, g, b, brightness=31):
start_frame = b"\x00\x00\x00\x00"
end_frame = b"\xff"
brightness = 0b11100000 | (brightness & 0b00011111)
led_frame = bytes([brightness, b, g, r])
spi.write(start_frame + led_frame + end_frame)
count = 0
consumption = 0
THRESHOLD = 5
consumption_to_color = {
0 * THRESHOLD: (0, 255, 0),
1 * THRESHOLD: (0, 0, 255),
2 * THRESHOLD: (255, 0, 255),
3 * THRESHOLD: (255, 0, 0),
4 * THRESHOLD: (255, 255, 0),
5 * THRESHOLD: (255, 255, 255),
}
dotstar_write(*consumption_to_color[consumption])
def irq_handler(pin):
global count
global consumption
count += 1
if count == 450:
consumption += 1
count = 0
data = Pin(Pin.board.D1, Pin.IN, Pin.PULL_UP)
data.irq(trigger=Pin.IRQ_FALLING, handler=irq_handler)
while True:
time.sleep(1)
dotstar_write(*consumption_to_color[consumption])
EOF
mpremote fs cp "${TMP}/main.py" :main.py
cp /home/sam/tmp/tmp.47VsRVXWsn/main.py :main.py
Up to date: main.py
mpremote soft-reset

Testing with a THRESHOLD of 1.
Putting several time 1/2L in the flowmeter.

I found out that those where the times when the light changed:
- blue
- 1.2 L
- purple
- 1.1 L
- red
- 1.9 L -> that’s a lot, I may have to check this more carefully
trying with leds and the wemos d1 mini
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import time
import requests
from machine import Pin
import network
def dump_exp(e):
with open('error.log', 'a+') as f:
f.write(f'{time.time()}: ')
sys.print_exception(e, f)
def print_error():
print(open("error.log").read())
leds = {
"green": Pin(0, Pin.OUT),
"blue": Pin(4, Pin.OUT),
"yellow": Pin(5, Pin.OUT),
"red": Pin(16, Pin.OUT),
}
def turn_on_led(name):
turn_off_leds()
leds[name].value(1)
def turn_on_all():
for led in leds.values():
led.value(1)
def turn_off_leds():
for led in leds.values():
led.value(0)
def progress():
while True:
for led in ["green", "blue", "yellow", "red"]:
turn_on_led(led)
yield led
class WiFi:
def __init__(self):
self.wlan = network.WLAN(network.STA_IF)
self.wasconnected = False
def __enter__(self):
if self.wlan.isconnected():
self.wasconnected = True
print("Noop, most likely a nested call")
return
print("Connecting to wifi")
self.wlan.active(True)
self.wlan.connect()
p = progress()
while True:
if self.wlan.isconnected():
update_color()
print("Connected to wifi")
return self.wlan
next(p)
time.sleep(0.3)
def __exit__(self, exc_type, exc_value, traceback):
if isinstance(exc_value, KeyboardInterrupt):
print("Ctrl-c => Keep wifi on for debugging")
elif self.wasconnected:
print("Noop, don't disturb the nested call")
self.wasconnected = False
else:
print("Disconnecting from wifi")
# return
self.wlan.disconnect()
self.wlan.active(False)
def post(*args, **kwargs):
with WiFi():
return requests.post(*args, **kwargs)
def wifi_on():
WiFi().__enter__()
start_tick = time.ticks_ms()
count = 0
consumption = 0
liter = 450
def irq_handler(pin):
global count
global consumption
count += 1
if count >= liter:
consumption += 1
count = 0
data = Pin(14, Pin.IN, Pin.PULL_UP)
def start():
data.irq(trigger=Pin.IRQ_FALLING, handler=irq_handler)
last_count = 0
done_count = -1
last_stable = None
THRESHOLD = 9
def update_color():
if consumption < 1 * THRESHOLD:
turn_on_led("green")
elif consumption < 2 * THRESHOLD:
turn_on_led("blue")
elif consumption < 3 * THRESHOLD:
turn_on_led("yellow")
else:
turn_on_led("red")
def update():
current_tick = time.ticks_ms()
ms_since_start = current_tick - start_tick
minute, second = divmod(ms_since_start // 1000, 60)
print("{:02d}:{:02d} -> {}, {}".format(minute, second, consumption, count))
global last_count
global done_count
global last_stable
if last_count == count and count != done_count:
try:
r = post(
"http://home/redislogger/",
json={"consumption": consumption + count / liter},
timeout=2,
)
print(r.status_code, r.reason)
done_count = count
last_stable = current_tick
except Exception as e:
# that's a pity, but that should not stop us
print("Error sending data: {}".format(e))
if last_stable is not None and (current_tick - last_stable) > 1000 * 60 * 10: # 10 minutes without actions. Was it forgotten ?
try:
post(
"http://home/ntfy/n",
headers={"title": "shower", "priority": "urgent"},
data="Please, shut me off!",
)
last_stable = current_tick # make it wait a bit more
except Exception as e:
# that's a pity, but that should not stop us
print("Error sending data: {}".format(e))
last_count = count
update_color()
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import time
from app import *
import app
import network
import machine
import sys
machine.freq(80000000)
if hasattr(machine, 'sleep_type'):
machine.sleep_type(machine.SLEEP_LIGHT)
with WiFi():
turn_on_led("yellow")
print("first update to avoid needing wifi directly after this")
update()
turn_on_led("blue")
print("Keep wifi on at startup to help debug")
time.sleep(5)
print("Now, ket's start!")
turn_on_led("yellow")
start()
while True:
try:
update()
except Exception as e:
dump_exp(e)
raise
time.sleep(1)
# to debug
# app.count += 100
# irq_handler(None)
op:put, host:showerlogger, port:8266, passwd:0000.
/tmp/main.py -> main.py
Remote WebREPL version: (1, 23, 0)
Sent 0 of 693 bytes
Sent 693 of 693 bytes
op:put, host:showerlogger, port:8266, passwd:0000.
/tmp/app.py -> app.py
Remote WebREPL version: (1, 23, 0)
Sent 0 of 3996 bytes
Sent 1024 of 3996 bytes
Sent 2048 of 3996 bytes
Sent 3072 of 3996 bytes
Sent 3996 of 3996 bytes
spawn webrepl_cli.py -p 0000 showerlogger
op:repl, host:showerlogger, port:8266, passwd:0000.
Remote WebREPL version: (1, 23, 0)
Use Ctrl-] to exit this shell
>>>

I could also test that it measured 5.5L (5L + 209 cycles) when I filled a 5L watering can almost to the top edge.

soldering the component on a perfboard



printing the support for the enclosure



The support was very hard to remove, so I left some.
testing it for real



some stuffs I learned
Soldering in the perfboard is not that easy. Keeping the pieces in place while holding the tin and the iron with both hands is complicated.
Using separate LEDs needs a lot more soldering and thinking about where to put them and the resistor. I prefer using the neopixel, much faster to setup.
The board tends to slide to the back of the support. I would add a stop to make sure it remains towards the end of the cup.
Removing something from the perfboard is not as easy as it seems. I have to use the tin pump several times to have a hole clean enough to put back the new piece.
Kids won’t think about removing the cup to switch the device on. It might be a good think to also nudge them with something more fancy, like a big push button.
It would have been nicer to think about a auto power off circuit before soldering the whole thing. It will be harder to add afterwards.
Also, the d1 mini still consumes a lot when in deep sleep, using another board more energy efficient might open the door for a device always on that would wake up when water starts flowing.
adding a cap to ease turning it off and on again
Using another cup as cap.


I needed to screw the smaller cup to prevent it from getting off when the cap was removed.

This is not ideal, for the weight alone of the cap is not enough to strongly grip on the smaller cup and sometimes, the MCU stops.