├── requirements.txt ├── README.md └── water_flow.py /requirements.txt: -------------------------------------------------------------------------------- 1 | RPi.GPIO==0.6.3 2 | psycopg2==2.7.6.1 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WaterFlowMeter 2 | Using a YF-201 Water Flow Sensor track water usage and send to database https://www.adafruit.com/product/828. See 'Stuff' section at the end to see everything I used. 3 | 4 | ## About 5 | I have a 200 gallon water tank that I refill from a well when it gets low. Using this sensor I can monitor usage and know when it's time to refill the tank. 6 | 7 | ## Wiring 8 | I used the wiring from this video https://youtu.be/wpenAP8gN3c. You can skip to 10:44 where there are a few screenshots of the wiring. I did not have to make any changes for wiring. 9 | 10 | ## Summary of the Code 11 | When testing I found that my estimates for any unit of volume and rate was different than in production. What I mean is that the volume per second that passed through when using a funnel w/ a water bottle was slightly different than when I plugged it into my tank. Not very surprising. So note the value of a cup in the code (cup_movements) and perform your own calibrations in production to get the right reading. 12 | 13 | It turns out that a rotation is signaled by sending whatever the alternate to the current state is - for example if the current input is a 1 at rest then the very first rotation will be a 0. As the water continues to flow the signal continues to alternate from 1 to 0 for each rotation. Resting state can be a 1 or 0. 14 | 15 | If the database insert/connection was not successful it stores the data in a list until the next attempt. 16 | 17 | The code itself is heavily commented - good luck! 18 | 19 | ## Getting Started 20 | 21 | `git clone https://github.com/Liamhanninen/WaterFlowMeter.git` 22 | 23 | `cd WaterFlowMeter` 24 | 25 | `pip install -r requirements.txt` 26 | 27 | `python water_flow.py` 28 | 29 | ## Stuff 30 | 0. Raspberry Pi Zero WH (Zero W with Headers) (headers optional) https://www.adafruit.com/product/3708?gclid=CjwKCAiAzanuBRAZEiwA5yf4ul30H_1a7R2wLFtjlCfOP4aGevPwQkN8Vo97Pvj_m4r_UZRgzIpz6RoCd7AQAvD_BwE 31 | 1. YF-201 Water Flow Sensor https://www.adafruit.com/product/828 32 | 2. 4.7k resistor https://www.amazon.com/Elegoo-Values-Resistor-Assortment-Compliant/dp/B072BL2VX1/ref=sr_1_1_sspa?crid=2H58UQTN5WJB9&keywords=resistor+kit&qid=1573608352&sprefix=resistor%2Caps%2C303&sr=8-1-spons&psc=1&spLa=ZW5jcnlwdGVkUXVhbGlmaWVyPUFGQ1FQOTdNQ0lEQUImZW5jcnlwdGVkSWQ9QTA2NDkzNjcxWERXWVFaUEFRS0s2JmVuY3J5cHRlZEFkSWQ9QTA2NTc0MDVIRFVIVUk3UzFGSDEmd2lkZ2V0TmFtZT1zcF9hdGYmYWN0aW9uPWNsaWNrUmVkaXJlY3QmZG9Ob3RMb2dDbGljaz10cnVl 33 | 3. 10k resistor (see link in #2) 34 | 4. Blue Monster Pipe Sealent tape (any sealent tape works) https://www.menards.com/main/plumbing/plumbing-installation-repair/pipe-sealants-caulk-putty/blue-monster-1-2-x-1429-pipe-thread-tape/70885/p-1444440441022-c-8531.htm?tid=-817058810073155805&ipos=10 35 | 5. Male adapter to garden hose https://www.menards.com/main/plumbing/rough-plumbing/pipe-tubing-hoses-fittings-accessories/fittings/garden-hose-fittings/sioux-chief-3-4-male-garden-hose-x-1-2-fip-brass-adapter/0122223/p-1444442661694-c-9432.htm?searchTermToProduct=0122223 36 | 6. Female adapter to garden hose https://www.menards.com/main/plumbing/rough-plumbing/pipe-tubing-hoses-fittings-accessories/fittings/garden-hose-fittings/sioux-chief-3-4-female-garden-hose-x-1-2-fip-brass-adapter/0122025/p-1444442656386-c-9432.htm?searchTermToProduct=0122025 37 | 7. Obviously also some wire, hose and patience. 38 | 39 | Good luck! 40 | 41 | -------------------------------------------------------------------------------- /water_flow.py: -------------------------------------------------------------------------------- 1 | import RPi.GPIO as GPIO 2 | import time,sys, datetime 3 | import psycopg2 4 | from psycopg2.extras import execute_values 5 | 6 | ''' 7 | Configure raspberry 8 | ''' 9 | 10 | GPIO.setmode(GPIO.BCM) 11 | inpt = 26 12 | GPIO.setup(inpt,GPIO.IN) 13 | 14 | ''' 15 | Configure some global variables 16 | ''' 17 | 18 | current_input = GPIO.input(inpt) # This is used to compare to the new_input later. 19 | total_rotations = 0 # This is a counter. It gets reset after the number of seconds in rotation_downtime. 20 | cup_movements = 200 # This is how many rotations occur as a cup of liquid passes through. 21 | rotation_downtime = 5 # Sets the cut-off time for establishing a water-flow event. 22 | last_movement_time = time.time() + rotation_downtime # This is used to determine if a new water-flow event should be created. 23 | record_data = False # A flag used to trigger database insert. 24 | 25 | data = [] 26 | 27 | ''' 28 | Enter database credentials here. I recommend the free-tier Postgres databses from Heroku which allow up to 10k rows. heroku.com 29 | But you can use any Postgres db - have not tested with other dbs. 30 | ''' 31 | 32 | host="" 33 | database="" 34 | user="" 35 | password="" 36 | 37 | print('Control C to exit') 38 | 39 | def commit_data(conn, data): 40 | 41 | ''' 42 | This passes data to the data base as a single row. It then resets/empties data. 43 | ''' 44 | 45 | cur = conn.cursor() 46 | insert_statement = "INSERT INTO flow_meter ('datetime','movements','cups','gallons') VALUES %s".replace("'",'') 47 | execute_values(cur,insert_statement,data) 48 | conn.commit() 49 | print ('Data sent.') 50 | cur.close() 51 | data = [] 52 | return data 53 | 54 | def prep_and_send(data,total_rotations): 55 | 56 | ''' 57 | Calculates measurements (cups and gallons). Prepares the data into a database-friendly tuple. Appends that tuple to a list. 58 | 59 | It then tries to connect to database. If it is not successful then it does nothing but saves the data; it will try to send 60 | the list of data-tuples the next time there is a water-flow event. 61 | 62 | Once the connection is successful data is emptied in commit_data(). 63 | ''' 64 | 65 | total_cups = total_rotations/cup_movements 66 | total_gallons = total_cups/16 67 | now = datetime.datetime.now() 68 | print('{}: Movements: {}. \nCups: {}. \nGallons: {}'.format(now,total_rotations,total_cups,total_gallons)) 69 | 70 | current_data = ( 71 | now, 72 | round(total_rotations,2), 73 | round(total_cups,2), 74 | round(total_gallons,2), 75 | ) 76 | data.append(current_data) 77 | try: 78 | ''' 79 | Establish connection with Db and try to insert. 80 | ''' 81 | conn = psycopg2.connect(host = host, database = database, user = user, password = password) 82 | data = commit_data(conn, data) 83 | conn.close() 84 | except psycopg2.OperationalError as e: 85 | '''In case of error does not reset data to [] (see commit_data).''' 86 | e = e + '\n' + e.__traceback__ 87 | print (e) 88 | return data 89 | 90 | while True: 91 | 92 | ''' 93 | This is what actually runs the whole time. 94 | It first checks to see if new_input is different from current_input. This would be the case if there was a rotation. 95 | Once it detects that the input is different it knows water is flowing. 96 | It starts tracking the total_rotations and when the last rotation occured. 97 | 98 | After each rotation it refreshes the value of the last rotation time. 99 | 100 | It waits a few seconds (rotation_downtime) after the last rotation time to make sure the water has stopped. 101 | Once the water stops it passes the total_rotations to prep_and_send(). 102 | It also passes 'data' which is any previous water-flow events that were not successfully sent at the time they were recorded. 103 | ''' 104 | 105 | new_input = GPIO.input(inpt) 106 | if new_input != current_input: 107 | total_rotations += 1 108 | if time.time() <= last_movement_time: #if it hasn't been more than 10 seconds 109 | record_data = True 110 | current_input = new_input 111 | last_movement_time = time.time() + rotation_downtime 112 | else: #flow starts 113 | last_movement_time = time.time() + rotation_downtime 114 | 115 | elif record_data == True and time.time() > last_movement_time: #if it's been x seconds since last change 116 | data = prep_and_send(data,total_rotations) 117 | record_data = False 118 | total_rotations = 0 119 | last_movement_time = time.time() + rotation_downtime 120 | current_input = new_input 121 | 122 | ''' 123 | This last part simply prints some helpful information. It also allows for a clean exit if user presses Ctrl + C. 124 | ''' 125 | 126 | try: 127 | print('New input: ',new_input, '. Current input: ', current_input, '. Movements: ', total_rotations) 128 | except KeyboardInterrupt: 129 | print('\nCTRL C - Exiting nicely') 130 | GPIO.cleanup() 131 | sys.exit() 132 | --------------------------------------------------------------------------------