Thursday, December 1, 2016

Amazon Dot: Now for the code on the Raspberry Pi

Last post <link> I stepped through creating the voice model and the Lambda function in Amazon Web Services (AWS) that got my request to manipulate the light on my East patio light out to Amazon's mqtt server for this service that they call, AWSIot (Amazon Web Services Internet of Things). Yes, it's a big name for something like this, but how else are you going to sell it? Gotta have a lot of marketing hype to impress the Harvard School of Business Managment.

At any rate, it's a good implementation of mqtt that reacts pretty quickly and will send the mqtt message containing the data I need to control the light. When you do this, you'll need something on the Pi to receive the message and do something with something real.

Unfortunately, I can't do that part for you.

I can show you how to receive the message and take it apart, but the actual twiddling of bits, or however you control your devices is up to you. I use a mix of XBees and web based devices and they certainly won't fit in with what you do. So, I'm going to use a couple of variables and just change them. You can take that and run with it since we all have variables.

Let's do this with code I put together special for just this purpose. In the code I initialize the variable to something so I can tell they changed, get and respond to the messages from Amazon that were initiated by voice command to the Dot, then change the values based on that. I'll build on the example I provided previously to get the temperature at my house, so if you don't remember what I did there (I wouldn't) go back a glance at it for a minute <link>; I'll wait.

Here's the python code:

#!/usr/bin/python
import os
import sys
import time
import paho.mqtt.client as mqtt
import ssl
import json
import pprint
from houseutils import timer, checkTimer

pp = pprint.PrettyPrinter(indent=2)

def on_awsConnect(client, userdata, flags, rc):
    print("mqtt connection to AWSIoT returned result: " + str(rc) )
    # Subscribing in on_connect() means that if we lose the connection and
    # reconnect then subscriptions will be renewed. You still have to do the
    # reconnect in code because that doesn't happen automatically
    client.subscribe ([(awsShadowDelta , 1 ),
                      (awsShadowDocuments, 1)])
                      
def on_awsMessage(client, userdata, msg):
    global eastPatioLight
    
    # If you want to see the shadow documents to observe what is going on
    # uncomment the prints below.
    #print "TOPIC = ",
    #print msg.topic
    #print "PAYLOAD = ",
    payload = {}
    payload = json.loads(msg.payload)
    #pp.pprint (payload)
    #print ""
    
    # The 'delta' message is the difference between the 'desired' entry and
    # the 'reported' entry. It's the way of telling me what needs to be changed
    # because I told alexa to do something. What I tell alexa to do goes into
    # the desired entry and the delta is then created and published. Note that
    # the delta is not removed, it has to be done specifically, hence the 
    # code further down.
    
    if msg.topic == awsShadowDelta:
        print "got a delta"
        pp.pprint(payload["state"])
        for item in payload["state"].keys():
            if item == "eastPatioLight":
                command = str(payload["state"][item])
                print "got command for east patio light: ", command
                # This is where you would actually do something to
                # change the state of a device.
                eastPatioLight = command
            else:
                print("I don't know about item ", item)
                
    # Right under here I get the entire document and compare the 'desire' part
    # to the corresponding items in the 'reported' part. When I find something in
    # the desire that is the same as something in the reported, I remove the entry
    # from the desire. If you get rid of all the entries in desire, the entire
    # desire part of the document is removed and just disappears until it's 
    # needed later.
    
    # The reason for this is because when the desire stays around and you walk
    # over and change something by hand, AWS will generate a delta because the
    # reported is suddenly different from the desired. That means you open the
    # garage door by hand, aws senses that the desire is closed, sends a delta
    # and closes the garage door on you.
    
    # Fortunately, I discovered this with a light, not a garage door.
        
    elif msg.topic == awsShadowDocuments:
        #print "got full thing"
        
        # AWSIoT sends the 'reported' state back to you so you can
        # do something with it if you need to. I don't need to deal 
        # with it ... yet.
        if "desired" not in payload["current"]["state"]:
            #print "'desired' not there"
            return
        desired = payload["current"]["state"]["desired"]
        reported = payload["current"]["state"]["reported"]
        #pp.pprint (reported)
        
        # This is probably a left over 'desired' state, which means
        # you've already changed something, but the desire is still
        # left hanging around, so compare it with the reported state
        # and if it has been satisfied, remove the desire from AWSIoT
        pp.pprint (desired)
        fixit = False
        fixitString = "{ \"state\" : { \"desired\": {"
        for item in desired.keys():
            # when updating this, you'll often encounter
            # items that aren't fully implemented yet
            # (because you're still working on them)
            # this not reported it's just to keep from dying
            if not reported.get(item):
                print ("found odd item " + item)
                break
            if desired[item] == reported[item]:
                fixit = True
                print "found left over desire at", item
                # to get rid of a desire, set it to null
                fixitString += "\"" + item + "\": null,"
        if not fixit:
            return
        fixitString = fixitString[:-1] #remove the trailing comma JSON doesn't like it
        fixitString +="} } }"
        print "sending:", fixitString
        err = awsMqtt.publish("$aws/things/house/shadow/update",fixitString)
        if err[0] != 0:
            print("got error {} on publish".format(err[0]))
    else:
        print "I don't have a clue how I got here"

# This keeps the Shadow updated with the latest state of the devices
# If you go over and push a button to turn on a light, you want the shadow
# to know so everything can work properly.        
def updateIotShadow():
    global temperature 
    global barometer
    global eastPatioLight
    print "Variable states: ", temperature, barometer, eastPatioLight
    # Create report in JSON format; this should be an object, etc.
    # but for now, this will do.
    report = "{ \"state\" : { \"reported\": {"
    report += "\"temp\": \"%s\", " %(int(round(temperature)))
    report += "\"barometer\": \"%s\", " %(int(round(barometer)))
    report += "\"eastPatioLight\": \"%s\", " %(eastPatioLight.lower())
    report += "\"lastEntry\": \"isHere\" " #This entry is only to make it easier on me
    report += "} } }" 
    # Print something to show it's alive
    print "On Tick: ", report
    err = awsMqtt.publish(awsShadowUpdate,report)
    if err[0] != 0:
        print("got error {} on publish".format(err[0]))

# These are the three items we'll deal with in this example
# They represent real devices that measure or change something
# that have been implemented somewhere.
temperature = 79.1
barometer = 1234.5
eastPatioLight = "on"

def updateWeather():
    global temperature
    global barometer
    # if you had a real device, this is where you read its status
    # and update the variables so the values would be reported back
    # to AWSIoT
    temperature = temperature;
    barometer = barometer;

# Actually starts here      
if __name__ == "__main__":
    # these are the two aws subscriptions you need to operate with
    # the 'delta' is for changes that need to be taken care of
    # and the 'documents' is where the various states and such
    # are kept
    awsShadowDelta = "$aws/things/house/shadow/update/delta"
    awsShadowDocuments = "$aws/things/house/shadow/update/documents"
    # this is the mqtt resource that you respond to when you change 
    # something.
    awsShadowUpdate = "$aws/things/house/shadow/update"
    # create an aws mqtt client and set up the connect handlers
    awsMqtt = mqtt.Client()
    awsMqtt.on_connect = on_awsConnect
    awsMqtt.on_message = on_awsMessage
    # certificates, host and port to use
    awsHost = "data.iot.us-east-1.amazonaws.com"
    awsPort = 8883
    caPath = "/home/pi/src/house/keys/aws-iot-rootCA.crt"
    certPath = "/home/pi/src/house/keys/cert.pem"
    keyPath = "/home/pi/src/house/keys/privkey.pem"
    # now set up encryption and connect
    awsMqtt.tls_set(caPath, certfile=certPath, keyfile=keyPath, cert_reqs=ssl.CERT_REQUIRED, tls_version=ssl.PROTOCOL_TLSv1_2, ciphers=None)
    awsMqtt.connect(awsHost, awsPort, keepalive=60)
    print ("did the connect to AWSIoT")
    
    # Now that everything is ready start the mqtt loop
    awsMqtt.loop_start()
    print ("mqtt loop started")

    # this timer fires every so often to update the
    # Amazon alexa device shadow; check 'seconds' below
    shadowUpdateTimer = timer(updateIotShadow, seconds=10)
    weatherUpdate = timer(updateWeather, seconds=3)
    print("Alexa Handling started")

    # The main loop
    while True:
        # Wait a bit
        checkTimer.tick()
        time.sleep(0.5)

Now, as usual, I'm going to step you through it so you have a chance of understanding what is going on. You'll have to adapt this to your devices and it's best if you understand it instead of copy and paste.

So, let's go to the bottom of the code where it says "Actually starts here," and see how to set things up. I start off with creating a couple of variables that hold the subscriptions that will be used. These will certainly be different from yours, so put your own device name in here where mine says 'house'. Then I create another one for the mqtt topic that we will be responding to. Same thing, change it to match yours.

Take special note here that this is a regular old mqtt client implementation, I DID NOT use their special library because of problems I encountered with it.

The next hunk of code we've seen and gone through the explanation already when we set up to send sensor data up to AWSIot, so I'm not going to repeat myself here <link>. Down at the line where I talk about timers, you'll see two of them; one is for updating the Shadow and the other is for updating the variables that I'm using to simulate real sensor devices. That code is so you can slip in whatever code you need to get the values and save them for updating AWSIoT. I do a lot of stuff with timers since it allows other processes to get time to run. That way I leverage all the power I can out of a little Raspberry Pi.

Below that, I just sleep for a half-second and update my timer and go back to sleep. Every 3 seconds I update the sensor variables and every 10 seconds I update AWSIot. Those numbers seem to work reasonably well for a demo, but you may want to play with them in your particular situation.

So, now that I'm hung up in a loop waiting for events (timers and such), I'll get a call to on_awsConnect() that is the result of the awsMqtt.connect() call I did a little up the page. The handler awsConnect() is up at the top of the file and that's where we'll subscribe to the two mqtt topics we need to listen to:

client.subscribe ([(awsShadowDelta , 1 ),
                      (awsShadowDocuments, 1)])

The Delta one is where commands come from and the Documents is where you get a report on the actual contents of the Shadow; we'll use both of them in this. 

Once we have subscribed, the timer will expire for updating the Shadow and we'll compose a JSON message to AWSIot that holds the state of the items we're reporting. This is done in the routine updateIotShadow() and covered in the other post I mentioned (twice now) and is sent to the 'update' topic:

 err = awsMqtt.publish(awsShadowUpdate,report)
 if err[0] != 0:
     print("got error {} on publish".format(err[0]))

And, the shadow now contains the latest information for Alexa to grab and report back to you through the Dot. Notice that I added the patio light in there.

Now we get to the good part, tell the Dot to turn on the east patio light and the voice service will compose an intent, and send it to the Lambda function which will format a 'desire' into the Shadow document. AWSIot will look at this desire and notice that it's different from what the Shadow currently is and create a 'delta'. Remember that all this is just JSON text inside the Shadow document, there's nothing magical going on.

The 'delta' portion of the JSON is sent to the mqtt topic I stuffed in the variable awsShadowDelta and the Pi code will be called at on_awsMessage() and handed the message. The code in the message handler is totally new, so let's step through it a bit closer.

    # If you want to see the shadow documents to observe what is going on
    # uncomment the prints below.
    #print "TOPIC = ",
    #print msg.topic
    #print "PAYLOAD = ",
    payload = {}
    payload = json.loads(msg.payload)
    #pp.pprint (payload)
    #print ""

Like it says, there's the opportunity to get further debugging as you need it here, but all this basically does is get the contents of the delta into a variable called 'payload'.

    if msg.topic == awsShadowDelta:
        print "got a delta"
        pp.pprint(payload["state"])
        for item in payload["state"].keys():
            if item == "eastPatioLight":
                command = str(payload["state"][item])
                print "got command for east patio light: ", command
                # This is where you would actually do something to
                # change the state of a device.
                eastPatioLight = command
            else:
                print("I don't know about item ", item)

I look at the mqtt topic and see if it's a delta message, print it so you can see what you got and then step through each (there can be more than one) item and change that came in. I index into payload and grab the command for eastPatioLight and set the variable to whatever came in. Seems simple right? Frankly, this was a real pain to figure out, but I finally got there.

That's it for handling the delta and turning on the patio light, but there's a ton of code just below that handles cleaning up after the command has been processed. What's going on is that the delta message keeps coming until it has been removed. This makes perfect sense when you consider that something may happen to the original message and it may need to be resent to accomplish whatever is needed. However, it does need to be taken care of.

What I do here is get the delta from a different mqtt subscription I called awsShadowDocuments which has all three sections in it. It has the desire, reported and delta all in one neat package. So, I just get the desired entry, look for it in the reported JSON and remove it if it's already been reported back in the correct state.

    elif msg.topic == awsShadowDocuments:
        #print "got full thing"
        
        # AWSIoT sends the 'reported' state back to you so you can
        # do something with it if you need to. With the exception of
        # this code, I don't need to deal with it ... yet.
        if "desired" not in payload["current"]["state"]:
            #print "'desired' not there"
            return
        desired = payload["current"]["state"]["desired"]
        reported = payload["current"]["state"]["reported"]
        #pp.pprint (reported)
        
        # This is probably a left over 'desired' state, which means
        # you've already changed something, but the desire is still
        # left hanging around, so compare it with the reported state
        # and if it has been satisfied, remove the desire from AWSIoT
        pp.pprint (desired)
        fixit = False
        fixitString = "{ \"state\" : { \"desired\": {"
        for item in desired.keys():
            # when updating this, you'll often encounter
            # items that aren't fully implemented yet
            # (because you're still working on them)
            # this not reported it's just to keep from dying
            if not reported.get(item):
                print ("found odd item " + item)
                break
            if desired[item] == reported[item]:
                fixit = True
                print "found left over desire at", item
                # to get rid of a desire, set it to null
                fixitString += "\"" + item + "\": null,"
        if not fixit:
            return
        fixitString = fixitString[:-1] #remove the trailing comma JSON doesn't like it
        fixitString +="} } }"
        print "sending:", fixitString
        err = awsMqtt.publish("$aws/things/house/shadow/update",fixitString)
        if err[0] != 0:
            print("got error {} on publish".format(err[0]))


Pay special attention to the above where I iterated through the desire JSON because there may be more than one thing in there; especially if you've been testing various items. And, of course, there's the usual catch all else statement:

    else:
        print "I don't have a clue how I got here"

And, that's it. We've covered the entire code that runs on the Pi to catch requests sent by the Lambda function created in the last post. Cool.

Also, we've actually stepped through a full build of the voice system. You can now actually control something in your house using voice; way better than a 'Clapper' (if you don't know, google it).

This is the last post where I'll work through the various topics, I'm going to put together an Errata post where I'll list some of the gotchas that can creep up and get you, but that may be a while. Also, understand something, you can't just say, "Turn on East patio light," it doesn't work that way. Using the code and methods I presented you have two methods of controlling things. For example:

Alexa (wait for it to hear you and wake up)
ask (or tell) desert home turn on east patio light
(it will respond back whatever you put in the Lambda function)

or

Alexa (wait for it to hear you and wake up)
Desert home (wait for it to respond with text from your Lambda function)
east patio light on

The first will get you there in one sentence, the other takes two, but is more fun. Due to the way I coded it, both will keep the session open for 10 seconds so you can do another command. I generally check the temperature and something else in one session, then tell it to "stop". Yes, I have code in there to handle the "stop" command.

Yes, you DO have to say both Alexa and your thing name (desert home in my case); this isn't Star Trek yet. However, once you get in session you can say a command over and over as long as you don't wait 10 seconds and the automatic session tear down happens. So:

Alexa
Desert home (wait for response)
East patio light on (wait for response)
What is the temperature? (wait)
What is the windspeed? (wait)
Turn off outside lights (wait)
etc.

The automatic session tear down is 10 seconds and can't be changed. Otherwise, someone out there would set it for hours eating up Amazon resources.

Another thing, remember back a bunch of postings ago where I told you that they change the interface often and without warning? Well during the period of putting together these posts they implemented an entirely new interface for AWSIoT and I had to prowl around it for about thirty minutes to get any information about what was going on with my implementation. You'll probably have to prowl a bit also.

Like I said, there hundreds of them spread around the world and only one of me out here in the desert.

Sigh.

Yet another post (I forgot something) <link>

No comments:

Post a Comment