diff --git a/.gitignore b/.gitignore index b2a17c4..d07ed69 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ instance .vscode -gunicorn_config.py \ No newline at end of file +gunicorn_config.py +function.zip +lambda_function/package \ No newline at end of file diff --git a/Makefile b/Makefile index 1f52857..7c2ca2e 100644 --- a/Makefile +++ b/Makefile @@ -7,4 +7,11 @@ dev: pipenv run python app.py prod: - pipenv run gunicorn -c gunicorn_config.py wsgi:app \ No newline at end of file + 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 diff --git a/lambda_function/.DS_Store b/lambda_function/.DS_Store new file mode 100644 index 0000000..a52536f Binary files /dev/null and b/lambda_function/.DS_Store differ diff --git a/lambda_function/Pipfile b/lambda_function/Pipfile new file mode 100644 index 0000000..b723d01 --- /dev/null +++ b/lambda_function/Pipfile @@ -0,0 +1,11 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] + +[packages] + +[requires] +python_version = "3.7" diff --git a/lambda_function/alexa/__init__.py b/lambda_function/alexa/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lambda_function/alexa/__init__.py @@ -0,0 +1 @@ + diff --git a/lambda_function/alexa/skills/__init__.py b/lambda_function/alexa/skills/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lambda_function/alexa/skills/__init__.py @@ -0,0 +1 @@ + diff --git a/lambda_function/alexa/skills/smarthome/__init__.py b/lambda_function/alexa/skills/smarthome/__init__.py new file mode 100644 index 0000000..ea75b49 --- /dev/null +++ b/lambda_function/alexa/skills/smarthome/__init__.py @@ -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 diff --git a/lambda_function/alexa/skills/smarthome/alexa_response.py b/lambda_function/alexa/skills/smarthome/alexa_response.py new file mode 100644 index 0000000..3ebc5bf --- /dev/null +++ b/lambda_function/alexa/skills/smarthome/alexa_response.py @@ -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 diff --git a/lambda_function/alexa/skills/smarthome/alexa_utils.py b/lambda_function/alexa/skills/smarthome/alexa_utils.py new file mode 100644 index 0000000..2f9f59b --- /dev/null +++ b/lambda_function/alexa/skills/smarthome/alexa_utils.py @@ -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)) + + diff --git a/lambda_function/lambda_function.py b/lambda_function/lambda_function.py new file mode 100644 index 0000000..a1d1426 --- /dev/null +++ b/lambda_function/lambda_function.py @@ -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 + diff --git a/lambda_function/requirements.txt b/lambda_function/requirements.txt new file mode 100644 index 0000000..663bd1f --- /dev/null +++ b/lambda_function/requirements.txt @@ -0,0 +1 @@ +requests \ No newline at end of file