-
Notifications
You must be signed in to change notification settings - Fork 64
/
Copy pathimpmonitor.agent.nut
350 lines (314 loc) · 12.4 KB
/
impmonitor.agent.nut
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
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
// impMonitor AGENT CODE
// Copyright (c) 2018, Electric Imp, Inc.
// Writer: Tony Smith
// Licence: MIT
// Version: 1.1.0
// IMPORTS
#require "Rocky.class.nut:2.0.1"
// If you are not using Squinter or an equivalent tool to combine multiple Squirrel files,
// you need to paste the contents of the accompanying files over the following four lines
#import "online.png.nut"
#import "offline.png.nut"
#import "warn.png.nut"
#import "spacer.png.nut"
// CONSTANTS
const LOGIN_KEY = "YOUR_LOGIN_KEY";
const HTML_HEADER = @"<!DOCTYPE html><html lang='en-US'><meta charset='UTF-8'>
<head>
<title>Device Status Monitor 1.1.0</title>
<link rel='stylesheet' href='https://netdna.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css'>
<link href='https://fonts.googleapis.com/css?family=Comfortaa' rel='stylesheet'>
<link rel='apple-touch-icon' href='https://smittytone.github.io/images/ati-imp.png'>
<link rel='shortcut icon' href='https://smittytone.github.io/images/ico-imp.ico'>
<meta http-equiv='Cache-Control' content='no-cache, no-store, must-revalidate'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<style>
.center { margin-left: auto; margin-right: auto; margin-bottom: auto; margin-top: auto; }
body {background-color: #21abd4;}
p {color: white; font-family: Comfortaa}
h3 {color: white; font-family: Comfortaa; font-weight:bold}
h4 {color: white; font-family: Comfortaa}
td {color: white; font-family: Comfortaa; vertical-align: center;}
.tabborder {width: 20px; text-align: center}
.tabcontent {width: 300px; text-align: left}
.uicontent {border: 2px solid white; padding: 5px}
.container {padding: 20px}
</style>
</head>
<body>
<div class='container'>
<div class='uicontent' align='center'>
<h3>Electric Imp</h3>
<h3>Device Status Monitor</h3>
<p>Updated at %s<br> </p>
<table width='100%%'>
";
const HTML_FOOTER = @" </table>
</div>
</div>
<script>
setTimeout(function() {
location.reload(true);
}, 30000);
</script>
</body>
</html>";
const ENTRY_START = @"<tr>
<td class='tabborder'><img src='%s' width='16' height='16'></td>
";
const ENTRY_END = @"<td class='tabborder'><img src='%s' width='16' height='16'></td>
<td class='tabcontent'><h4>%s</h4></td>
</tr>
";
const API_BASE_URL = "https://api.electricimp.com/v5/";
const USER_AGENT = "impAgent-impMonitor-1.1.0";
const LOOP_TIME = 60;
const PAGE_SIZE = 100;
const EXPIRY_DELTA = 5;
// GLOBALS
local api = null;
local token = null;
local isLoggedIn = false;
local htmlBody = "";
local devices = [];
// API ACCESS FUNCTIONS
#line 1000
function getDeviceData() {
server.log("Updating device information");
// Ask the impCentral API for a list of devices, calling up multiple pages
// as needed and then combining them into a single list 'uDevices'
local headers = { "User-Agent": "impAgent Monitor 1.1.0",
"Authorization": "Bearer " + token.accessToken };
local url = API_BASE_URL + "devices" + "?page[size]=" + format("%i", PAGE_SIZE);
local nextURL = "";
local uDevices = [];
do {
// Check the access token is still valid - it may expire mid-run
// If it is not valid, abandon the update
if (!(isAccessTokenValid())) return;
local request = http.get(url, headers);
local data = request.sendsync();
try {
data = http.jsondecode(data.body);
if ("links" in data) {
nextURL = getNextPageLink(getNextURL(data.links));
foreach (device in data.data) uDevices.append(device);
url = nextURL;
}
} catch (err) {
server.error(err + " (data: " + data.body + ")");
}
} while (nextURL.len() != 0);
// Build the UI from the list of devices gathered above
if (uDevices.len() > 0) {
htmlBody = "";
uDevices.sort(sorter);
foreach (device in uDevices) {
// 'device' is a table - use it to get the device's name (or ID if it has no name),
// and its online status, then build the device's entry in the HTML table
local isOnline = device.attributes.device_online;
local deviceName = device.attributes.name;
if (deviceName == null) deviceName = device.id;
local statusImageURL = http.agenturl() + "/images/o" + (isOnline ? "n" : "ff") + ".png";
local tableEntry = format(ENTRY_END, statusImageURL, deviceName);
local warnImageCode = "/images/spacer.png";
// Find the current device's record in the saved list to see
// if its online status has changed since we last checked
foreach (aDevice in devices) {
if (aDevice.id == device.id) {
if (aDevice.attributes.device_online != device.attributes.device_online) {
// The device's status has changed, so add a warning triangle to the web UI
warnImageCode = "/images/warn.png";
}
break;
}
}
// Assemble the table entry: add a space, or a warning triangle if the device's status has changed
tableEntry = format(ENTRY_START, http.agenturl() + warnImageCode) + tableEntry;
// Add the table entry to the HTML body
htmlBody = htmlBody + tableEntry;
}
// Having processed the updated device list, store it for next time
devices = uDevices;
} else {
// Create a simple HTML body indicating the user has no devices
htmlBody = @"<tr><td class='tabcontent'><h4 align='center'>No Devices</h4></td></tr>";
}
}
function sorter(first, second) {
// Sort devices by name
local a = first.attributes.name.tolower();
local b = second.attributes.name.tolower();
if (a == null || a > b) return 1;
if (b == null || a < b) return -1;
return 0;
}
function isFirstPage(links) {
// Check the 'links' dictionary returned by the impCentral API.
// Responds 'true' or 'false' if the received data is the first page of several
local isFirst = false;
foreach (link in links) {
if (link == "first") {
local currentPageLink = links.self;
local firstPageLink = links.first;
if (currentPageLink == firstPageLink) {
// The current page is the first one
isFirst = true;
break;
}
}
}
return isFirst;
}
function getNextURL(links) {
// Check the 'links' dictionary returned by the the impCentral API.
// Returns the URL of the next page of data
local nextURLString = "";
foreach (link in links) {
if (link == "next") {
// We have at least one more page to recover before we have the full list
nextURLString = links.next;
break;
}
}
return nextURLString;
}
function getNextPageLink(url = null) {
// Strips the non-query content out of the supplied URL, or
// returns an empty string if 'url' is nil or empty - what's
// returned is added to a full URL by the calling method
if (url = null || url.len() == 0) return "";
return url.slice(31);
}
function login(key = null) {
// Login is the process of sending the user's login key to the impCentral API
// in order to retrieve a new access token
if (key == null || key.len() == 0) {
server.error("Could not log in to the Electric Imp impCloud — no login key");
return;
}
// Retain the credential for future use
token = { "loginkey": key };
// Get a new token using the credentials provided
getNewAccessToken();
}
function getNewAccessToken() {
// Request a new access token using the stored credentials,
// Set up a POST request to the /auth URL to get an access token
local body = { "key": token.loginkey };
local headers = { "User-Agent": USER_AGENT,
"Content-Type": "application/json" };
local url = API_BASE_URL + "auth/token";
local request = http.post(url, headers, http.jsonencode(body));
server.log("Getting acccess token");
request.sendasync(gotAccessToken);
}
function gotAccessToken(respData) {
// We have retrieved a new access token, so add it and other
// useful data to the 'token' structure
if (respData.statuscode != 200) {
isLoggedIn = false;
server.error("Function: gotAccessToken()");
server.error("Code: " + respData.statuscode);
server.error("Data: " + respData.body);
return;
}
try {
// Attempt to decode the response from impCentral
local data = http.jsondecode(respData.body);
if ("access_token" in data) {
if ("accessToken" in token) {
token.accessToken = data.access_token;
} else {
token.accessToken <- data.access_token;
}
server.log("Acquired Access Token: " + token.accessToken.slice(0, 40) + "...");
isLoggedIn = true;
} else {
server.error("Could not get access token. JSON: " + respData.body);
isLoggedIn = false;
return;
}
if ("expires_in" in data) {
if ("expiryTime" in token) {
token.expiryTime = time() + data.expires_in.tointeger();
} else {
token.expiryTime <- time() + data.expires_in.tointeger();
}
server.log("Expires in " + data.expires_in + " seconds");
} else {
server.error("No access token expiry time");
}
} catch (err) {
server.error("gotAccessToken() " + err);
}
}
function isAccessTokenValid() {
// Check if the currently held access token has expired.
// Return 'true' if it is good, or 'false' if we need a new one
// No token available? Return false
local rv = true;
if (token == null || !("expiryTime" in token)) {
// We don't have a token (or it lacks an expiry time)
// so just mark it as expired
rv = false;
} else {
local now = time();
// Expire the token even it it hasn't expired, but is nonetheless
// within EXPIRY_DELTA of expiry. This is to prevent the token
// expiring while the request is being made, ie. after the request
// is sent but before the server has checked it
if (now >= token.expiryTime - EXPIRY_DELTA) rv = false;
}
if (!rv) server.log("Access Token has expired");
return rv;
}
function timestamp() {
// Return the current date and time in a printable form
local now = date();
local timeString = format("%02i", now.hour) + ":" + format("%02i", now.min) + ":" + format("%02i", now.sec);
local dateString = format("%04i", now.year) + "-" + format("%02i", now.month + 1) + "-" + format("%02i", now.day);
return dateString + " " + timeString;
}
// DEVICE-CHECK LOOP FUNCTION
function updateDevices() {
if (isLoggedIn) {
// Check the current access token is valid
if (!(isAccessTokenValid())) {
// The access token has expired - refresh it
getNewAccessToken();
} else {
// The access token is good - update the devices' status
getDeviceData();
}
} else {
// Initialize (or re-initialize after a disconnect)
// our access to the impCentral API
login(LOGIN_KEY);
}
// Schedule the next check in LOOP_TIME seconds
updateTimer = imp.wakeup(LOOP_TIME, updateDevices);
}
// START OF PROGRAM
// Define the API to serve the web UI
api = Rocky();
// Any call to the endpoint / is sent the current web page
api.get("/", function(context) {
context.send(200, format(HTML_HEADER, timestamp()) + htmlBody + HTML_FOOTER);
});
// Any call to the endpoint /images is sent the correct PNG data
api.get("/images/([^/]*)", function(context) {
// Determine which image has been requested and send the appropriate
// stored data back to the requesting web browser
context.setHeader("Content-Type", "image/png");
local path = context.matches[1];
local imageData = ONLINE_PNG;
if (path == "off.png") imageData = OFFLINE_PNG;
if (path == "warn.png") imageData = WARN_PNG;
if (path == "spacer.png") imageData = SPACER_PNG;
context.send(200, imageData);
});
// Set default text to display in the web UI
htmlBody = @"<tr><td class='tabcontent'><h4 align='center'>Getting Device Info...</h4></td></tr>";
// Start the device check loop
updateDevices();