Alexa Connected Devices   >    Development Resources    >    Smart Home Skill Tutorial    >    Step 2: Add an AWS Lambda Function

Step 2: Add an AWS Lambda Function

You’ll notice a required field of “Default endpoint”, which you will create now. The backend code for your smart home skill is hosted as a Lambda function on AWS, so you will need an AWS account to complete this section. If you do not have an AWS account, you can create one here. AWS Lambda is a service that lets you run code in the cloud without managing servers. Alexa sends your skill requests and your code inspects the request, takes any necessary actions such as communicating with the device cloud for that customer, and then sends back a response.

console nav bar screenshot
Note:

This tutorial builds resources in the AWS region of “N. Virginia” (us-east-1). The region is displayed in the upper right corner as shown above. Typically, you will build resources in a region closest to the majority of your users. For this tutorial, please build your resource(s) in the “N. Virginia” region.

2.1 Create a policy for the Lambda function

1. Navigate to the IAM Management Console policies at https://console.aws.amazon.com/iam/home?region=us-east-1#/policies.

2. Click the Create policy button.

3. Select the JSON tab and then copy & paste the following policy into the text area:

Copied to clipboard
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "dynamodb:UpdateItem",
                "logs:CreateLogGroup",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}

4. Click the Review policy button.

5. In the Review policy section, set the Name of the policy to “smart_home_skill”. You can leave the Description blank.

6. Click the Create policy button to create the policy.

2.2 Create a Lambda execution role

1. Navigate to the IAM Management Console roles at https://us-east-1.console.aws.amazon.com/iam/home?region=us-east-1#/roles.

2. Click the Create role button.

3. On the Create role page, select “AWS Service” for the type of trusted entity and then select Lambda from the list of use cases.

4. Click the Next: Permissions button.

5. Under Attach permissions policies, filter and find the previously created “smart_home_skill“ policy and select its check box.

6. Click the Next: Tags button. You may leave the tags blank.

7. Click the Next: Review button.

8. In the Review section, set the Role name to “lambda_smart_home_skill”

9. Click Create role to create the execution role.

2.3 Create the Lambda function

1. Navigate to the AWS Lambda Console at https://console.aws.amazon.com/lambda/home?region=us-east-1#/functions

2. Click Create function button

create function screenshot

3. Select Author from scratch, and enter the following information:

author from scratch screenshot

4. Under *Basic Information,* enter the following information:

  • Function Name: Provide a name for your Lambda function. For example, my-smart-home-skill
  • Runtime: To use the sample code provided in this topic, choose Python 3.8 or alternatively, select the runtime for the code you plan to write. Supported languages are: Node.js, Java, Python, C#, or Go.

5. Role: Under Permissions, click the “Choose or create an execution role” and select Use an existing role as the Execution role and choose the “lambda_smart_home_skill” role you created previously.

6. Click Create function. Your function should be created and you will move to Configuration.

configuration screenshot

7. On the Configuration designer tab, under click Add Trigger

add trigger screenshot

8. Under Trigger configuration, type alexa to filter out the triggers, and then choose Alexa Smart Home

alexa smart home trigger screenshot

9. Application ID: Paste the Skill ID we copied while creating the Smart Home Skill in the Amazon Developer Console into the Application ID box.

application ID screenshot

10. Leave Enable trigger checked. This enables the Amazon Alexa service to call your Lambda function. If you don't enable it when you create, you will not be able to enable it in the console later.

11. Click Add.

enable trigger screenshot

12. Under ConfigurationDesigner, click on the name of your Lambda function to display the function code.

13. In the Function code section make sure Edit code inline is selected. Leave the Runtime and Handler set to their defaults.

function code screenshot

14. Paste in the following code, completely replacing the code in lambda_function.py

Copied to clipboard
# -*- coding: utf-8 -*-

# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Amazon Software License (the "License"). You may not use this file except in
# compliance with the License. A copy of the License is located at
#
#    http://aws.amazon.com/asl/
#
# or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific
# language governing permissions and limitations under the License.

import boto3
import json
import random
import uuid
import time

aws_dynamodb = boto3.client('dynamodb')

def lambda_handler(request, context):

    # Dump the request for logging - check the CloudWatch logs
    print('lambda_handler request  -----')
    print(json.dumps(request))

    if context is not None:
        print('lambda_handler context  -----')
        print(context)

    # Validate we have an Alexa directive
    if 'directive' not in request:
        aer = AlexaResponse(
            name='ErrorResponse',
            payload={'type': 'INVALID_DIRECTIVE',
                     'message': 'Missing key: directive, Is the request a valid Alexa Directive?'})
        return send_response(aer.get())

    # Check the payload version
    payload_version = request['directive']['header']['payloadVersion']
    if payload_version != '3':
        aer = AlexaResponse(
            name='ErrorResponse',
            payload={'type': 'INTERNAL_ERROR',
                     'message': 'This skill only supports Smart Home API version 3'})
        return send_response(aer.get())

    # Crack open the request and see what is being requested
    name = request['directive']['header']['name']
    namespace = request['directive']['header']['namespace']

    # Handle the incoming request from Alexa based on the namespace

    if namespace == 'Alexa.Authorization':
        if name == 'AcceptGrant':
            # Note: This sample accepts any grant request
            # In your implementation you would use the code and token to get and store access tokens
            grant_code = request['directive']['payload']['grant']['code']
            grantee_token = request['directive']['payload']['grantee']['token']
            aar = AlexaResponse(namespace='Alexa.Authorization', name='AcceptGrant.Response')
            return send_response(aar.get())

    if namespace == 'Alexa.Discovery':
        if name == 'Discover':
            adr = AlexaResponse(namespace='Alexa.Discovery', name='Discover.Response')
            capability_alexa = adr.create_payload_endpoint_capability()
            capability_alexa_powercontroller = adr.create_payload_endpoint_capability(
                interface='Alexa.PowerController',
                supported=[{'name': 'powerState'}])
            adr.add_payload_endpoint(
                friendly_name='Sample Switch',
                endpoint_id='sample-switch-01',
                capabilities=[capability_alexa, capability_alexa_powercontroller])
            return send_response(adr.get())

    if namespace == 'Alexa.PowerController':
        # Note: This sample always returns a success response for either a request to TurnOff or TurnOn
        endpoint_id = request['directive']['endpoint']['endpointId']
        power_state_value = 'OFF' if name == 'TurnOff' else 'ON'
        correlation_token = request['directive']['header']['correlationToken']

        # Check for an error when setting the state
        state_set = set_device_state(endpoint_id=endpoint_id, state='powerState', value=power_state_value)
        if not state_set:
            return AlexaResponse(
                name='ErrorResponse',
                payload={'type': 'ENDPOINT_UNREACHABLE', 'message': 'Unable to reach endpoint database.'}).get()

        apcr = AlexaResponse(correlation_token=correlation_token)
        apcr.add_context_property(namespace='Alexa.PowerController', name='powerState', value=power_state_value)
        return send_response(apcr.get())

def send_response(response):
    print('lambda_handler response -----')
    print(json.dumps(response))
    return response

def set_device_state(endpoint_id, state, value):
    attribute_key = state + 'Value'
    response = aws_dynamodb.update_item(
        TableName='SampleSmartHome',
        Key={'ItemId': {'S': endpoint_id}},
        AttributeUpdates={attribute_key: {'Action': 'PUT', 'Value': {'S': value}}})
    print(response)
    if response['ResponseMetadata']['HTTPStatusCode'] == 200:
        return True
    else:
        return False

def get_utc_timestamp(seconds=None):
    return time.strftime('%Y-%m-%dT%H:%M:%S.00Z', time.gmtime(seconds))

class AlexaResponse:

    def __init__(self, **kwargs):

        self.context_properties = []
        self.payload_endpoints = []

        # Set up the response structure
        self.context = {}
        self.event = {
            'header': {
                'namespace': kwargs.get('namespace', 'Alexa'),
                'name': kwargs.get('name', 'Response'),
                'messageId': str(uuid.uuid4()),
                'payloadVersion': kwargs.get('payload_version', '3')
                # 'correlation_token': kwargs.get('correlation_token', 'INVALID')
            },
            'endpoint': {
                "scope": {
                    "type": "BearerToken",
                    "token": kwargs.get('token', 'INVALID')
                },
                "endpointId": kwargs.get('endpoint_id', 'INVALID')
            },
            'payload': kwargs.get('payload', {})
        }

        if 'correlation_token' in kwargs:
            self.event['header']['correlation_token'] = kwargs.get('correlation_token', 'INVALID')

        if 'cookie' in kwargs:
            self.event['endpoint']['cookie'] = kwargs.get('cookie', '{}')

        # No endpoint in an AcceptGrant or Discover request
        if self.event['header']['name'] == 'AcceptGrant.Response' or self.event['header']['name'] == 'Discover.Response':
            self.event.pop('endpoint')

    def add_context_property(self, **kwargs):
        self.context_properties.append(self.create_context_property(**kwargs))

    def add_cookie(self, key, value):

        if "cookies" in self is None:
            self.cookies = {}

        self.cookies[key] = value

    def add_payload_endpoint(self, **kwargs):
        self.payload_endpoints.append(self.create_payload_endpoint(**kwargs))

    def create_context_property(self, **kwargs):
        return {
            'namespace': kwargs.get('namespace', 'Alexa.EndpointHealth'),
            'name': kwargs.get('name', 'connectivity'),
            'value': kwargs.get('value', {'value': 'OK'}),
            'timeOfSample': get_utc_timestamp(),
            'uncertaintyInMilliseconds': kwargs.get('uncertainty_in_milliseconds', 0)
        }

    def create_payload_endpoint(self, **kwargs):
        # Return the proper structure expected for the endpoint
        endpoint = {
            'capabilities': kwargs.get('capabilities', []),
            'description': kwargs.get('description', 'Sample Endpoint Description'),
            'displayCategories': kwargs.get('display_categories', ['OTHER']),
            'endpointId': kwargs.get('endpoint_id', 'endpoint_' + "%0.6d" % random.randint(0, 999999)),
            'friendlyName': kwargs.get('friendly_name', 'Sample Endpoint'),
            'manufacturerName': kwargs.get('manufacturer_name', 'Sample Manufacturer')
        }

        if 'cookie' in kwargs:
            endpoint['cookie'] = kwargs.get('cookie', {})

        return endpoint

    def create_payload_endpoint_capability(self, **kwargs):
        capability = {
            'type': kwargs.get('type', 'AlexaInterface'),
            'interface': kwargs.get('interface', 'Alexa'),
            'version': kwargs.get('version', '3')
        }
        supported = kwargs.get('supported', None)
        if supported:
            capability['properties'] = {}
            capability['properties']['supported'] = supported
            capability['properties']['proactivelyReported'] = kwargs.get('proactively_reported', False)
            capability['properties']['retrievable'] = kwargs.get('retrievable', False)
        return capability

    def get(self, remove_empty=True):

        response = {
            'context': self.context,
            'event': self.event
        }

        if len(self.context_properties) > 0:
            response['context']['properties'] = self.context_properties

        if len(self.payload_endpoints) > 0:
            response['event']['payload']['endpoints'] = self.payload_endpoints

        if remove_empty:
            if len(response['context']) < 1:
                response.pop('context')

        return response

    def set_payload(self, payload):
        self.event['payload'] = payload

    def set_payload_endpoint(self, payload_endpoints):
        self.payload_endpoints = payload_endpoints

    def set_payload_endpoints(self, payload_endpoints):
        if 'endpoints' not in self.event['payload']:
            self.event['payload']['endpoints'] = []

        self.event['payload']['endpoints'] = payload_endpoints
function code screenshot

15. Click Save

2.4 Finish configuring and test the Lambda function

1. Now we are going to test your Lambda function with a discovery directive

2. Click on Test. The Configure test event page appears.

3. Leave Create new test event selected.

4. For Event template, leave the default Hello World template.

5. In the Event name, enter DiscoveryTest and replace the entire contents in the editor with the following test input code

Copied to clipboard
{
    "directive": {
        "header": {
            "namespace": "Alexa.Discovery",
            "name": "Discover",
            "payloadVersion": "3",
            "messageId": "1bd5d003-31b9-476f-ad03-71d471922820"
        },
        "payload": {
            "scope": {
                "type": "BearerToken",
                "token": "access-token-from-skill"
            }
        }
    }
}

6. Click Create

function code screenshot

7. Click Test with DiscoveryTest selected. 

discovery test screenshot

8. If successful, you should get a message confirming that the test succeeded,

discovery test succeeded screenshot

with the Execution Result similar to the following:

Copied to clipboard
{
  "event": {
    "header": {
      "namespace": "Alexa.Discovery",
      "name": "Discover.Response",
      "messageId": "b5a1d155-3a97-479e-80fa-913b4afee758",
      "payloadVersion": "3"
    },
    "payload": {
      "endpoints": [
        {
          "capabilities": [
            {
              "type": "AlexaInterface",
              "interface": "Alexa",
              "version": "3"
            },
            {
              "type": "AlexaInterface",
              "interface": "Alexa.PowerController",
              "version": "3",
              "properties": {
                "supported": [
                  {
                    "name": "powerState"
                  }
                ],
                "proactivelyReported": false,
                "retrievable": false
              }
            }
          ],
          "description": "Sample Endpoint Description",
          "displayCategories": [
            "OTHER"
          ],
          "endpointId": "sample-switch-01",
          "friendlyName": "Sample Switch",
          "manufacturerName": "Sample Manufacturer"
        }
      ]
    }
  }
}