· 4 years ago · Apr 12, 2021, 08:22 AM
1# -*- coding: utf-8 -*-
2
3# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
4#
5# Licensed under the Amazon Software License (the "License"). You may not use this file except in
6# compliance with the License. A copy of the License is located at
7#
8# http://aws.amazon.com/asl/
9#
10# or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific
12# language governing permissions and limitations under the License.
13
14import boto3
15import json
16import random
17import uuid
18import time
19
20aws_dynamodb = boto3.client('dynamodb')
21
22def lambda_handler(request, context):
23
24 # Dump the request for logging - check the CloudWatch logs
25 print('lambda_handler request -----')
26 print(json.dumps(request))
27
28 if context is not None:
29 print('lambda_handler context -----')
30 print(context)
31
32 # Validate we have an Alexa directive
33 if 'directive' not in request:
34 aer = AlexaResponse(
35 name='ErrorResponse',
36 payload={'type': 'INVALID_DIRECTIVE',
37 'message': 'Missing key: directive, Is the request a valid Alexa Directive?'})
38 return send_response(aer.get())
39
40 # Check the payload version
41 payload_version = request['directive']['header']['payloadVersion']
42 if payload_version != "3":
43 aer = AlexaResponse(
44 name='ErrorResponse',
45 payload={'type': 'INTERNAL_ERROR',
46 'message': 'This skill only supports Smart Home API version 3'})
47 return send_response(aer.get())
48
49 # Crack open the request and see what is being requested
50 name = request['directive']['header']['name']
51 namespace = request['directive']['header']['namespace']
52
53 # Handle the incoming request from Alexa based on the namespace
54
55 if namespace == 'Alexa.Authorization':
56 if name == 'AcceptGrant':
57 # Note: This sample accepts any grant request
58 # In your implementation you would use the code and token to get and store access tokens
59 grant_code = request['directive']['payload']['grant']['code']
60 grantee_token = request['directive']['payload']['grantee']['token']
61 aar = AlexaResponse(namespace='Alexa.Authorization', name='AcceptGrant.Response')
62 return send_response(aar.get())
63
64 if namespace == 'Alexa.Discovery':
65 if name == 'Discover':
66 adr = AlexaResponse(namespace='Alexa.Discovery', name='Discover.Response')
67 capability_alexa = adr.create_payload_endpoint_capability()
68 capability_alexa_powercontroller = adr.create_payload_endpoint_capability(
69 interface='Alexa.PowerController',
70 supported=[{'name': 'powerState'}])
71 adr.add_payload_endpoint(
72 friendly_name='Sample Switch',
73 endpoint_id='sample-switch-01',
74 capabilities=[capability_alexa, capability_alexa_powercontroller])
75 return send_response(adr.get())
76
77 if namespace == 'Alexa.PowerController':
78 # Note: This sample always returns a success response for either a request to TurnOff or TurnOn
79 endpoint_id = request['directive']['endpoint']['endpointId']
80 power_state_value = 'OFF' if name == 'TurnOff' else 'ON'
81 correlation_token = request['directive']['header']['correlationToken']
82
83 # Check for an error when setting the state
84 state_set = set_device_state(endpoint_id=endpoint_id, state='powerState', value=power_state_value)
85 if not state_set:
86 return AlexaResponse(
87 name='ErrorResponse',
88 payload={'type': 'ENDPOINT_UNREACHABLE', 'message': 'Unable to reach endpoint database.'}).get()
89
90 apcr = AlexaResponse(correlation_token=correlation_token)
91 apcr.add_context_property(namespace='Alexa.PowerController', name='powerState', value=power_state_value)
92 return send_response(apcr.get())
93
94
95
96 if namespace == "Alexa":
97 name = request['directive']['header']['name']
98 correlation_token = request['directive']['header']['correlationToken']
99 token = request['directive']['endpoint']['scope']['token']
100 endpoint_id = request['directive']['endpoint']['endpointId']
101
102 if name == 'ReportState':
103 # Get the User ID from the access_token
104 response_user_id = json.loads(ApiAuth.get_user_id(token).read().decode('utf-8'))
105 result = dynamodb_aws.get_item(TableName='SampleSmartHome', Key={'EndpointId': {'S': endpoint_id}})
106 capabilities_string = self.get_db_value(result['Item']['Capabilities'])
107 capabilities = json.loads(capabilities_string)
108 props=[]
109 for c in capabilities:
110 if not 'properties' in c:
111 continue
112 retrievable = c['properties'].get('retrievable', False)
113 if retrievable:
114 props.append(c)
115 state = {}
116 try:
117 res = iot_data_aws.get_thing_shadow(thingName=endpoint_id)
118 shadow=json.loads(res['payload'].read())
119 state = shadow['state']['desired']
120 except ClientError as e:
121 print('LOG ', e)
122
123
124 print('Sending StateReport for', response_user_id, 'on endpoint', endpoint_id)
125 statereport_response = AlexaResponse(name='StateReport', endpoint_id=endpoint_id, correlation_token=correlation_token, token=token)
126
127 for p in props:
128 key = p['properties']['supported'][0]['name']
129 if 'instance' in p:
130 key = p['instance']+'.'+key
131 current_state = state.get(key, DEFAULT_VAL[p['interface']])
132 if 'instance' in p:
133 statereport_response.add_context_property(namespace=p['interface'],
134 name=p['properties']['supported'][0]['name'], value=current_state,
135 instance=p['instance'])
136 else:
137 statereport_response.add_context_property(namespace=p['interface'],
138 name=p['properties']['supported'][0]['name'], value=current_state)
139
140
141 response = statereport_response.get()
142
143
144
145
146def send_response(response):
147 print('lambda_handler response -----')
148 print(json.dumps(response))
149 return response
150
151def set_device_state(endpoint_id, state, value):
152 attribute_key = state + 'Value'
153 response = aws_dynamodb.update_item(
154 TableName='SampleSmartHome',
155 Key={'ItemId': {'S': endpoint_id}},
156 AttributeUpdates={attribute_key: {'Action': 'PUT', 'Value': {'S': value}}})
157 print(response)
158 if response['ResponseMetadata']['HTTPStatusCode'] == 200:
159 return True
160 else:
161 return False
162
163def get_utc_timestamp(seconds=None):
164 return time.strftime('%Y-%m-%dT%H:%M:%S.00Z', time.gmtime(seconds))
165
166class AlexaResponse:
167
168 def __init__(self, **kwargs):
169
170 self.context_properties = []
171 self.payload_endpoints = []
172
173 # Set up the response structure
174 self.context = {
175 "properties": [
176
177 {
178
179 "timeOfSample": get_utc_timestamp(),
180
181 "uncertaintyInMilliseconds": 10000,
182
183 "namespace": "Alexa.PowerController",
184
185 "name": "powerState",
186
187 "value": "OFF"
188
189 }
190
191 ]
192 }
193 self.event = {
194 'header': {
195 'namespace': kwargs.get('namespace', 'Alexa'),
196 'name': kwargs.get('name', 'Response'),
197 'messageId': str(uuid.uuid4()),
198 'payloadVersion': kwargs.get('payload_version', '3')
199 # 'correlation_token': kwargs.get('correlation_token', 'INVALID')
200 },
201 'endpoint': {
202 "scope": {
203 "type": "BearerToken",
204 "token": kwargs.get('token', 'INVALID')
205 },
206 "endpointId": kwargs.get('endpoint_id', 'INVALID')
207 },
208 'payload': kwargs.get('payload', {})
209 }
210
211 if 'correlation_token' in kwargs:
212 self.event['header']['correlation_token'] = kwargs.get('correlation_token', 'INVALID')
213
214 if 'cookie' in kwargs:
215 self.event['endpoint']['cookie'] = kwargs.get('cookie', '{}')
216
217 # No endpoint in an AcceptGrant or Discover request
218 if self.event['header']['name'] == 'AcceptGrant.Response' or self.event['header']['name'] == 'Discover.Response':
219 self.event.pop('endpoint')
220
221 def add_context_property(self, **kwargs):
222 self.context_properties.append(self.create_context_property(**kwargs))
223
224 def add_cookie(self, key, value):
225
226 if "cookies" in self is None:
227 self.cookies = {}
228
229 self.cookies[key] = value
230
231 def add_payload_endpoint(self, **kwargs):
232 self.payload_endpoints.append(self.create_payload_endpoint(**kwargs))
233
234 def create_context_property(self, **kwargs):
235 return {
236 'namespace': kwargs.get('namespace', 'Alexa.EndpointHealth'),
237 'name': kwargs.get('name', 'connectivity'),
238 'value': kwargs.get('value', {'value': 'OK'}),
239 'timeOfSample': get_utc_timestamp(),
240 'uncertaintyInMilliseconds': kwargs.get('uncertainty_in_milliseconds', 0)
241 }
242
243 def create_payload_endpoint(self, **kwargs):
244 # Return the proper structure expected for the endpoint
245 endpoint = {
246 'capabilities': kwargs.get('capabilities', []),
247 'description': kwargs.get('description', 'Sample Endpoint Description'),
248 'displayCategories': kwargs.get('display_categories', ['LIGHT']),
249 'endpointId': kwargs.get('endpoint_id', 'endpoint_' + "%0.6d" % random.randint(0, 999999)),
250 'friendlyName': kwargs.get('friendly_name', 'Sample Endpoint'),
251 'manufacturerName': kwargs.get('manufacturer_name', 'Sample Manufacturer')
252 }
253
254 if 'cookie' in kwargs:
255 endpoint['cookie'] = kwargs.get('cookie', {})
256
257 return endpoint
258
259 def create_payload_endpoint_capability(self, **kwargs):
260 capability = {
261 'type': kwargs.get('type', 'AlexaInterface'),
262 'interface': kwargs.get('interface', 'Alexa'),
263 'version': kwargs.get('version', '3')
264 }
265 supported = kwargs.get('supported', None)
266 if supported:
267 capability['properties'] = {}
268 capability['properties']['supported'] = supported
269 capability['properties']['proactivelyReported'] = kwargs.get('proactively_reported', False)
270 capability['properties']['retrievable'] = kwargs.get('retrievable', True)
271 return capability
272
273 def get(self, remove_empty=True):
274
275 response = {
276 'context': self.context,
277 'event': self.event
278 }
279
280 if len(self.context_properties) > 0:
281 response['context']['properties'] = self.context_properties
282
283 if len(self.payload_endpoints) > 0:
284 response['event']['payload']['endpoints'] = self.payload_endpoints
285
286 if remove_empty:
287 if len(response['context']) < 1:
288 response.pop('context')
289
290 return response
291
292 def set_payload(self, payload):
293 self.event['payload'] = payload
294
295 def set_payload_endpoint(self, payload_endpoints):
296 self.payload_endpoints = payload_endpoints
297
298 def set_payload_endpoints(self, payload_endpoints):
299 if 'endpoints' not in self.event['payload']:
300 self.event['payload']['endpoints'] = []
301
302 self.event['payload']['endpoints'] = payload_endpoints