-
Notifications
You must be signed in to change notification settings - Fork 1
/
app.js
226 lines (191 loc) · 6.9 KB
/
app.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
'use strict';
const express = require('express');
const fs = require('fs');
const fetch = require('node-fetch');
const app = express();
const api_key = "EOOEMOW4YR6QNB07";
// TODO: Take timezone as a parameter?
const timeZone = 'Canada/Eastern';
const Datastore = require('@google-cloud/datastore');
const datastore = Datastore();
const sessionKey = datastore.key(['Session', 'default']);
async function doLogin() {
const session = await login();
session.device_serial = await getDevice(session);
await datastore.upsert({key: sessionKey, data: session});
return session;
}
async function getSession() {
const sessions = await datastore.get(sessionKey);
if (sessions[0]) {
// Note that if this session has expired, we'll try to login again in getTemps
return sessions[0];
}
return await doLogin();
}
async function login() {
const secrets = JSON.parse(fs.readFileSync(__dirname + "/secrets.json"));
const response = await fetch('https://support.iaqualink.com/users/sign_in.json', {
body: JSON.stringify({
api_key: api_key,
email: secrets.email,
password: secrets.password }),
headers: {
'content-type': 'application/json'},
method: 'POST'
});
if (response.status !== 200)
throw new Error('sign_in.json failure - status:' + response.status);
const json = await response.json();
if (!('session_id' in json) || !('id' in json) || !('authentication_token' in json)) {
console.error('Unexpected signin response: ', json);
throw new Error('sign_in.json failure - unexpected response');
}
const s = {
id: json.session_id,
user_id: json.id,
authentication_token: json.authentication_token};
console.log('logged in with session', s);
return s;
}
async function getDevice(session) {
const url = 'https://support.iaqualink.com/devices.json' +
'?api_key=' + api_key +
'&authentication_token=' + session.authentication_token +
'&user_id=' + session.user_id;
const response = await fetch(url);
if (response.status !== 200) {
const body = await response.text();
console.error('devices.json failure', url, response.status, response.statusText, body);
throw new Error('devices.json failure:' + response.status + ' ' + response.statusText);
}
const json = await response.json();
if (!('0' in json) || !('serial_number' in json[0])) {
console.error('Unexpected devices response: ', json);
throw new Error('devices.json failure - unexpected response');
}
return json[0].serial_number;
}
async function getTemps(session) {
let body;
for (let attempt = 0; attempt < 2; attempt++) {
const url = 'https://iaqualink-api.realtime.io/v1/mobile/session.json' +
'?actionID=command' +
'&command=get_home' +
'&serial=' + session.device_serial +
'&sessionID=' + session.id;
const response = await fetch(url);
body = await response.text();
if (response.status !== 200) {
console.error('session.json failure', url, response.status, response.statusText, body);
throw new Error('session.json failure:' + response.status + ' ' + response.statusText);
}
if (body) {
// Success fetching something, no more attempts
break;
} else {
// Empty body seems to imply a bad session ID, re-auth
if (!attempt) {
const oldSessionId = session.id;
session = await doLogin();
console.error(`session.json empty response with session ${oldSessionId}, retrying with new session ${session.id}`);
} else {
throw new Error('session.json repeated empty response');
}
}
}
const json = JSON.parse(body);
// Convert array of key/value pairs into an object
const items = Object.assign({}, ...json.home_screen);
if (items.status !== 'Online') {
console.error(`Failed to get temps. Status: ${items.status} Response: ${items.response}`);
// Use empty data so we can visualize how much is missing
return {air: '', pool: '', heater: ''};
}
// Compute heater temperature.
// "1" means heating, "3" means on but not heating
// "spa" (temp 1) seems to take precedence when it's on
let heater = 0;
if (items.spa_heater==="1")
heater = parseInt(items.spa_set_point, 10);
else if(items.pool_heater==="1")
heater = parseInt(items.pool_set_point, 10);
else if(items.spa_heater!=="0" && items.spa_heater!=="3")
throw new Error('Unexpected spa_heater: ' + items.spa_heater);
else if(items.pool_heater!=="0" && items.pool_heater!=="3")
throw new Error('Unexpected pool_heater: ' + items.pool_heater);
return {
air: items.air_temp ? parseInt(items.air_temp, 10) : '',
pool: items.pool_temp ? parseInt(items.pool_temp, 10) : '',
heater: heater};
}
async function update() {
const session = await getSession();
const temps = await getTemps(session);
if (!temps)
return 'Temperature unavailable';
temps.timestamp = new Date();
// Get the running entry
const latestTempsKey = datastore.key(['Temps', 'latest']);
const results = await datastore.get(latestTempsKey);
const last = results[0];
// If the temperatures haven't changed and we don't need to keep the running entry
const same = last && temps.air === last.air && temps.pool === last.pool && temps.heater === last.heater;
if (same && !last.keep) {
// Update the running entry with the new timestamp
await datastore.upsert({key: latestTempsKey, data: temps});
return 'No change: ' + JSON.stringify(temps);
}
// There's been a change, permanently save the prior entry.
delete last.keep;
await datastore.save({key: datastore.key(['Temps']), data: last});
// Setup a new running entry.
// If there's actually a temp change, mark the new running entry to prevent
// coalescing to ensure we keep the timestamp immediately after a change.
if (!same)
temps.keep = true;
await datastore.upsert({key: latestTempsKey, data: temps});
return 'Added entry: ' + JSON.stringify(temps);
}
async function log(response) {
const query = datastore.createQuery('Temps')
.order('timestamp', { descending: true });
const results = await datastore.runQuery(query);
// Create the response in a string.
// If this gets really big we could setup a streaming response,
// and use a query cursor.
let csv = 'timesamp, air, pool, heater\n';
for(let temps of results[0]) {
let date = temps.timestamp.toLocaleString('en-US', { timeZone: timeZone });
date = date.replace(",","");
csv += `${date}, ${temps.air}, ${temps.pool}, ${temps.heater}\n`;
}
response
.status(200)
.set('Content-Type', 'text/csv')
.send(csv)
.end();
}
app.get('/', (req, res) => {
res.sendFile(__dirname + "/static/index.html");
});
app.get('/update', (req, res) => {
update().then(msg => {
res.status(200).send(msg).end();
}).catch(error => {
console.error(error);
res.status(500).send('Server error!');
});
});
app.get('/log.csv', (req, res) => {
log(res).catch(error => {
console.error(error);
res.status(500).send('Server error!');
});
});
// Start the server
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
console.log(`App listening on port ${PORT}`);
console.log('Press Ctrl+C to quit.');
});