├── Parts.md ├── README.md └── salt-GitHub.py /Parts.md: -------------------------------------------------------------------------------- 1 | HC-SR04 Ultrasonic Module Distance Sensor 2 | https://amzn.to/39ishu7 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Salt-Level 2 | Checks level of salt in water softner brine tank. 3 | 4 | Utilizes utrasonic sensor attached to a Raspberry Pi to comunicate level via email or MQTT. 5 | 6 | Python 2.7; not currently python 3 syntaxed. 7 | Utilizes paho mqtt client (not needed if not utilizing MQTT); just comment out import of mqtt if not using. 8 | Utilizes smtplib library for email communication. 9 | Currently no LCD display setup in script. 10 | 11 | Email sent on a custumizable day of the week to an email address; one additional cc is setup. 12 | Default is Friday for standard email status of salt tank. 13 | Email is sent regardless of day of week if tank is empty. 14 | 15 | Email contains: percentage full, distance of salt from sensor, level of salt in tank, bags of salt needed to fill tank, date and time of email 16 | 17 | MQTT is intergrated with customizable time interval. Default is 10 mins. 18 | Built in error handling for MQTT broker communication failure. 19 | 20 | Debugging has been pre-worked with the used of a single variable at the begining of the script. 21 | Debugging screen color codes are not all used; this is just a section that I put in all my scripts so I have the colors available without having to recreate it everytime. 22 | 23 | Once script is working for your situation, just add to a cron job to start at boot-up. 24 | 25 | ## MQTT 26 | Bags - How many 40lb bags of salt to fill tank 27 | DebugEnabled - True or False 28 | Count - # of MQTT Topics 29 | MQTT_Finish - Ture or False 30 | Email - True or False 31 | MailDay - Day of Week email sent 32 | MailDayInfo - Defines values for MailDay 33 | Time - Time of last MQTT post 34 | Level - Level of salt in inches from bottom of tank 35 | Percent - Percentage full of salt 36 | BottomLv - Just above water level 37 | lvBag - Inches per bag 38 | lvEmpty - Inches from top considered empty 39 | lvFull - level considered full 40 | lvTop - Top of tank from bottom 41 | 42 | ## Change Log 43 | 4/8/2020 44 | Added a pause between MQTT Publish Topics, due to dropped topics on some MQTT Brokers 45 | Expanded MQTT Debug 46 | 47 | 4/17/2020 1430 48 | Added MQTT Topics 49 | Added decimal to PercentFull and Bags 50 | 51 | 4/18/2020 2130 52 | Added MQTT QoS and Retain 53 | -------------------------------------------------------------------------------- /salt-GitHub.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.7 2 | 3 | # Salt Tank Level Program 4 | 5 | # Last Change 4/18/2020 2130 6 | # Added MQTT QoS and Retain 7 | 8 | import time # Sleep Function 9 | import RPi.GPIO as GPIO # GPIO Controls 10 | from datetime import datetime # Now Fuction 11 | import smtplib # Email Library 12 | from decimal import Decimal # Convert float to Decimal 13 | import paho.mqtt.client as mqtt # Allow publishing to MQTT Server 14 | 15 | # sudo python2.7 -m pip install paho-mqtt 16 | 17 | ''' Use BCM GPIO references instead of physical pin numbers ''' 18 | GPIO.setmode(GPIO.BCM) 19 | GPIO.setwarnings(False) 20 | 21 | ''' Define Variables ''' 22 | Trigger = 8 # Set Trigger to GPIO 8 *** As Needed *** 23 | Echo = 25 # Set Echo to GPIO 25 *** As Needed *** 24 | Wait = 2 # Set time between pulses in seconds 25 | Pulse = 0.00001 # Set ultrasonic pulse length in seconds 26 | PWait = 20 # Wait time for testing 27 | PWait = 600 # Time between readings in seconds (24*60*60) *** As desired *** 28 | Debug = True # Print to screen if True; do not comment out 29 | Debug = False # Do not Print to screen if False; comment out to be True 30 | Email = False # False to not send emails; do not comment out 31 | Email = True # True to send emails; *** comment out to be False *** 32 | MQTT_Email = Email # Variable to send Email info to MQTT since a 'Email' variable 'sometimes' as issues 33 | MQTT_Enable = False # False to not send MQTT; do not comment out 34 | MQTT_Enable = True # True to send MQTT *** comment out to be False *** 35 | MQTT_Debug = Debug # Variable to send Debug info to MQTT since a 'Debug' variable 'sometimes' as issues 36 | Samples = 11 # Number of samples to take for average 37 | lvTop = 3 # Top of Tank from Sensor *** Depends on your tank *** 38 | lvBag = 4 # Level of inces of Salt per 40lb bag (4" per bag leveled) *** Depends on your tank *** 39 | lvFull = lvTop + lvBag # Full Tank level 40 | TopLv = 33 # Tank top level from bottom (33.3") *** Depends on your tank *** 41 | BottomLv= 11 # Top of water level from absolute bottom (lowest measureable level) *** Depends on your tank *** 42 | lvEmpty = TopLv-BottomLv# Water level in tank is empty from Sensor, Sensor is at 35" from bottom 43 | Broker_IP = "10.74.1.224" # MQTT Broker IP *** IP address of your MQTT Broker *** 44 | Broker_Port = "1883" # MQTT Broker Port *** Port of your MQTT Broker 1883 is default *** 45 | MQTT_Wait = .01 # Pause between MQTT Topic Pulishing 46 | MailDay = 4 # Monday = 0, Tuesday = 1, Wensday = 2, Thursday = 3, Friday = 4, Saturday = 5, Sunday = 6 *** As Desired *** 47 | 48 | ''' GMail variables ''' 49 | ''' Sanatize for GitHub ''' # Sanatize for GitHub 50 | gmail_user = 'YourSendingEmail@gmail.com' # Gmail account *** Insert your email *** # Sanatize for GitHub 'YourSendingEmail@gmail.com' 51 | gmail_password = 'YourPassword' # Gmail Password *** Insert your password *** # Sanatize for GitHub 'YourPassword' 52 | sent_from = gmail_user # Email Sender 'Do NOT edit' 53 | to = ['1stEmail@gmail.com'] # Email Recipient *** Insert your 1st Recipient *** # Sanatize for GitHub '1stEmail@gmail.com' 54 | cc = ['2ndEmail@gmail.com'] # 2nd Email Recipient *** Insert your 2nd Recipient or comment out cc below *** # Sanatize for GitHub '2ndEmail@gmail.com' 55 | subject = 'Salt Tank Status' # Email Subject can be updated 56 | body = 'Body of email' # Email Body can be updated 57 | 58 | mail_sent = False # Allow only one email on the designated day. 59 | 60 | ''' Debug print to screen colors ''' 61 | POff = '\033[0m' # Color Effects Off 62 | PBold = '\033[1m' # Bold 63 | PUnderline = '\033[4m' # Underline single 64 | PBoldOff = '\033[21m' # Bold Off 65 | PBlinkOff = '\033[25m' # Blink Off 66 | PBlack = '\033[90m' # Black 67 | PRed = '\033[91m' # Red 68 | PGreen = '\033[92m' # Green 69 | PYellow = '\033[93m' # Yellow 70 | PBlue = '\033[94m' # Blue 71 | PPurple = '\033[95m' # Purple 72 | PCyan = '\033[96m' # Cyan 73 | PWhite = '\033[97m' # White 74 | 75 | 76 | ''' Set pins as output and input ''' 77 | GPIO.setup(Trigger, GPIO.OUT) # Trigger 78 | GPIO.setup(Echo, GPIO.IN) # Echo 79 | 80 | ''' Set trigger to False (Low) ''' 81 | GPIO.output(Trigger, False) 82 | 83 | ''' Define functions''' 84 | 85 | def measure(): 86 | # This function measures a distance 87 | GPIO.output(Trigger, True) 88 | time.sleep(Pulse) 89 | GPIO.output(Trigger, False) 90 | start = time.time() 91 | while GPIO.input(Echo)==0: 92 | start = time.time() 93 | while GPIO.input(Echo)==1: 94 | stop = time.time() 95 | elapsed = stop-start 96 | distance = Decimal(elapsed * 6744.166310046,2) - lvTop 97 | # 34300 is speed of sound in cm/sec or 13488.332620092 in/sec 98 | # Divide by 2 to account for sound there and echo back 99 | # Divide by 2.54 to convert to inches from cm 100 | # Conversion = 6751.95 in/sec or 6744.166310046 101 | # v = 331m/s + 0.6m/s/C * T speed of sound in m/s compensated for temperature or speed = 331 + 0.6 * Temp in C 102 | # Velocity = 331.4 + 0.6*Temperature + 0.0124*Relative_Humidity ... Temperature is in Celsius Degrees ... Relative Humidity can be measured by sensors in %age ... Velocity in m/sec 103 | return distance 104 | 105 | def measure_average(): 106 | # This function takes measurements and returns the average. 107 | distance = 0 # Ensure distance zeroed out 108 | for i in range (1, Samples, 1): 109 | time.sleep(Wait) 110 | distance = distance + measure() 111 | if Debug is True: 112 | print (Samples - i), 113 | print "\t", 114 | print (round(distance/i,2)) 115 | distance = distance / (Samples - 1) 116 | return distance 117 | 118 | def Message(): 119 | # This function sends the email 120 | # Indents affect email formating 121 | toAll = to + cc 122 | if Debug is True: 123 | # Removed cc for testing 124 | toAll = to 125 | try: 126 | email_text = """\ 127 | From: %s 128 | To: %s 129 | Subject: %s 130 | 131 | %s 132 | """ % (sent_from, ", ".join(to), subject, body) 133 | server = smtplib.SMTP_SSL('smtp.gmail.com', 465) 134 | server.ehlo() 135 | server.login(gmail_user, gmail_password) 136 | server.sendmail(sent_from, toAll, email_text) 137 | server.close() 138 | if Debug is True: 139 | print (PCyan) 140 | print "sent_from:\t", 141 | print (sent_from) 142 | print "to:\t\t", 143 | print (to) 144 | print "subject:\t", 145 | print (subject) 146 | print "body:\t\t", 147 | print (body) 148 | print (PPurple) 149 | print "email_text:" 150 | print (email_text) 151 | print (POff) 152 | 153 | except: 154 | #Handle email errors without crashing 155 | if Debug is True: 156 | print "Email send failure" 157 | 158 | def MQTT(): 159 | # Send to the MQTT Broker 160 | try: 161 | mqttc = mqtt.Client("python_pub") 162 | mqttc.connect(Broker_IP, Broker_Port) 163 | time.sleep(MQTT_Wait) 164 | QoS = 0 165 | Retain = True 166 | # mqttc.publish(Topic, Payload, QoS, Retain) 167 | # QoS 0=Send only, 1=Confirm, 2=send until confirmed 168 | mqttc.publish("salt/Debugy/MQTT_Finish", 'False', QoS, Retain) 169 | if Debug is True: print 'MQTT published MQTT_All False' 170 | time.sleep(MQTT_Wait) 171 | mqttc.publish("salt/Debugy/Count", '17', QoS, Retain) 172 | if Debug is True: print 'MQTT published TopicCount' 173 | time.sleep(MQTT_Wait) 174 | mqttc.publish("salt/Percent", PercentFull, QoS, Retain) 175 | if Debug is True: print 'MQTT published Percent' 176 | time.sleep(MQTT_Wait) 177 | mqttc.publish("salt/Level", SaltLv, QoS, Retain) 178 | if Debug is True: print 'MQTT published Level' 179 | time.sleep(MQTT_Wait) 180 | mqttc.publish("salt/Bags", Bags, QoS, Retain) 181 | if Debug is True: print 'MQTT published Bags' 182 | time.sleep(MQTT_Wait) 183 | mqttc.publish("salt/Email/Emails", MQTT_Email, QoS, Retain) 184 | if Debug is True: print 'MQTT published Emails' 185 | time.sleep(MQTT_Wait) 186 | mqttc.publish("salt/Time", ETime, QoS, Retain) 187 | if Debug is True: print 'MQTT published Time' 188 | time.sleep(MQTT_Wait) 189 | mqttc.publish("salt/Debugy/DebugEnabled", MQTT_Debug, QoS, Retain) 190 | if Debug is True: print 'MQTT published Debug' 191 | time.sleep(MQTT_Wait) 192 | mqttc.publish("salt/Email/MailDay", MailDay, QoS, Retain) 193 | if Debug is True: print 'MQTT published MailDay' 194 | time.sleep(MQTT_Wait) 195 | mqttc.publish("salt/Email/MailDayInfo", 'Monday = 0, Tuesday = 1, Wensday = 2, Thursday = 3, Friday = 4, Saturday = 5, Sunday = 6', QoS, Retain) 196 | if Debug is True: print 'MQTT published MailDayInfo' 197 | time.sleep(MQTT_Wait) 198 | mqttc.publish("salt/Constant/Samples", Samples, QoS, Retain) 199 | if Debug is True: print 'MQTT published Samples' 200 | time.sleep(MQTT_Wait) 201 | mqttc.publish("salt/Constant/lvTop", lvTop, QoS, Retain) 202 | if Debug is True: print 'MQTT published lvTop' 203 | time.sleep(MQTT_Wait) 204 | mqttc.publish("salt/Constant/lvBag", lvBag, QoS, Retain) 205 | if Debug is True: print 'MQTT published lvBag' 206 | time.sleep(MQTT_Wait) 207 | mqttc.publish("salt/Constant/lvFull", lvFull, QoS, Retain) 208 | if Debug is True: print 'MQTT published lvFull' 209 | time.sleep(MQTT_Wait) 210 | mqttc.publish("salt/Constant/TopLv", TopLv, QoS, Retain) 211 | if Debug is True: print 'MQTT published TopLv' 212 | time.sleep(MQTT_Wait) 213 | mqttc.publish("salt/Constant/BottomLv", BottomLv, QoS, Retain) 214 | if Debug is True: print 'MQTT published BottomLv' 215 | time.sleep(MQTT_Wait) 216 | mqttc.publish("salt/Constant/lvEmpty", lvEmpty, QoS, Retain) 217 | if Debug is True: print 'MQTT published lvEmpty' 218 | time.sleep(MQTT_Wait) 219 | mqttc.publish("salt/Debugy/MQTT_Finish", 'True', QoS, Retain) 220 | if Debug is True: print 'MQTT published MQTT_All True' 221 | if Debug is True: print "All MQTT updated" 222 | except: 223 | # Prevent crashing if Broker is disconnected 224 | if Debug is True: 225 | print "MQTT Failed" 226 | 227 | ''' Main Script ''' 228 | ''' Wrap main content in a try block so we can catch the user pressing CTRL-C and run the GPIO cleanup function. ''' 229 | ''' This will also prevent the user seeing lots of unnecessary error messages. ''' 230 | 231 | try: 232 | 233 | if Debug is True: 234 | now = datetime.now() 235 | current_time = "Starting Salt Tank Measurement at: " + str(now.strftime("%H:%M:%S")) 236 | print (PRed) 237 | print (current_time) 238 | print ("Debug is On") 239 | if Email is True: 240 | print ("Email will be sent") 241 | else: 242 | print ("Email will NOT be sent") 243 | Temp = (Samples*Wait) 244 | print (str(Temp)) + " seconds per Average; ", 245 | print (str(Samples-1)) + " Samples per Average" 246 | print (str(PWait)) + " second(s) or " + str(PWait/60) + " minute(s) or " + str(PWait/60/60) + " hour(s); between Reading" 247 | print "Inches to max possible salt level from sensor: " + str(lvTop) 248 | print "Inches of salt per 40lb bag: " + str(lvBag) 249 | print "Inches from sensor considered full of salt: " + str(lvFull) 250 | print "Inches from sensor considered empty of salt: " + str(lvEmpty) 251 | print (POff) 252 | 253 | while True: 254 | distance = measure_average() 255 | Dist = str(round(distance,2)) 256 | Bags = str(round((distance-lvTop)/lvBag,1)) 257 | #PercentFull = str(int((TopLv - lvTop - distance)/(TopLv-lvTop)*100)) 258 | PercentFull = str(round((lvEmpty - distance + lvTop)/(lvEmpty)*100,1)) 259 | SaltLv = str(round(TopLv - distance,2)) 260 | now = datetime.now() 261 | ETime = str(now.strftime("%H:%M:%S on %m-%d-%Y")) 262 | if MQTT_Enable is True: 263 | MQTT() 264 | 265 | if Debug is True: 266 | print (PYellow) 267 | #now = datetime.now() 268 | Dist_Time = "Average Distance = " + Dist + " at " + ETime 269 | print (Dist_Time), 270 | print " / " + Bags + " bags of salt needed. / ", 271 | print (PercentFull), 272 | print "% Full / Salt Level is ", 273 | print (SaltLv), 274 | print "inces." 275 | print (POff) 276 | 277 | if distance <= lvFull: 278 | subject = 'Salt Tank is FULL' 279 | body = "Tank Full at " + PercentFull + "%, " + SaltLv + " inches of Salt, " + Dist + " inches from top " + ETime 280 | elif distance >= lvEmpty: 281 | subject = 'Salt Tank is EMPTY' 282 | body = "Tank Empty at " + PercentFull + "%, " + SaltLv + " inches of Salt, " + Dist + " inches from top, " + Bags + " Bags of Salt to Fill " + ETime 283 | else: 284 | subject = 'Salt Tank Status' 285 | body = "Tank Level is " + PercentFull + "%, " + SaltLv + " inches of Salt, " + Dist + " inches from top, " + Bags + " Bags of Salt to Fill " + ETime 286 | 287 | if Email is True: 288 | today = datetime.today() 289 | Weekday = today.weekday() 290 | if Debug is True: 291 | # Send Email during Debug regardless of day or send schedule 292 | print "Current day of week is " + str(Weekday) 293 | print "Monday = 0, Tuesday = 1, Wensday = 2, Thursday = 3, Friday = 4, Saturday = 5, Sunday = 6" 294 | Message() 295 | else: 296 | if distance >= lvEmpty and mail_sent is False: 297 | # Send email if empty regardless of day of week 298 | subject = 'Salt Tank is EMPTY' 299 | body = "Tank Empty at " + PercentFull + "%, " + SaltLv + " inches of Salt, " + Dist + " inches from top, " + Bags + " Bags of Salt to Fill " + ETime 300 | Message() 301 | mail_sent = True # Allow only one empty level email vice one every read cycle 302 | else: 303 | if Weekday == MailDay and mail_sent is False: 304 | # Send email for tank status 305 | subject = 'Salt Tank Status' 306 | body = "Tank Level is " + PercentFull + "%, " + SaltLv + " inches of Salt, " + Dist + " inches from top, " + Bags + " Bags of Salt to Fill " + ETime 307 | Message() 308 | mail_sent = True # Allow only this email on sending day 309 | if Weekday <> MailDay: 310 | mail_sent = False # Reset to allow email next time 311 | 312 | time.sleep(PWait) # Sleep between readings 313 | 314 | except KeyboardInterrupt: 315 | ''' User pressed CTRL-C / Reset GPIO settings ''' 316 | GPIO.cleanup() 317 | --------------------------------------------------------------------------------