Skip to content

Commit

Permalink
add lambda function for alexa
Browse files Browse the repository at this point in the history
  • Loading branch information
cekk committed Dec 15, 2019
1 parent 78ce58a commit 8f3139b
Show file tree
Hide file tree
Showing 11 changed files with 355 additions and 2 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
instance
.vscode
gunicorn_config.py
gunicorn_config.py
function.zip
lambda_function/package
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,11 @@ dev:
pipenv run python app.py

prod:
pipenv run gunicorn -c gunicorn_config.py wsgi:app
pipenv run gunicorn -c gunicorn_config.py wsgi:app

deploy_lambda:
pipenv run pip install --target ./lambda_function/package -r lambda_function/requirements.txt
cd lambda_function/package && zip -r9 ../../function.zip .
cd lambda_function && zip -g ../function.zip lambda_function.py
cd lambda_function && zip -r9 -g ../function.zip alexa
aws2 lambda update-function-code --function-name gestore_tapparelle --zip-file fileb://function.zip
Binary file added lambda_function/.DS_Store
Binary file not shown.
11 changes: 11 additions & 0 deletions lambda_function/Pipfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]

[packages]

[requires]
python_version = "3.7"
1 change: 1 addition & 0 deletions lambda_function/alexa/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

1 change: 1 addition & 0 deletions lambda_function/alexa/skills/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

15 changes: 15 additions & 0 deletions lambda_function/alexa/skills/smarthome/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-

# Copyright 2018 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.

from .alexa_response import AlexaResponse
from .alexa_utils import get_utc_timestamp
152 changes: 152 additions & 0 deletions lambda_function/alexa/skills/smarthome/alexa_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# -*- coding: utf-8 -*-

# Copyright 2018 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 random
import uuid

from .alexa_utils import get_utc_timestamp


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"),
}
instance = kwargs.get("instance", None)
if instance:
capability["instance"] = instance
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)
semantics = kwargs.get("semantics", None)
if semantics:
capability["semantics"] = semantics
capability_resources = kwargs.get("capability_resources", None)
if capability_resources:
capability["capabilityResources"] = capability_resources
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
20 changes: 20 additions & 0 deletions lambda_function/alexa/skills/smarthome/alexa_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-

# Copyright 2018 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 time


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


143 changes: 143 additions & 0 deletions lambda_function/lambda_function.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# -*- coding: utf-8 -*-
from alexa.skills.smarthome import AlexaResponse

import boto3
import json
import requests
import os

api_endpoint = os.environ["API_ENDPOINT"]


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":
res = requests.get("{}/blinds".format(api_endpoint))

adr = AlexaResponse(namespace="Alexa.Discovery", name="Discover.Response")
capability_alexa = adr.create_payload_endpoint_capability()
capability_alexa_togglecontroller = adr.create_payload_endpoint_capability(
interface="Alexa.ToggleController",
supported=[{"name": "toggleState"}],
instance="Blind.BlindState",
capability_resources={
"friendlyNames": [
{
"@type": "text",
"value": {"text": "tapparella", "locale": "it-IT"},
}
]
},
semantics={
"actionMappings": [
{
"@type": "ActionsToDirective",
"actions": ["Alexa.Actions.Close"],
"directive": {"name": "TurnOff", "payload": {}},
},
{
"@type": "ActionsToDirective",
"actions": ["Alexa.Actions.Open"],
"directive": {"name": "TurnOn", "payload": {}},
},
],
"stateMappings": [
{
"@type": "StatesToValue",
"states": ["Alexa.States.Closed"],
"value": "OFF",
},
{
"@type": "StatesToValue",
"states": ["Alexa.States.Open"],
"value": "ON",
},
],
},
)

for blind in res.json():
adr.add_payload_endpoint(
friendly_name=blind.get("name"),
endpoint_id=blind.get("id"),
description="Remote controlled blind",
capabilities=[capability_alexa, capability_alexa_togglecontroller],
display_categories=["INTERIOR_BLIND"],
)
return send_response(adr.get())

if namespace == "Alexa.ToggleController":
# Note: This sample always returns a success response for either a request to TurnOff or TurnOn
device_id = request["directive"]["endpoint"]["endpointId"]
action = "close" if name == "TurnOff" else "open"
print(">>>>> NAME: {}".format(name))
print(request)
res = requests.get(
"{api_endpoint}/roller/{device_id}/{action}".format(
api_endpoint=api_endpoint, device_id=device_id, action=action
)
)
correlation_token = request["directive"]["header"]["correlationToken"]

apcr = AlexaResponse(correlation_token=correlation_token)
apcr.add_context_property(
namespace="Alexa.ToggleController", name="toggleState", value="ciccio",
)
return send_response(apcr.get())


def send_response(response):
# TODO Validate the response
print("lambda_handler response -----")
print(json.dumps(response))
return response

1 change: 1 addition & 0 deletions lambda_function/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
requests

0 comments on commit 8f3139b

Please sign in to comment.