Amazon ALEXA Lists Exercise – Put’em on Paper…

4. Solution Components

Let’s look at each of the components in detail. You may want to create the items in the same order as we did here, particularly with the ALEXA stuff.

4.1 Amazon AWS Lambda glue code

To understand the code, you’d have to make yourself familiar with the following resources:

And yes, we’re using plain old python spaghetti code (POPSCTM) – no Flask-Ask or similar. After all, this is supposed to be an excercise…

4.1.1 Source Code

from urllib.request import *
from urllib.error import HTTPError
from urllib.parse import urlencode, quote_plus, quote
import json, sys, ssl, socket


def lambda_handler(event, context):
    # ******************************
    # *** Do we have a valid event?
    # ******************************
    if not "request" in event:
        # wrong event passed or not a dict.
        return { 'version': '1.0', 'response': { 'outputSpeech': { 'type': 'PlainText', 'text': "Sorry, I didn't get your request." } }, "shouldEndSession": True }
    if event["request"].get("type", '')!='IntentRequest' and event["request"].get("type", '')!='LaunchRequest':
        # we don't have an intent here
        return { 'version': '1.0', 'response': { 'outputSpeech': { 'type': 'PlainText', 'text': "Sorry, I didn't get your request." } }, "shouldEndSession": True }
    
    # ******************************
    # *** Do we have a consent-token? If not->user must approve in APP
    # ******************************
    if not "context" in event:
        # This should not happen
        return { 'version': '1.0', 'response': { 'outputSpeech': { 'type': 'PlainText', 'text': "Sorry, I didn't get your request." } }, "shouldEndSession": True }
    apiEndPoint    = event["context"].get("System", {}).get("apiEndpoint", "")
    apiAccessToken = event["context"].get("System", {}).get("apiAccessToken", "")
    if apiEndPoint=="" or apiAccessToken=="":
        # User must grant access to this skill in their Alexa App
        return { 'version': '1.0', 'response': { 'outputSpeech': { 'type': 'PlainText', 'text': "Please open your ALEXA app and grant both read and write access on your lists to this skill." } }, "shouldEndSession": True }
    
    # Determine which one of the two lists the user wanted to print, default=shopping list
    liste = "shopping list"
    # this is intended for the German voice interface, obviously a Q&D hack; you'd need to work with IDs to make it language independent
    if event["request"].get("intent",{}).get("slots",{}).get("listName",{}).get("value",'')=='aufgabenliste' or event["request"].get("intent",{}).get("slots",{}).get("listName",{}).get("value",'')=='aufgabenzettel':
        # ok, it's the TODO list
        liste = "todo list"

    # ******************************
    # **** TRY TO LOAD ALEXA LISTS
    # ******************************
    try:
        req  = Request(apiEndPoint+"/v2/householdlists/", data=None, headers={"Authorization": "Bearer "+apiAccessToken, "Content-Type": "json"}, method="GET")
        resp = urlopen(req)
        alexaListsJson = resp.read()        
    except:
        # nah, alexa has a bad day today.
        return { 'version': '1.0', 'response': { 'outputSpeech': { 'type': 'PlainText', 'text': "Sorry, I could not access your lists." } }, "shouldEndSession": True }

    # ******************************
    # **** TRY TO DECODE ALEXA LISTS
    # ******************************
    try:
        alexaListsDict = json.loads(alexaListsJson)
    except:
        # nah, alexa has a bad day today.
        return { 'version': '1.0', 'response': { 'outputSpeech': { 'type': 'PlainText', 'text': "Sorry, I received an unexpected list format." } }, "shouldEndSession": True }
    
    # ******************************
    # *** Fetch list item arguments
    # ******************************
    for currentList in alexaListsDict["lists"]:
        listState = currentList["state"]
        listName  = currentList["name"]
        listId    = currentList["listId"]
        listUrl   = ""
        # get 'active' list URL
        for listUrls in currentList["statusMap"]:
            if listUrls["status"]=="active":
                listUrl = listUrls.get("href", "")
                break
        if liste=="shopping list" and listName == "Alexa shopping list":
            break;
        if liste=="todo list" and listName == "Alexa to-do list":
            break;
    if listUrl=="":
		    # Shoot, we don't have a URL for the selected list
        return { 'version': '1.0', 'response': { 'outputSpeech': { 'type': 'PlainText', 'text': "Sorry, I received an unexpected list format." } }, "shouldEndSession": True }
    # ok, we now have the correct URL for the particular list
    listUrl = apiEndPoint + listUrl
     
    # ******************************
    # *** Fetch list items arguments
    # ******************************
    try:
        req  = Request(listUrl, data=None, headers={"Authorization": "Bearer "+apiAccessToken, "Content-Type": "json"}, method="GET")
        resp = urlopen(req)
        alexaListItemsJson = resp.read()        
    except:
        # We could not retrieve the list's items
        return { 'version': '1.0', 'response': { 'outputSpeech': { 'type': 'PlainText', 'text': "Sorry, I could not retrieve your "+liste+"." } }, "shouldEndSession": True }
    try:
        alexaListItemsDict = json.loads(alexaListItemsJson)
    except:
        # item payload has an unexpected format
        return { 'version': '1.0', 'response': { 'outputSpeech': { 'type': 'PlainText', 'text': "Sorry, your "+liste+" has an unexpected format." } }, "shouldEndSession": True }
    alexaListItemsList = []
    for listItem in alexaListItemsDict["items"]:
        if listItem.get("value",'')=="": continue
        alexaListItemsList.append(listItem.get("value",''))
    if len(alexaListItemsList) == 0:
        return { 'version': '1.0', 'response': { 'outputSpeech': { 'type': 'PlainText', 'text': "Sorry, your "+liste+" is empty." } }, "shouldEndSession": True }
        
    # ******************************
    # *** Print the list items
    # ******************************
    secret = "1234567890__REPLACE__WITH__YOUR__SECRET__1234567890" # obviously, URL-compatible characters only. Or url-encode / quote_plus() it!
    # ignore self-signed certs
    ctx = ssl.create_default_context()
    ctx.check_hostname = False
    ctx.verify_mode = ssl.CERT_NONE    
    # Encode stuff
    listeEncoded     = quote_plus(liste)
    listItemsEncoded = quote_plus(json.dumps(alexaListItemsList))
    
    # start the printer...
    urlString = "https://!!YOURHOSTHERE!!/edge_script_printer.php?listName="+ listeEncoded +"&listItems="+ listItemsEncoded +"&secret="+secret
    try:
        req  = Request(urlString, data=None, headers={"Content-Type": "json"}, method="GET")
        resp = urlopen(req, timeout=5, context=ctx)
    except HTTPError as error:
        # received an error from EDGE server
        return { 'version': '1.0', 'response': { 'outputSpeech': { 'type': 'PlainText', 'text': "Sorry, service error at printer service." } }, "shouldEndSession": True }
    except:
        # could not connect to EDGE server
        return { 'version': '1.0', 'response': { 'outputSpeech': { 'type': 'PlainText', 'text': "Sorry, connection error to printer service." } }, "shouldEndSession": True }

    # ******************************
    # *** Clear the respective list
    # ******************************
    if event["request"].get("intent",{}).get("slots",{}).get("deleteFlag",{}).get("value",'')!='':
        # do not delete the list
        return { 'version': '1.0', 'response': { 'outputSpeech': { 'type': 'PlainText', 'text': "I printed out your "+liste+"." } }, "shouldEndSession": True }        
        
    error=False
    for listItem in alexaListItemsDict["items"]:
        itemUrl = apiEndPoint + listItem.get("href", "")
        try:
            req  = Request(itemUrl, data=None, headers={"Authorization": "Bearer "+apiAccessToken, "Content-Type": "application/json"}, method="DELETE")
            resp = urlopen(req)
        except:
            print (sys.exc_info())
            print (listItem)
            error=True
    if error==True:
        # at least one item could not be deleted
        return { 'version': '1.0', 'response': { 'outputSpeech': { 'type': 'PlainText', 'text': "I printed out your "+liste+", but there was an error clearing it." } }, "shouldEndSession": True }        
        
    # all printed, list emptied!
    return { 'version': '1.0', 'response': { 'outputSpeech': { 'type': 'PlainText', 'text': "I printed out and cleared your "+liste+"." } }, "shouldEndSession": True }

Nothing fancy in there, but do note that you’re required to change two things:

  1. line 103: generate your own secret here.
  2. line 113: replace with your own DynDNS host name.

4.1.2 Creating the Lambda function

In order to create the lambda function:

  • log into your Amazon developer console,
  • navigate to AWS,
  • find the „Lambda“ button,
  • create a new lambda function from scratch named printer-glue-code with lambda_basic_execution role running on python 3.6; it can remain named lambda_function.lambda_handler .
  • Then, paste the code from above and save the function.
  • Also, you should set the timeout to 20 seconds under „Basic Settings“.

Now, add ALEXA as allowed calling source:

  • Switch to the tab „Triggers“ (current one is called „configuration“) and press „Add Trigger“
  • In the grey box, select „ALEXA Skills Kit“ and press „Submit“
  • Never mind the error, just close the box by pressing „cancel“.
  • Upon refreshing the triggers, you should see the ALEXA Skills Kit trigger

Lastly, take note of the ARN in the upper right corner:

You can press the „Test“ button and use the default event. It should come up with a green success message.

5 Kommentare zu “Amazon ALEXA Lists Exercise – Put’em on Paper…”

1.   Kommentar von Tom
Erstellt am 09. Dezember 2018 um 22:40 Uhr.

Hi,
great post.
Could please tell which thermal printer you have used?
The amazon link isnt‘ working anymore.
Thanks

2.   Kommentar von McSeven
Erstellt am 10. Dezember 2018 um 20:27 Uhr.

hi, Thanks. The one I used isn’t available on Amazon anymore, but any USB receipt printer should work… Just look for what mike24’s ESC/POS library is supporting and buy one of those models… Best, Christoph

3.   Kommentar von Tom
Erstellt am 11. Dezember 2018 um 20:53 Uhr.

Thanks for the reply. This product list on github helps a lot.
One further question: Could you really delete the original shopping list with the skill? In your video you use the original „Einkaufsliste“ so you could also delete this one per API? In your code for the lambda it is „shopping list“ and not the original code snipped?

Danke und Gruß
Tom

4.   Kommentar von McSeven
Erstellt am 12. Dezember 2018 um 20:23 Uhr.

hi, well, the Einkaufsliste is used by the ALEXA language model. it translates into „shopping list“ in the skill. Therefore the difference… Cheers.

5.   Kommentar von BennyBoom
Erstellt am 07. Oktober 2020 um 00:10 Uhr.

Hello!

Nice work ! I try to integrate it to home assistant .
Your video link is dead could you update it please?

regards

Ben

Einen Kommentar hinterlassen