-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathserver.js
More file actions
265 lines (231 loc) · 8.34 KB
/
server.js
File metadata and controls
265 lines (231 loc) · 8.34 KB
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
require('dotenv').config();
const path = require('path');
const express = require('express');
const cookieSession = require('cookie-session');
const logger = require('morgan');
const encodeUrl = require('encodeurl');
const SSE = require('express-sse');
const FileContextStore = require('@smartthings/file-context-store');
const SmartApp = require('@smartthings/smartapp');
const { nanoid } = require('nanoid');
const port = process.env.PORT || 3000;
const appId = process.env.APP_ID;
const clientId = process.env.CLIENT_ID;
const clientSecret = process.env.CLIENT_SECRET;
const serverUrl = process.env.SERVER_URL || `https://${process.env.PROJECT_DOMAIN}.glitch.me`;
const redirectUri = `${serverUrl}/oauth/callback`;
const scope = encodeUrl('r:locations:* r:devices:* x:devices:*');
/*
* Server-sent events. Used to update the status of devices on the web page from subscribed events
*/
const sse = new SSE();
/**
* Stores access tokens and other properties for calling the SmartThings API. This implementation is a simple flat file
* store that is for demo purposes not appropriate for production systems. Other context stores exist, including
* DynamoDB and Firebase.
*/
const contextStore = new FileContextStore('data');
/*
* Thew SmartApp. Provides an API for making REST calls to the SmartThings platform and
* handles calls from the platform for subscribed events as well as the initial app registration challenge.
*/
const apiApp = new SmartApp()
.appId(appId)
.clientId(clientId)
.clientSecret(clientSecret)
.contextStore(contextStore)
.redirectUri(redirectUri)
.enableEventLogging(2)
.subscribedEventHandler('switchHandler', async (ctx, event) => {
/* Device event handler. Current implementation only supports main component switches */
if (event.componentId === 'main') {
try {
sse.send({
deviceId: event.deviceId,
switchState: event.value
});
} catch(e) {
console.log(e.message);
}
}
console.log(`EVENT ${event.deviceId} ${event.componentId}.${event.capability}.${event.attribute}: ${event.value}`);
});
/*
* Webserver setup
*/
const server = express();
server.set('views', path.join(__dirname, 'views'));
server.use(cookieSession({
name: 'session',
keys: ['key1', 'key2']
}));
server.set('view engine', 'ejs');
server.use(logger(':date[iso] :method :url :res[location] :status :response-time ms'));
server.use(express.json());
server.use(express.urlencoded({extended: false}));
server.use(express.static(path.join(__dirname, 'public')));
// Needed to avoid flush error with express-sse and newer versions of Node
server.use(function (req, res, next) {
res.flush = function () { /* Do nothing */ };
next();
});
/*
* Handles calls to the SmartApp from SmartThings, i.e. registration challenges and device events
*/
server.post('/', async (req, res) => {
apiApp.handleHttpCallback(req, res);
});
/*
* Main web page. Shows link to SmartThings if not authenticated and list of switch devices afterwards
*/
server.get('/',async (req, res) => {
if (req.session.smartThings) {
// Cookie found, display page with list of devices
const data = req.session.smartThings;
res.render('devices', {
installedAppId: data.installedAppId,
locationName: data.locationName
});
}
else {
// No context cookie. Display link to authenticate with SmartThings
const state = nanoid(24);
req.session.smartThingsState = state;
res.render('index', {
url: `https://api.smartthings.com/oauth/authorize?client_id=${clientId}&scope=${scope}&response_type=code&redirect_uri=${redirectUri}&state=${state}`
});
}
});
/**
* Returns view model data for the devices page
*/
server.get('/viewData', async (req, res) => {
const data = req.session.smartThings;
// Read the context from DynamoDB so that API calls can be made
const ctx = await apiApp.withContext(data.installedAppId);
try {
// Get the list of switch devices, which doesn't include the state of the switch
const deviceList = await ctx.api.devices.list({capability: 'switch'});
// Query for the state of each one
const ops = deviceList.map(it => {
return ctx.api.devices.getCapabilityStatus(it.deviceId, 'main', 'switch').then(state => {
return {
deviceId: it.deviceId,
label: it.label,
switchState: state.switch.value
};
});
});
// Wait for all those queries to complete
const devices = await Promise.all(ops);
// Respond to the request
res.send({
errorMessage: devices.length > 0 ? '' : 'No switch devices found in location',
devices: devices.sort( (a, b) => {
return a.label === b.label ? 0 : (a.label > b.label) ? 1 : -1;
})
});
} catch (error) {
res.send({
errorMessage: `${error.message || error}`,
devices: []
});
}
});
/*
* Logout. Uninstalls app and clears context cookie
*/
server.get('/logout', async function(req, res) {
try {
// Read the context from DynamoDB so that API calls can be made
const ctx = await apiApp.withContext(req.session.smartThings.installedAppId);
// Delete the installed app instance from SmartThings
await ctx.api.installedApps.delete();
}
catch (error) {
console.error('Error logging out', error.message);
}
// Delete the session data
req.session = null;
res.redirect('/');
});
/*
* Handles OAuth redirect
*/
server.get('/oauth/callback', async (req, res, next) => {
try {
// Validate the state
if (req.query.state !== req.session.smartThingsState) {
res.status(400).send('Invalid state');
return;
}
// Store the SmartApp context including access and refresh tokens. Returns a context object for use in making
// API calls to SmartThings
const ctx = await apiApp.handleOAuthCallback(req);
// Get the location name (for display on the web page)
const location = await ctx.api.locations.get(ctx.locationId);
// Set the cookie with the context, including the location ID and name
req.session.smartThings = {
locationId: ctx.locationId,
locationName: location.name,
installedAppId: ctx.installedAppId
};
// Remove any existing subscriptions and unsubscribe to device switch events
try {
await ctx.api.subscriptions.delete();
try {
await ctx.api.subscriptions.subscribeToCapability('switch', 'switch', 'switchHandler');
} catch (error) {
console.error('Error subscribing to switch events', error.message);
}
} catch (error) {
console.error('Error deleting subscriptions', error.message);
}
// Redirect back to the main page
res.redirect('/');
} catch (error) {
console.log('Error handling OAuth callback', error.message);
next(error);
}
});
/**
* Executes a device command from the web page
*/
server.post('/command/:deviceId', async(req, res, next) => {
try {
// Read the context from DynamoDB so that API calls can be made
const ctx = await apiApp.withContext(req.session.smartThings.installedAppId);
// Execute the device command
await ctx.api.devices.executeCommands(req.params.deviceId, req.body.commands);
res.send({});
} catch (error) {
next(error);
}
});
/**
* Executes a command for all devices
*/
server.post('/commands', async(req, res) => {
console.log(JSON.stringify(req.body.commands, null, 2));
// Read the context from DynamoDB so that API calls can be made
const ctx = await apiApp.withContext(req.session.smartThings.installedAppId);
const devices = await ctx.api.devices.list({capability: 'switch'});
const ops = [];
for (const device of devices) {
ops.push(ctx.api.devices.executeCommands(device.deviceId, req.body.commands));
}
await Promise.all(ops);
res.send({});
});
/**
* Handle SSE connection from the web page
*/
server.get('/events', sse.init);
/**
* Start the HTTP server and log URLs. Use the "open" URL for starting the OAuth process. Use the "callback"
* URL in the API app definition using the SmartThings Developer Workspace.
*/
server.listen(port);
console.log(`\nTarget URL -- Copy this value into the targetUrl field of you app creation request:\n${serverUrl}\n`);
console.log(`Redirect URI -- Copy this value into redirectUris field of your app creation request:\n${redirectUri}\n`);
console.log(`Website URL -- Visit this URL in your browser to log into SmartThings and connect your account:\n${serverUrl}`);