-
Notifications
You must be signed in to change notification settings - Fork 1
/
skill.js
289 lines (260 loc) · 9.16 KB
/
skill.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
const request = require('request-promise-lite');
// Devices server should be set in environment
const SERVER_URL = process.env.DEVICES_SERVER_URL;
/**
* Generate a unique message ID
* @return {string} uuid 4
*/
function generateMessageID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
/**
* Basic logger. Can be replaced by anything (such as winston)
* @param {string} level - log level to show in the message
* @param {*} args - logged message and arguments
*/
function log(level, ...args) {
console.log(level.toUpperCase(), ...args);
}
/**
* Generate a response message
*
* @param {string} namespace - Directive namespace
* @param {string} name - Directive name
* @param {Object} [payload] - Any special payload required for the response
* @returns {Object} Response object
*/
function generateResponse(namespace, name, payload = {}) {
return {
header: {
messageId: generateMessageID(),
name,
namespace,
payloadVersion: '2',
},
payload,
};
}
function getDevicesFromPartnerCloud(token) {
return request.get(`${SERVER_URL}/devices/`, {
json: true,
headers: {
Authorization: `Bearer ${token}`
}
}).then((devices) => {
// convert to Alexa format
return devices.map(device => ({
applianceId: device.id,
modelName: device.model,
version: device.version,
manufacturerName: 'Sonoff',
friendlyName: device.name,
friendlyDescription: `Sonoff switch: ${device.name}`,
isReachable: device.isOnline,
actions: ['turnOn', 'turnOff'],
additionalApplianceDetails: {}
}))
});
}
function isValidToken(token) {
if (!token) {
return Promise.reject();
}
return request.get(`${SERVER_URL}/users/me`, {
json: true,
headers: {
Authorization: `Bearer ${token}`
}
});
}
function isDeviceOnline(applianceId, token) {
log('DEBUG', `isDeviceOnline (applianceId: ${applianceId})`);
/**
* Always returns true for sample code.
* You should update this method to your own validation.
*/
return request.get(`${SERVER_URL}/devices/${applianceId}`, {
json: true,
headers: {
Authorization: `Bearer ${token}`
}
}).then((device) => {
return device.isOnline;
});
}
function turnOn(applianceId, token) {
log('DEBUG', `turnOn (applianceId: ${applianceId})`);
return request.patch(`${SERVER_URL}/devices/${applianceId}`, {
json: true,
body: { state: { switch: 'on' } },
headers: {
Authorization: `Bearer ${token}`
}
}).then(() => {
log('INFO', `turned on successfully ${applianceId}`);
return generateResponse('Alexa.ConnectedHome.Control', 'TurnOnConfirmation');
}).catch((e) => {
log('ERROR',`unable to turn on ${applianceId}`, e);
});
}
function turnOff(applianceId, token) {
log('DEBUG', `turnOff (applianceId: ${applianceId})`);
return request.patch(`${SERVER_URL}/devices/${applianceId}`, {
json: true,
body: {
state: { switch: 'off' } },
headers: {
Authorization: `Bearer ${token}`
}
}).then(() => {
log('INFO', `turned off successfully ${applianceId}`);
return generateResponse('Alexa.ConnectedHome.Control', 'TurnOffConfirmation');
}).catch((e) => {
log('ERROR',`unable to turn off ${applianceId}`, e);
});
}
/**
* Main logic
*/
/**
* This function is invoked when we receive a "Discovery" message from Alexa Smart Home Skill.
* We are expected to respond back with a list of appliances that we have discovered for a given customer.
*
* @param {Object} request - The full request object from the Alexa smart home service. This represents a DiscoverAppliancesRequest.
* https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/smart-home-skill-api-reference#discoverappliancesrequest
*/
function handleDiscovery(request) {
log('DEBUG', `Discovery Request: ${JSON.stringify(request)}`);
/**
* Get the OAuth token from the request.
*/
const userAccessToken = request.payload.accessToken.trim();
return isValidToken(userAccessToken)
.then(() => getDevicesFromPartnerCloud(userAccessToken))
.then((devices) => {
const response = generateResponse('Alexa.ConnectedHome.Discovery', 'DiscoverAppliancesResponse', {
discoveredAppliances: devices
});
/**
* log the response. These messages will be stored in CloudWatch.
*/
log('DEBUG', `Discovery Response: ${JSON.stringify(response)}`);
return response;
}).catch((e) => {
const errorMessage = `Discovery Request [${request.header.messageId}] failed. Invalid access token: ${userAccessToken}`;
log('ERROR',errorMessage, e);
return errorMessage;
});
}
/**
* A function to handle control events.
* This is called when Alexa requests an action such as turning off an appliance.
*
* @param {Object} request - The full request object from the Alexa smart home service.
*/
function handleControl(request) {
log('DEBUG', `Control Request: ${JSON.stringify(request)}`);
/**
* Get the OAuth token from the request.
*/
const userAccessToken = request.payload.accessToken.trim();
return isValidToken(userAccessToken).then(() => {
/**
* Grab the applianceId from the request.
*/
const applianceId = request.payload.appliance.applianceId;
/**
* If the applianceId is missing, return UnexpectedInformationReceivedError
* https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/smart-home-skill-api-reference#unexpectedinformationreceivederror
*/
if (!applianceId) {
log('ERROR', 'No applianceId provided in request');
const payload = { faultingParameter: `applianceId: ${applianceId}` };
return Promise.resolve(generateResponse(
'Alexa.ConnectedHome.Control',
'UnexpectedInformationReceivedError',
payload
));
}
/**
* At this point the applianceId and accessToken are present in the request.
*
* Please review the full list of errors in the link below for different states that can be reported.
* If these apply to your device/cloud infrastructure, please add the checks and respond with
* accurate error messages. This will give the user the best experience and help diagnose issues with
* their devices, accounts, and environment
* https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/smart-home-skill-api-reference#error-messages
*/
return isDeviceOnline(applianceId, userAccessToken).then((status) => {
if (!status) {
log('ERROR', `Device offline: ${applianceId}`);
return Promise.resolve(generateResponse(
'Alexa.ConnectedHome.Control',
'TargetOfflineError'));
}
// Device is online
switch (request.header.name) {
case 'TurnOnRequest':
return turnOn(applianceId, userAccessToken);
case 'TurnOffRequest':
return turnOff(applianceId, userAccessToken);
default: {
log('ERROR', `Unsupported directive name: ${request.header.name}`);
return Promise.resolve(generateResponse(
'Alexa.ConnectedHome.Control',
'UnsupportedOperationError'));
}
}
});
}).catch((e) => {
const errorMessage = `Control Failed. Request [${request.header.messageId}] failed. Invalid access token: ${userAccessToken}`;
log('ERROR', errorMessage, e);
return errorMessage;
});
}
/**
* Main entry point.
* Incoming events from Alexa service through Smart Home API are all handled by this function.
*
* It is recommended to validate the request and response with Alexa Smart Home Skill API Validation package.
* https://github.com/alexa/alexa-smarthome-validation
*/
exports.handler = (request, context, callback) => {
let response;
switch (request.header.namespace) {
/**
* The namespace of 'Alexa.ConnectedHome.Discovery' indicates a request is being made to the Lambda for
* discovering all appliances associated with the customer's appliance cloud account.
*
* For more information on device discovery, please see
* https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/smart-home-skill-api-reference#discovery-messages
*/
case 'Alexa.ConnectedHome.Discovery':
response = handleDiscovery(request);
break;
/**
* The namespace of "Alexa.ConnectedHome.Control" indicates a request is being made to control devices such as
* a dimmable or non-dimmable bulb. The full list of Control events sent to your lambda are described below.
* https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/smart-home-skill-api-reference#payload
*/
case 'Alexa.ConnectedHome.Control':
response = handleControl(request);
break;
/**
* Received an unexpected message
*/
default: {
const errorMessage = `Unsupported namespace: ${request.header.namespace}`;
log('ERROR', errorMessage);
response = Promise.reject(errorMessage);
}
}
response.then((resp) => {
callback(null, resp);
}).catch((errorMessage) => {
callback(new Error(errorMessage));
});
};