####################################################################### # facilities.py - Monitors a Mosquitto MQTT queue for facility events # from an array of facility panels in bathrooms. These panels consist # of a 3 button panel and an ESP8266 board to gain WIFI access and # send publish messages to the MQTT broker. These # panels are used to indicate issues in the bathrooms that need attention. # Currently, these are known as: # Top Button = Paper Towels = Button 1 value # Middle Button = Soap Dispenser Issue = Button 2 value # Bottom Button = Plumbing Issue = Button 3 value # # The hardware to do this is already developed (AdaFruit Huzzah # ESP8266 with NodeCMU), along with the Lua software to run on # the ESP8266. # # This code is expecting messages from the Mosquitto MQTT broker # which include a sensor name (room number) followed by 3 values for # the 3 buttons on each panel. An example of the contents would be: # "BR-C-404 1 0 1" # # where a value of 1 indicates the button is currently pressed # and a value of 0 indicates the button is not pressed. These messages # are published under the topic "/facilities". # # Upon receiving the button press event, this code then determines # the current state of the facilities request for this type of service # request. If no facilities request has been made, then an email is # generated to the facilities request email address via a # gmail account. After the request email has been made, future button # presses for the same service will not result in additional emails # to the facilities department. # # The facilities request department then assigns the issue internally # in their system, which generates an email back to the gmail account # with a subject of 'Your Facilities Request #00047546 has been assigned'. # # Once the bathroom item is serviced, the facilities request department # will mark the work order as completed, which generates an email back # to the gmail account with the subject of "Your Facilities Request # #00047546 is complete. Can you please complete our Survey?". When this # happens, this script then publishes back to the Mosquitto MQTT broker # on the topic "/completed". When the ESP8266 board receives this message, # it resets the LED near the button. # # In order to track the number of requests serviced, we created a # dashboard in the Cayenne myDevices tool. Once the Cayenne software # is loaded on the Raspberry Pi, it uses another MQTT broker to send and # receive messages with the dashboard. This code then sends messages to # local Cayenne MQTT broker, which publishes them to the dashboard. # Each statistic that is tracked gets a unique channel number within # the Cayenne MQTT server. The channel numbers are as follows: # # Problem Item Action Taken MQTT Channel Number # =========================================================== # Paper Towels Button Pressed 1 # Soap Dispenser Button Pressed 2 # Plumbing Issue Button Pressed 3 # Paper Towels Submitted Requests 4 # Soap Dispenser Submitted Requests 5 # Plumbing Issue Submitted Requests 6 # Paper Towels Completed Requests 7 # Soap Dispenser Completed Requests 8 # Plumbing Issue Completed Requests 9 # Paper Towels Reset Last Request 10 # Soap Dispenser Reset Last Request 11 # Plumbing Issue Reset Last Request 12 # # Pressing the Reset Last Request button on the dashboard sends a value # of '1' to the MQTT broker on the Raspberry Pi on that particular channel. # This causes the Pi to reset the state of the current outstanding request for # that particular problem. This then allows the button to pressed again. # ###################################################################### ######################## # Libraries ######################## import os import string import paho.mqtt.client as mqtt import Adafruit_IO import time import smtplib from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from gmail import Gmail # Cayenne stuff import cayenne.client import time ######################## # Globals ######################## # -- Change these as needed for your installation -- localBroker = "xx.xx.xx.xx" # Local MQTT broker ( IP Address of the Raspberry Pi ) localPort = 1883 # Local MQTT port localUser = "mosquitto" # Local MQTT user localPass = "hackathon" # Local MQTT password localTopic = "/facilities" # Local MQTT topic to monitor publishTopic = "/completed" # Used to publish complete messages to localTimeOut = 120 # Local MQTT session timeout # Cayenne authentication info. This should be obtained from the Cayenne Dashboard. MQTT_USERNAME = "CAYENNE_USERNAME" MQTT_PASSWORD = "CAYENNE_PASSWORD" MQTT_CLIENT_ID = "CAYENNE_CLIENT_ID" # Gmail account to use for sending facilities requests and looking for responses. sender = 'gmailaccount@gmail.com' sender_password = 'gmailpassword' # Receivers of the facilities request messages receivers = 'facilitiesrequestemailaddress@yourcompany.com' sensorList = {} # List of sensor objects ########################### # Create a cayenne client ########################### cayenneClient = cayenne . client . CayenneMQTTClient () ######################## # Classes and Methods ######################## class sensor (): def __init__ ( self ): self . name = "" # Name of sensor in MQTT ( Room Number in this case ) self . humanName = "" # Human-meaningful name (e.g., "front door") self . buttonState = [ "Not Pressed" , "Not Pressed" , "Not Pressed" ] # State of the object: unknown, Not Pressed, Pressed self . buttonName = [ "Paper Towels" , "Soap Dispenser" , "Plumbing" ] # Names for each button self . facRequestState = [ "Not Started" , "Not Started" , "Not Started" ] # State of the facilities request: Not Started, Sent, Received, Completed self . requestID = [ 0 , 0 , 0 ] # used to store the internal request ID assigned to this button press in the facilities request system self . statsButtonPresses = [ 0 , 0 , 0 ] # stats to track button presses on each button self . statsRequestSent = [ 0 , 0 , 0 ] # stats to track requests sent on each button self . statsRequestCompleted = [ 0 , 0 , 0 ] # stats to track completed reqeusts on each button def setButtonState ( self , index , newstate ): # Store the new state for this button index self . buttonState [ index ] = newstate def getButtonState ( self , index ): # get the button state for this button index return self . buttonState [ index ] def setFacRequestState ( self , index , newState ): # store the facilities request state for this button index self . facRequestState [ index ] = newState def getFacRequestState ( self , index ): # get the facilities request state for this button index return self . facRequestState [ index ] def setFacRequestID ( self , index , id ): # store the facilities request ID for this button index self . requestID [ index ] = id def getFacRequestID ( self , index ): # get the facilities request ID for this button index return self . requestID [ index ] def setname ( self , newName , humanName ): self . name = newName self . humanName = humanName def getname ( self ): return self . humanName def getButtonName ( self , index ): # get the button name for this button index return self . buttonName [ index ] def checkButtonState ( self , index , newState ): # check the button state for this button index to see if it has changed if ( "unknown" == self . buttonState [ index ]): self . buttonState [ index ] = newState return 0 else : if ( newState != self . buttonState [ index ]): # state has changed self . buttonState [ index ] = newState # if the button is now in the pressed state if ( "Pressed" == self . buttonState [ index ]): # increment our stats for this button self . statsButtonPresses [ index ] += 1 ######################## # Cayenne Update ######################## # Index is 0, 1, 2 for the different buttons # 0 = Paper Towels # 1 = Soap # 2 = Plumbing # Channel Numbers for Button Presses are: # 1 = Paper Towels # 2 = Soap # 3 = Plumbing cayenneClient . virtualWrite ( index + 1 , self . statsButtonPresses [ index ]) # return -1 for button press return - 1 else : # return 1 for button not pressed return 1 return 0 def incrementStatsRequestSent ( self , index ): # increment stats on request sent for this button index self . statsRequestSent [ index ] += 1 ###################### # Google Sheets Update ###################### #sheet.update_cell(index+2,3,self.statsRequestSent[index]) # ######################## # Cayenne Update ######################## # Index is 0, 1, 2 for the different buttons # 0 = Paper Towels # 1 = Soap # 2 = Plumbing # Channel Numbers for StatsRequestSent are: # 4 = Paper Towels # 5 = Soap # 6 = Plumbing cayenneClient . virtualWrite ( index + 1 + 3 , self . statsRequestSent [ index ]) def incrementStatsRequestCompleted ( self , index ): # increment stats on requests completed for this button index self . statsRequestCompleted [ index ] += 1 ######################## # Cayenne Update ######################## # Index is 0, 1, 2 for the different buttons # 0 = Paper Towels # 1 = Soap # 2 = Plumbing # Channel Numbers for StatsRequestcompleted are: # 7 = Paper Towels # 8 = Soap # 9 = Plumbing cayenneClient . virtualWrite ( index + 1 + 6 , self . statsRequestCompleted [ index ]) class sensorList (): def __init__ ( self ): self . sensorList = {} def dumpSensorStats ( self ): #dump some stats for ids in self . sensorList : print "Sensor:" + ids # print the stats for each button associated with this sensor print "B1 Presses:" + str ( self . sensorList [ ids ] . statsButtonPresses [ 0 ]) print " ReqSent:" + str ( self . sensorList [ ids ] . statsRequestSent [ 0 ]) print " ReqComp:" + str ( self . sensorList [ ids ] . statsRequestCompleted [ 0 ]) print "B2 Presses:" + str ( self . sensorList [ ids ] . statsButtonPresses [ 1 ]) print " ReqSent:" + str ( self . sensorList [ ids ] . statsRequestSent [ 1 ]) print " ReqComp:" + str ( self . sensorList [ ids ] . statsRequestCompleted [ 1 ]) print "B3 Presses:" + str ( self . sensorList [ ids ] . statsButtonPresses [ 2 ]) print " ReqSent:" + str ( self . sensorList [ ids ] . statsRequestSent [ 2 ]) print " ReqComp:" + str ( self . sensorList [ ids ] . statsRequestCompleted [ 2 ]) # send to Cayenne a periodic update on our stats since it doesn't seem to update sometimes for index in xrange ( 0 , 3 ): cayenneClient . virtualWrite ( index + 1 , self . sensorList [ ids ] . statsButtonPresses [ index ]) cayenneClient . virtualWrite ( index + 1 + 3 , self . sensorList [ ids ] . statsRequestSent [ index ]) cayenneClient . virtualWrite ( index + 1 + 6 , self . sensorList [ ids ] . statsRequestCompleted [ index ]) cayenneClient . virtualWrite ( 10 , 0 ) cayenneClient . virtualWrite ( 11 , 0 ) cayenneClient . virtualWrite ( 12 , 0 ) def addSensor ( self , sensorName , humanName ): # add a new sensor into the list self . sensorList [ sensorName ] = sensor () self . sensorList [ sensorName ] . setname ( sensorName , humanName ) def getSensorName ( self , sensorID ): if sensorID in self . sensorList : #print 'Sensor Found' return self . sensorList [ sensorID ] . getname () else : print 'Unknown sensor, adding new sensor' return "unknown" def sendEmail ( self , sensorID , index ): #send an email to facilities request msg = MIMEMultipart () msg [ 'From' ] = sender msg [ 'To' ] = receivers msg [ 'Subject' ] = "Bathroom needs servicing room " + sensorID body = 'There is a ' + self . sensorList [ sensorID ] . getButtonName ( index ) + " issue in the bathroom located in room ID:" + sensorID msg . attach ( MIMEText ( body , 'plain' )) email_text = msg . as_string () try : server = smtplib . SMTP_SSL ( 'smtp.gmail.com' , 465 ) server . ehlo () server . login ( sender , sender_password ) server . sendmail ( sender , receivers , email_text ) server . close () self . sensorList [ sensorID ] . incrementStatsRequestSent ( index ) print "Email has been sent" #update the facilities request state self . sensorList [ sensorID ] . setFacRequestState ( index , "Sent" ) except smtplib . SMTPException : print "Error: unable to send email" def processFacilitiesRequest ( self , sensorID , index , rv , currentState ): # if rv is -1, then a button has been pressed if ( 0 > rv ): # button pressed # checkt the state we are in if ( "Not Started" == currentState ): # first button press, generate an email to facilities for this item print "First Button press, sending facilities request for " + self . sensorList [ sensorID ] . getButtonName ( index ) self . sendEmail ( sensorID , index ) else : # if we have already sent the request to facilities, just ignore the button if ( "Sent" == currentState ): print "Already sent facilities request, ignoring" else : # if we have received a response from facilities, ignore button presses if ( "Received" == currentState ): print "Received facilities request response, ignoring" else : if ( "Completed" == currentState ): print "Request completed, ignoring" else : print "Uknown state" def sensorState ( self , sensorID , monitorState1 , monitorState2 , monitorState3 ): # Analyze the new states for each button for this sensor # check the first button rv1 = self . sensorList [ sensorID ] . checkButtonState ( 0 , monitorState1 ) if ( 0 != rv1 ): # State changed! if ( 0 > rv1 ): outBuf = "State Change: Button 1 Pressed " + sensorID print ( outBuf ) else : outBuf = "State Change: Button 1 Not Pressed " + sensorID print ( outBuf ) # get the current facilities request state for this button requestState = self . sensorList [ sensorID ] . getFacRequestState ( 0 ) # process this button state change self . processFacilitiesRequest ( sensorID , 0 , rv1 , requestState ) # check the second button ( same logic ) rv2 = self . sensorList [ sensorID ] . checkButtonState ( 1 , monitorState2 ) if ( 0 != rv2 ): # State changed! if ( 0 > rv2 ): outBuf = "State Change: Button 2 Pressed " + sensorID print ( outBuf ) else : outBuf = "State Change: Button 2 Not Pressed " + sensorID print ( outBuf ) requestState = self . sensorList [ sensorID ] . getFacRequestState ( 1 ) self . processFacilitiesRequest ( sensorID , 1 , rv2 , requestState ) # check the third button rv3 = self . sensorList [ sensorID ] . checkButtonState ( 2 , monitorState3 ) if ( 0 != rv3 ): # State changed! if ( 0 > rv3 ): outBuf = "State Change: Button 3 Pressed " + sensorID print ( outBuf ) else : outBuf = "State Change: Button 3 Not Pressed " + sensorID print ( outBuf ) requestState = self . sensorList [ sensorID ] . getFacRequestState ( 2 ) self . processFacilitiesRequest ( sensorID , 2 , rv3 , requestState ) def setFacRequestState ( self , sensorID , index , newState , publisher ): # set the request state for this sensor and this button index self . sensorList [ sensorID ] . setFacRequestState ( index , newState ) #if new state is completed, then publish back to the ESP8266 #so he can turn off the flashing LED if ( newState == "Completed" ): publisher . publish ( publishTopic , payload = sensorID + " " + str ( index + 1 )) # set to Not Started state so we can start looking for # button presses again self . sensorList [ sensorID ] . setFacRequestState ( index , "Not Started" ) # increment our completed requests stats for this button index self . sensorList [ sensorID ] . incrementStatsRequestCompleted ( index ) # Reset the button state for this self . sensorList [ sensorID ] . setButtonState ( index , "Not Pressed" ) def setFacRequestID ( self , sensorID , index , requestNo ): # just a wrapper fucntion to set the request ID on the sensor object self . sensorList [ sensorID ] . setFacRequestID ( index , requestNo ) def getFacRequestID ( self , sensorID , index ): # just a wrapper fucntion to get the request ID on the sensor object return self . sensorList [ sensorID ] . getFacRequestID ( index ) ######################## # Functions ######################## # Return state string for button press value def returnState ( state ): actualState = string . atoi ( state ) if ( 0 == actualState ): return "Not Pressed" if ( 1 == actualState ): return "Pressed" else : return "unknown" ######################## # Main ######################## if "__main__" == __name__ : # Set timer sensList = sensorList () pubMan = mqtt . Client ( "pi" ) # Publisher client for sending the completed messages #sensList.addSensor("BR-C-404", "Mens room") def on_publish ( client , userdata , result ): print ( "Data published" ) # The callback for when the client receives a CONNACK response from the server. def on_connect ( client , userdata , flags , rc ): print ( "Connected with result code " + str ( rc )) # Subscribing in on_connect() means that if we lose the connection and # reconnect then subscriptions will be renewed. client . subscribe ( "/facilities" ) # The callback for when a PUBLISH message is received from the server. # This is the message coming from the ESP8266 when a button is pressed def on_message ( client , userdata , msg ): # decode the message ( sensorID , button1State , button2State , button3State ) = string . split ( msg . payload ) # lookup the sensor name to see if we know about this sensor ( room number ) sensorName = sensList . getSensorName ( sensorID ) if ( "unknown" == sensorName ): # sensor not found, so add it into our list of sensors sensList . addSensor ( sensorID , "test" ) # Get the string value for each value of the button presses button1String = returnState ( button1State ) button2String = returnState ( button2State ) button3String = returnState ( button3State ) # Evaluate the sensor state for each button press state sensList . sensorState ( sensorID , button1String , button2String , button3String ) # print some debug print ( sensorName + " [" + sensorID + "] b1=" + button1String + " b2=" + button2String + " b3=" + button3String ) ######################## # Cayenne Callback function ######################## # The callback for when a message is received from Cayenne. def cayenne_on_message ( message ): print ( "message received: " + str ( message )) if ( message . channel == 10 ): print ( "resetting the paper towel state machine" ) # set to Not Started state so we can start looking for # button presses again index = 0 sensorID = "BR-C-404" sensList . setFacRequestState ( sensorID , index , "Completed" , pubMan ) # reset the cayenne pushbutton back to 0 so it will send a '1' again cayenneClient . virtualWrite ( 10 , 0 ) if ( message . channel == 11 ): print ( "resetting the soap dispenser state machine" ) # set to Not Started state so we can start looking for # button presses again index = 1 sensorID = "BR-C-404" sensList . setFacRequestState ( sensorID , index , "Completed" , pubMan ) # reset the cayenne pushbutton back to 0 so it will send a '1' again cayenneClient . virtualWrite ( 11 , 0 ) if ( message . channel == 12 ): print ( "resetting the plumbing state machine" ) # set to Not Started state so we can start looking for # button presses again index = 2 sensorID = "BR-C-404" sensList . setFacRequestState ( sensorID , index , "Completed" , pubMan ) # reset the cayenne pushbutton back to 0 so it will send a '1' again cayenneClient . virtualWrite ( 12 , 0 ) def checkEmail (): # This function checks gmail for new unread messages from the facilities request department # If these messages have the correct subject lines, these are parsed to determine the state # of the facilities request g = Gmail () try : # login to gmail g . login ( sender , sender_password ) except : print 'Could not log into Gmail' return # get messages that are unread and from facilities request # NOTE: sometimes this errors out due to hostname lookup unread = g . inbox () . mail ( unread = True , sender = receivers ) for email in unread : print 'Got an email' email . read () email . fetch () print 'Subject:' + email . subject # look at the subject to see if we care about this one if ( email . subject . find ( "has been assigned" ) != - 1 ): print "Found assigned email response -- new work order" # new work order created # extract the facilities request number from the subject line # To do this: # 1) get second part of string after "Your Facilities Request" # 2) Get first part before the first space ( should be "#83838383" after this ) # 3) Look for "#" character # 4) Get the rest of the digits after "#" character # 5) convert it to an integer requestNoString = ( email . subject . split ( "Your Facilities Request" , 1 )[ 1 ]) . lstrip () requestNoString = requestNoString . split ( " " , 1 )[ 0 ] print "requestNoString" + requestNoString if ( requestNoString [ 0 ] == "#" ): print "Request Number string is:" + requestNoString [ 1 :] requestNo = int ( requestNoString [ 1 :]) else : requestNo = 0 # Get the sensorID from the body of the email # To do this: # 1) Look for the "room ID:" string and get anything after it # 2) Get anything from before the next carriage return / line feed # 3) strip any leading or trailing spaces # 4) get the first 8 characters ( assumes name format of "BR-C-404" ) sensorID = email . body . split ( "room ID:" , 1 )[ 1 ] sensorID = sensorID . split ( " \r

" , 1 )[ 0 ] sensorID = sensorID . lstrip () sensorID = sensorID [ 0 : 8 ] print "Sensor ID from return email is :" + sensorID # Look in the body to determine which button this request pertains to if ( email . body . find ( "Paper Towels" ) != - 1 ): index = 0 print ( "Paper Towels issue" ) if ( email . body . find ( "Soap Dispenser" ) != - 1 ): index = 1 print ( "Soap Dispenser issue" ) if ( email . body . find ( "Plumbing" ) != - 1 ): index = 2 print ( "Plumbing issue" ) # See if we know about this sensor sensorName = sensList . getSensorName ( sensorID ) if ( "unknown" == sensorName ): sensList . addSensor ( sensorID , "test" ) # change the state of the facilities request sensList . setFacRequestState ( sensorID , index , "Received" , pubMan ) sensList . setFacRequestID ( sensorID , index , requestNo ) else : if ( email . subject . find ( "is complete" ) != - 1 ): print "Found complete email response -- completed work order" # work order has been completed # make sure the request number matches sensorID = email . body . split ( "room ID:" , 1 )[ 1 ] sensorID = sensorID . split ( " \r

" , 1 )[ 0 ] sensorID = sensorID . lstrip () sensorID = sensorID [ 0 : 8 ] print "Sensor ID from return email is :" + sensorID if ( email . body . find ( "Paper Towels" ) != - 1 ): index = 0 print ( "Paper Towels issue" ) if ( email . body . find ( "Soap Dispenser" ) != - 1 ): index = 1 print ( "Soap Dispenser issue" ) if ( email . body . find ( "Plumbing" ) != - 1 ): index = 2 print ( "Plumbing issue" ) sensorName = sensList . getSensorName ( sensorID ) if ( "unknown" == sensorName ): sensList . addSensor ( sensorID , "test" ) # change the state of the facilities request sensList . setFacRequestState ( sensorID , index , "Completed" , pubMan ) print "Completed work order #" + str ( sensList . getFacRequestID ( sensorID , index )) else : print "Uknown email contents" # logout of gmail each time we check for new emails g . logout () # ************************ # Main Function Here # ************************ cayenneClient . on_message = cayenne_on_message cayenneClient . begin ( MQTT_USERNAME , MQTT_PASSWORD , MQTT_CLIENT_ID ) # setup the mqtt client that listens for messages from the MQTT broker client = mqtt . Client () # setup callbacks for the client client . on_connect = on_connect client . on_message = on_message # setup callbacks for the publisher ( messages to the ESP8266 ) pubMan . on_publish = on_publish # connect to the MQTT broker as a publisher pubMan . connect ( localBroker , localPort , localTimeOut ) # connect to the MQTT broker as a client client . connect ( localBroker , localPort , localTimeOut ) loop_flag = 1 counter = 0 cayenneReset = 0 # start looking for messages from the MQTT broker client . loop_start () pubMan . loop_start () # begin loop while loop_flag == 1 : cayenneClient . loop () time . sleep ( 1 ) print "In the loop...." # Check for new facilities request emails in gmail checkEmail () counter += 1 if ( counter > 5 ): print "Dumping Stats" counter = 0 sensList . dumpSensorStats () #time.sleep(3) # use the line below to publish every so often to the ESP8266 ( used for testing) #pubMan.publish(publishTopic,payload="BR-C-404"+" 1") if ( cayenneReset == 0 ): for channel in range ( 0 , 3 ): print "Initializing index" + str ( channel ) cayenneClient . virtualWrite ( channel + 1 , 0 ) cayenneClient . virtualWrite ( channel + 1 + 3 , 0 ) cayenneClient . virtualWrite ( channel + 1 + 6 , 0 ) cayenneReset = 1 cayenneClient . virtualWrite ( 10 , 0 ) cayenneClient . virtualWrite ( 11 , 0 ) cayenneClient . virtualWrite ( 12 , 0 ) # disconnect the client and stop the loop client . disconnect () client . loop_stop () # disconnect the publisher and stop the loop pubMan . disconnect () pubMan . loop_stop () quit ()