Welcome to Hackster!
Hackster is a community dedicated to learning hardware, from beginner to pro. Join us, it's free!
Hackster is hosting Impact Spotlights: Smart Home. Watch the stream live on Thursday!Hackster is hosting Impact Spotlights: Smart Home. Stream on Thursday!
Chad
Published

Water Softener Monitoring

Monitor Water Softener Flow, Flow Total, Salt Level, and Alarms

IntermediateWork in progress12 hours766
Water Softener Monitoring

Things used in this project

Story

Read more

Schematics

Alarm

Code

Code

Python
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()

             
        

Credits

Chad
5 projects • 12 followers
Contact

Comments

Please log in or sign up to comment.