Hardware components | ||||||
![]() |
| × | 1 | |||
![]() |
| × | 4 | |||
![]() |
| × | 1 | |||
Software apps and online services | ||||||
![]() |
|
Wanted to monitor salt level in my softener brine tank, I seem to always forget.
I have a Duel Vessel Water Softener with a common brine tank. I tapped into the onboard flow sensors to get the current flow of water throughput. These are Pulse type flow sensors that pulse with every gallon of water. Voltage divider to reduce the voltage from 6V pluses to 3V for GPIO.
Then added a few micro limit switches to give status of the valve positions in the softener to determine if it is in service, standby or regeneration.
Then for Brine Tank level used a ToF Laser Distance Sensor good for up to 4M with a 2mm accuracy. I started out with ultrasonic distance but it was not stable.
The display graphic is made in Pygame to give visualization when I connect with VNC from my smart phone. I will add a small local screen possibly as well.
All the data is pushed to CloudRPI for trending and alarms.
https://cloud4rpi.io/s/2PyVs8iZc here is a link to view the data.
import pygame
from time import *
import RPi.GPIO as GPIO
import time, sys
from pygame.locals import *
from arrow import *
import cloud4rpi
import rpi
import qwiic
ToF = qwiic.QwiicVL53L1X()
if (ToF.sensor_init() == None):# Begin returns 0 on a good init
print("Sensor online!\n")
# setup inputs/outputs
GPIO.setmode(GPIO.BCM)
#Setup GPIO
GPIO.setwarnings(False)
GPIO.setup(15, GPIO.IN, pull_up_down = GPIO.PUD_UP) #vessel1 flow sensor
GPIO.setup(14, GPIO.IN, pull_up_down = GPIO.PUD_UP) #vessel2 flow sensor
GPIO.setup(20, GPIO.IN, pull_up_down = GPIO.PUD_DOWN)
GPIO.setup(16, GPIO.IN, pull_up_down = GPIO.PUD_DOWN)
GPIO.setup(26, GPIO.IN, pull_up_down = GPIO.PUD_DOWN)
GPIO.setup(19, GPIO.IN, pull_up_down = GPIO.PUD_DOWN)
Vessel1 = 'REGEN'
Vessel2 = 'REGEN'
# Put your device token here. To get the token,
# sign up at https://cloud4rpi.io and create a device.
DEVICE_TOKEN = 'Your Token Here'
# Change these values depending on your requirements.
DATA_SENDING_INTERVAL = 60 # secs
DIAG_SENDING_INTERVAL = 650 # secs
POLL_INTERVAL = 1.5 # 500 ms
#Max Level
max_a = 100
barPos = (430, 245)
barSize = (20, 125)
borderColor = (0, 0, 0)
barColor = (0, 128, 0)
#####################################################################################
# pygame
pygame.init()
# setup screen
screenDimentions = (1024, 768)
screen = pygame.display.set_mode((screenDimentions))
pygame.display.set_caption('WATER SOFTENER')
view_mode = 'Normal'
done = False
# setup font size
FONTSIZE = 22
LINEHEIGHT = 18
basicFont = pygame.font.SysFont(None, FONTSIZE)
Alarm = ''
# setup font colors
BLACK = (255,255,255)
WHITE = (0,0,0)
GREY = (200,200,200)
DKGREY = (169,169,169)
RED = (255, 0, 0)
BLUE = (0, 0, 255)
# Background
bg = pygame.image.load('SOFT-bg4.png')
screen.blit(bg,(0,0))
alarm_bg = pygame.image.load('Alarm.png')
# setup the flow arrows
in_arrow = arrow(135, 123, 135, 150)
out_arrow = arrow(735, 408, 735, 750)
class Button():
def __init__(self, txt, location, action, bg=DKGREY, fg=WHITE, size=(80, 30), font_name="Segoe Print", font_size=16):
self.color = bg # the static (normal) color
self.bg = bg # actual background color, can change on mouseover
self.fg = fg # text color
self.size = size
self.font = pygame.font.SysFont(font_name, font_size)
self.txt = txt
self.txt_surf = self.font.render(self.txt, 1, self.fg)
self.txt_rect = self.txt_surf.get_rect(center=[s//2 for s in self.size])
self.surface = pygame.surface.Surface(size)
self.rect = self.surface.get_rect(center=location)
self.call_back_ = action
def draw(self):
self.mouseover()
self.surface.fill(self.bg)
self.surface.blit(self.txt_surf, self.txt_rect)
screen.blit(self.surface, self.rect)
def mouseover(self):
self.bg = self.color
pos = pygame.mouse.get_pos()
if self.rect.collidepoint(pos):
self.bg = GREY # mouseover color
def call_back(self):
self.call_back_()
def DrawBar(pos, size, borderC, barC):
pygame.draw.rect(screen, borderC, (*pos, *size), 1)
innerPos = (pos[0]+3, pos[1]+3)
innerSize = ((size[0]-6) * size[1]-6)
# def mousebuttondown():
# pos = pygame.mouse.get_pos()
# for button in buttons:
# if button.rect.collidepoint(pos):
# button.call_back()
def drawText(surface, text, color, rect, font, aa=False, bkg=None):
rect = Rect(rect)
y = rect.bottom
lineSpacing = -2
# get the height of the font
fontHeight = font.size("Tg")[1]
while text:
i = 1
# determine if the row of text will be outside our area
if y + fontHeight > rect.bottom:
break
# determine maximum width of line
while font.size(text[:i])[0] < rect.width and i < len(text):
i += 1
# if we've wrapped the text, then adjust the wrap to the last word
if i < len(text):
i = text.rfind(" ", 0, i) + 1
# render the line and blit it to the surface
if bkg:
image = font.render(text[:i], 1, color, bkg)
image.set_colorkey(bkg)
else:
image = font.render(text[:i], aa, color)
screen.blit(image, (rect.left, y))
y += fontHeight + lineSpacing
# remove the text we just blitted
text = text[i:]
return text
startTime = int(time.time())
class FlowMeter():
gallons_per_liter = 0.264172
seconds_per_minute = 1
MS_per_second = 1000
displayFormat = 'metric'
enabled = True
clicks = 0
lastClick = 0
clickDelta = 0
hertz = 0.0
flow = 0 # in Liters per second
thisflow = 0.0 # in Liters
total = 0.0 # in Liters
constant = .37854
def __init__(self, displayFormat, enabled):
self.displayFormat = displayFormat
self.clicks = 0
self.lastClick = int(time.time() * FlowMeter.MS_per_second)
self.clickDelta = 0
self.hertz = 0.0
self.flow = 0.0
self.thisflow = 0.0
self.total = 0.0
self.enabled = True
def update(self, currentTime):
self.clicks += 1
# get the time delta
self.clickDelta = max((currentTime - self.lastClick), 1)
# calculate the instantaneous speed
if (self.enabled == True):
self.hertz = FlowMeter.MS_per_second / self.clickDelta
self.flow = self.hertz / (FlowMeter.seconds_per_minute * FlowMeter.constant) # In Liters per second
instflow = self.flow * (self.clickDelta / FlowMeter.MS_per_second)
self.thisflow += instflow
self.total += instflow
# Update the last click
self.lastClick = currentTime
def getFormattedClickDelta(self):
return str(self.clickDelta) + ' ms'
def getFormattedHertz(self):
return str(round(self.hertz,3)) + ' Hz'
def getFormattedFlow(self):
if(self.displayFormat == 'metric'):
return str(round(self.flow,3)) + ' L/s'
else:
return str(round(self.flow * FlowMeter.gallons_per_liter, 3)) + ' gallons/s'
def getFormattedThisflow(self):
if(self.displayFormat == 'metric'):
return str(round(self.thisflow,3)) + ' L'
else:
return str(round(self.thisflow * FlowMeter.gallons_per_liter, 3)) + ' gallons'
def getFormattedTotalflow(self):
if(self.displayFormat == 'metric'):
return str(round(self.total,3)) + ' L'
else:
return str(round(self.total * FlowMeter.gallons_per_liter, 3)) + ' gallons'
def clear(self):
self.thisflow = 0;
self.total = 0;
# Flow, on Pin 15.
def doAClick(channel):
currentTime = int(time.time() * FlowMeter.MS_per_second)
if fm.enabled == True:
fm.update(currentTime)
# Flow, on Pin 14.
def doAClick2(channel):
currentTime = int(time.time() * FlowMeter.MS_per_second)
if fm2.enabled == True:
fm2.update(currentTime)
GPIO.add_event_detect(15, GPIO.RISING, callback=doAClick, bouncetime=20)
GPIO.add_event_detect(14, GPIO.RISING, callback=doAClick2, bouncetime=20)
fm = FlowMeter('metric', 'enabled')
fm2 = FlowMeter('metric', 'enabled')
def distance():
ToF.start_ranging()# Write configuration bytes to initiate measurement
time.sleep(.005)
distance = ToF.get_distance()# Get the result of the measurement from the sensor
time.sleep(.005)
ToF.stop_ranging()
return(distance)
def Vessel1():
vessel1 = 'REGEN'
if GPIO.input(20) == 1 and GPIO.input(16) == 1:
Vessel1 = 'IN SERVICE'
if GPIO.input(20) == 0 and GPIO.input(16) == 0:
Vessel1 = 'STANDBY'
if GPIO.input(20) == 1 and GPIO.input(16) == 0:
Vessel1 = 'REGEN'
if GPIO.input(20) == 0 and GPIO.input(16) == 1:
Vessel1 = 'REGEN'
return(Vessel1)
def Vessel2():
vessel2 = 'REGEN'
if GPIO.input(26) == 1 and GPIO.input(19) == 1:
Vessel2 = 'IN SERVICE'
if GPIO.input(26) == 0 and GPIO.input(19) == 0:
Vessel2 = 'STANDBY'
if GPIO.input(26) == 1 and GPIO.input(19) == 0:
Vessel2 = 'REGEN'
if GPIO.input(26) == 0 and GPIO.input(19) == 1:
Vessel2 = 'REGEN'
return(Vessel2)
def sensor_not_connected():
return 'Sensor not connected'
flowtime = int(time.time())
def FLOW1():
currentTime = int(time.time() * FlowMeter.MS_per_second)
if (currentTime - fm.lastClick > 1000):
fm.flow = 0.0
FLOW1 = round(fm.flow,3)
return(FLOW1)
def FLOW1T():
if Vessel1 == 'STANDBY':
fm.total = 0.0
currentTime = startTime * FlowMeter.MS_per_second
FLOW1T = round((fm.total / 60),3)
return(FLOW1T)
def FLOW2():
currentTime = int(time.time() * FlowMeter.MS_per_second)
if (currentTime - fm2.lastClick > 1000):
fm2.flow = 0.0
FLOW2 = round(fm2.flow,3)
return(FLOW2)
def FLOW2T():
if Vessel2 == 'STANDBY':
fm2.total = 0.0
currentTime = int(time.time() * FlowMeter.MS_per_second)
FLOW2T = round((fm2.total / 60),3)
return(FLOW2T)
def FLOW():
FLOW = round(fm.flow + fm2.flow,3)
return(FLOW)
def FLOWT():
FLOWT = round(((fm.total + fm2.total) / 60),3)
return(FLOWT)
def LEVEL():
LEVEL = round((1000 - distance())/10/80*100,1)
return(LEVEL)
screen.blit(bg,(0,0))
#####################################################################################
# main loop
while not done:
screen.blit(bg,(0,0))
# Put variable declarations here
# Available types: 'bool', 'numeric', 'string', 'location'
variables = {
'Vessel1': {
'type': 'string',
'bind': Vessel1
},
'Vessel2': {
'type': 'string',
'bind': Vessel2
},
'FLOW': {
'type': 'numeric',
'bind': FLOW
},
'FLOWT': {
'type': 'numeric',
'bind': FLOWT
},
'FLOW1': {
'type': 'numeric',
'bind': FLOW1
},
'FLOW1T': {
'type': 'numeric',
'bind': FLOW1T
},
'FLOW2': {
'type': 'numeric',
'bind': FLOW2
},
'FLOW2T': {
'type': 'numeric',
'bind': FLOW2T
},
'LEVEL': {
'type': 'numeric',
'bind': LEVEL
},
#'LED On': {
# 'type': 'bool',
# 'value': False,
# 'bind': led_control
# },
'CPU Temp': {
'type': 'numeric',
'bind': rpi.cpu_temp
},
}
diagnostics = {
'CPU Temp': rpi.cpu_temp,
# 'IP Address': rpi.ip_address,
# 'Host': rpi.host_name,
# 'Operating System': rpi.os_name,
# 'Client Version:': cloud4rpi.__version__,
}
device = cloud4rpi.connect(DEVICE_TOKEN)
try:
device.declare(variables)
device.declare_diag(diagnostics)
device.publish_config()
# Adds a 1 second delay to ensure device variables are created
sleep(1)
data_timer = 0
diag_timer = 0
while True:
screen.blit(bg,(0,0))
#####################################################################################
def renderThings(FLOW, FLOWT, FLOW1, FLOW1T, LEVEL, windowSurface, basicFont):
screen.blit(bg,(0,0))
#draw the flow arrows
if FLOW() >= 2:
in_arrow.update()
screen.blit(in_arrow.image,(in_arrow.x, in_arrow.y))
out_arrow.update()
screen.blit(out_arrow.image,(out_arrow.x, out_arrow.y))
# Draw Vessel1 Status Box
if GPIO.input(20) == 1 and GPIO.input(16) == 1:
pygame.draw.rect(screen, (0, 255, 0), pygame.Rect(565, 135, 30, 10))
if GPIO.input(20) == 0 and GPIO.input(16) == 0:
pygame.draw.rect(screen, (255, 0, 0), pygame.Rect(565, 135, 30, 10))
if GPIO.input(20) == 1 and GPIO.input(16) == 0:
pygame.draw.rect(screen, (0, 255, 255), pygame.Rect(565, 135, 30, 10))
if GPIO.input(20) == 0 and GPIO.input(16) == 1:
pygame.draw.rect(screen, (0, 255, 255), pygame.Rect(565, 135, 30, 10))
# Draw Vessel2 Status Box
if GPIO.input(26) == 1 and GPIO.input(19) == 1:
pygame.draw.rect(screen, (0, 255, 0), pygame.Rect(643, 135, 30, 10))
if GPIO.input(26) == 0 and GPIO.input(19) == 0:
pygame.draw.rect(screen, (255, 0, 0), pygame.Rect(643, 135, 30, 10))
if GPIO.input(26) == 1 and GPIO.input(19) == 0:
pygame.draw.rect(screen, (0, 255, 255), pygame.Rect(643, 135, 30, 10))
if GPIO.input(26) == 0 and GPIO.input(19) == 1:
pygame.draw.rect(screen, (0, 255, 255), pygame.Rect(643, 135, 30, 10))
# Draw Salt Level
text = basicFont.render((str(LEVEL()) + ' %'), True, WHITE, BLACK)
textRect = text.get_rect()
screen.blit(text, (450, 375 + LINEHEIGHT))
text = basicFont.render("SALT LEVEL", True, WHITE, BLACK)
textRect = text.get_rect()
screen.blit(text, (425, 430 - LINEHEIGHT))
#Draw Pressure 1
text = basicFont.render("SUPPLY", True, WHITE, BLACK)
textRect = text.get_rect()
screen.blit(text, (275, 130 - LINEHEIGHT))
# text = basicFont.render((str(PRESS1) + ' PSI'), True, WHITE, BLACK)
# textRect = text.get_rect()
# screen.blit(text, (275, 120 + LINEHEIGHT))
#Draw Pressure 2
text = basicFont.render("SYSTEM", True, WHITE, BLACK)
textRect = text.get_rect()
screen.blit(text, (735, 415 - LINEHEIGHT))
# text = basicFont.render((str(PRESS2) + ' PSI'), True, WHITE, BLACK)
# textRect = text.get_rect()
# screen.blit(text, (735, 407 + LINEHEIGHT))
#Draw Vessel1 flow
text = basicFont.render((str(FLOW1()) + ' L/min'), True, WHITE, BLACK)
textRect = text.get_rect()
screen.blit(text, (545, 90 + LINEHEIGHT))
#Draw Vessel2 flow
text = basicFont.render((str(FLOW2()) + ' L/min'), True, WHITE, BLACK)
textRect = text.get_rect()
screen.blit(text, (635, 90 + LINEHEIGHT))
#Draw Total flow
text = basicFont.render((str(FLOW()) + ' L/min'), True, WHITE, BLACK)
textRect = text.get_rect()
screen.blit(text, (735, 425 + LINEHEIGHT))
# Draw Totalizers
text = basicFont.render("TOTALIZERS", True, WHITE, BLACK)
textRect = text.get_rect()
screen.blit(text, (55, 40 - LINEHEIGHT))
text = basicFont.render('VESSEL 1 ' + (str(FLOW1T()) + ' Liters'), True, WHITE, BLACK)
textRect = text.get_rect()
screen.blit(text, (40, 30 + (LINEHEIGHT)))
text = basicFont.render('VESSEL 2 ' + (str(FLOW2T()) + ' Liters'), True, WHITE, BLACK)
textRect = text.get_rect()
screen.blit(text, (40, 30 + (2 * (LINEHEIGHT))))
text = basicFont.render('TOTAL ' + (str(FLOWT()) + ' Liters'), True, WHITE, BLACK)
textRect = text.get_rect()
screen.blit(text, (40, 30 + (3 * (LINEHEIGHT))))
#Draw Level indicator
DrawBar(barPos, barSize, borderColor, barColor)
pygame.draw.rect(screen, (0, 0, 128), pygame.Rect((430, 370), (20, (-115 / max_a * LEVEL()))))
pygame.display.flip()
if view_mode == 'Alarm':
screen.blit(alarm_bg,(0,0))
else:
view_mode == 'Normal'
# Setup Alarms
if LEVEL() <= 20:
view_mode = 'Alarm'
text = basicFont.render("LOW SALT LEVEL ALARM", True, WHITE, BLACK)
textRect = text.get_rect()
screen.blit(text, (75, 35 - LINEHEIGHT))
if data_timer <= 0:
device.publish_data()
data_timer = DATA_SENDING_INTERVAL
if diag_timer <= 0:
device.publish_diag()
diag_timer = DIAG_SENDING_INTERVAL
sleep(POLL_INTERVAL)
diag_timer -= POLL_INTERVAL
data_timer -= POLL_INTERVAL
for event in pygame.event.get():
if event.type == pygame.QUIT:
GPIO.cleanup
pygame.quit()
sys.exit()
except Exception as e:
error = cloud4rpi.get_error_message(e)
cloud4rpi.log.exception("ERROR! %s %s", error, sys.exc_info()[0])
finally:
GPIO.cleanup
pygame.quit()
sys.exit()
Comments
Please log in or sign up to comment.