-
Notifications
You must be signed in to change notification settings - Fork 4
/
app.js
307 lines (266 loc) · 9.79 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
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
/* eslint-disable prefer-rest-params */
/* eslint-disable node/no-unsupported-features/node-builtins */
/* eslint-disable global-require */
'use strict';
const Homey = require('homey');
const nodemailer = require('nodemailer');
const { Log } = require('homey-log');
const sectoralarm = require('sectoralarm');
const { Mutex } = require('async-mutex');
const SETTINGS = {
USERNAME: 'username',
PASSWORD: 'password',
SITEID: 'siteid',
LOCKCODE: 'lockcode',
POLLINTERVAL: 'pollinterval',
};
if (process.env.DEBUG === '1') {
require('inspector').open(9229, '0.0.0.0', false);
}
class MyApp extends Homey.App {
onInit() {
this.homeyLog = new Log({ homey: this.homey });
this.updateLog('MyApp is running...');
this.homey.settings.set('diagLog', '');
this.homey.settings.set('sendLog', '');
this.mutex = new Mutex();
this._site = null;
this._password = null;
this._username = null;
this._siteid = null;
this._MaxRetryLoginCount = 10;
this._retryLoginCount = this._MaxRetryLoginCount;
this.homey.settings.on('set', setting => {
if (setting === 'diagLog') return;
this.log(`Setting "${setting}" has changed`);
const diagLog = this.homey.settings.get('diagLog');
const sendLog = this.homey.settings.get('sendLog');
if (setting === 'sendLog' && (sendLog === 'send') && (diagLog !== '')) {
this.sendLog();
}
});
}
updateLog(newMessage, ignoreSetting = 1) {
let logLevel = this.homey.settings.get('logLevel');
if (!logLevel || logLevel === '') {
logLevel = 1;
}
if (ignoreSetting > logLevel) {
return;
}
this.log(newMessage);
let oldText = this.homey.settings.get('diagLog') || '';
if (oldText.length > 10000) {
// Remove the first 5000 characters.
oldText = oldText.substring(5000);
const n = oldText.indexOf('\n');
if (n >= 0) {
// Remove up to and including the first \n so the log starts on a whole line
oldText = oldText.substring(n + 1);
}
}
const nowTime = new Date(Date.now());
if (oldText.length === 0) {
oldText = 'Log ID: ';
oldText += nowTime.toJSON();
oldText += '\r\n';
oldText += 'App version ';
oldText += Homey.manifest.version;
oldText += '\r\n\r\n';
this.logLastTime = nowTime;
}
if (this.logLastTime === undefined) {
this.logLastTime = nowTime;
}
// const dt = new Date(nowTime.getTime() - this.logLastTime.getTime());
this.logLastTime = nowTime;
oldText += '+';
oldText += nowTime.getHours();
oldText += ':';
oldText += nowTime.getMinutes();
oldText += ':';
oldText += nowTime.getSeconds();
oldText += '.';
const milliSeconds = nowTime.getMilliseconds().toString();
if (milliSeconds.length === 2) {
oldText += '0';
} else if (milliSeconds.length === 1) {
oldText += '00';
}
oldText += milliSeconds;
oldText += ': ';
oldText += newMessage;
oldText += '\r\n';
this.homey.settings.set('diagLog', oldText);
this.homeyLog.setExtra({
diagLog: this.homey.settings.get('diagLog'),
});
this.homey.settings.set('sendLog', '');
}
async sendLog() {
let tries = 5;
while (tries-- > 0) {
try {
this.updateLog('Sending log', 0);
// create reusable transporter object using the default SMTP transport
const transporter = nodemailer.createTransport(
{
host: Homey.env.MAIL_HOST, // Homey.env.MAIL_HOST,
port: 587,
ignoreTLS: false,
secure: false, // true for 465, false for other ports
auth:
{
user: Homey.env.MAIL_USER, // generated ethereal user
pass: Homey.env.MAIL_SECRET, // generated ethereal password
},
tls:
{
// do not fail on invalid certs
rejectUnauthorized: false,
},
},
);
// send mail with defined transport object
const info = await transporter.sendMail(
{
from: `"Homey User" <${Homey.env.MAIL_USER}>`, // sender address
to: Homey.env.MAIL_RECIPIENT, // list of receivers
subject: 'Sector Alarm log', // Subject line
text: this.homey.settings.get('diagLog'), // plain text body
},
);
this.updateLog(`Message sent: ${info.messageId}`);
// Preview only available when sending through an Ethereal account
this.log('Preview URL: ', nodemailer.getTestMessageUrl(info));
return '';
} catch (err) {
this.updateLog(`Send log error: ${err.stack}`, 0);
}
}
this.updateLog('Send log FAILED', 0);
return '';
}
}
// *****************************************************************************
// *** WRAPPER FUNCTIONS FOR SECTOR_ALARM API ***
// *****************************************************************************
// Private function to get and refresh SectorAlarm connection
async function privatConnectToSite(app) {
const username = await app.homey.settings.get(SETTINGS.USERNAME);
const password = await app.homey.settings.get(SETTINGS.PASSWORD);
const siteid = await app.homey.settings.get(SETTINGS.SITEID);
if (app._site === null
|| app._password !== password
|| app._username !== username
|| app._siteid !== siteid) {
const settings = await sectoralarm.createSettings();
settings.numberOfRetries = 2;
settings.retryDelayInMs = 3000;
app._password = password;
app._username = username;
app._siteid = siteid;
await sectoralarm.connect(username, password, siteid, settings)
.then(site => {
app.updateLog('privateConnectToSite -> Got connection');
app._site = site;
return true;
})
.catch(error => {
app.updateLog('privateConnectToSite -> Error: No connection', 0);
throw (error);
});
}
return true;
}
// Private wraper function to enforce retrying all sectoralarm api calls upon errors
// Any parameters after the "functioncall" variable will be passed on to "functioncall"
// when it is called
async function privateSectoralarmWrapper(functioncall) {
// Make sure there is a connection first and refresh it if broken
try {
await privatConnectToSite(this);
} catch (error) {
this.updateLog('privateSectoralarmWrapper -> reconnect err', 0);
throw (error);
}
// Try to execute the api call
try {
const args = Array.prototype.slice.call(arguments, 1); // Only pass on arguments that belong to the function call
const retval = await functioncall.apply(this._site, args);
this._retryLoginCount = this._MaxRetryLoginCount;
this.updateLog(`privateSectoralarmWrapper -> Success: ${String(retval)}`);
return retval;
} catch (error) {
// Upon error retry until we get positive api call response or retry limit is reached
if (error.code === 'ERR_INVALID_SESSION') {
this.updateLog('private_sectoralarmWrapper Info: Invalid session, logging back in.');
let sessionId;
try {
sessionId = await this._site.login();
} catch (innerError) {
this.updateLog(`private_sectoralarmWrapper Unable to log in: ${innerError}`, 0);
throw (innerError);
}
if (sessionId.match(/(?<=ASPXAUTH=).+(?=; expires)/)) {
// A valid session id was returned
if (this._retryLoginCount > 0) {
this.updateLog(`private_sectoralarmWrapper -> Retry communication (${String(this._retryLoginCount)})`);
this._retryLoginCount--;
return privateSectoralarmWrapper.apply(this, arguments);
}
this.updateLog('private_sectoralarmWrapper -> number of login retries exceeded');
throw (error);
} else {
// Unable to obtain session cookie, most likely because the user has been blocked
throw new Error('Unable to log in. Please check that the user has not been blocked and that the user/password and pin codes are correct');
}
} else {
// Pass on errors
this.updateLog(`privateSectoralarmWrapper -> outer err code was: ${String(error.code)}`, 0);
throw (error);
}
}
}
// Wrap all sectoralarm function calls inside mutex-protected promises
async function privateSectoralarmProtectedWrapper(functioncall) {
return new Promise((resolve, reject) => {
this.mutex.runExclusive(async () => {
try {
const value = await privateSectoralarmWrapper.apply(this, arguments);
resolve(value);
} catch (error) {
reject(error);
}
});
});
}
// Create prototype functions for all sectoralarm API calls that need to be handled
// by the wrapper functions above
MyApp.prototype.connectToSite = async function connectToSite() {
await this.mutex.runExclusive(async () => {
return privatConnectToSite(this);
});
};
MyApp.prototype.status = async function status() {
return privateSectoralarmProtectedWrapper.call(this, this._site.status);
};
MyApp.prototype.locks = async function locks(lockId) {
return privateSectoralarmProtectedWrapper.call(this, this._site.locks, lockId);
};
MyApp.prototype.lock = async function lock(lockId, code) {
return privateSectoralarmProtectedWrapper.call(this, this._site.lock, lockId, code);
};
MyApp.prototype.unlock = async function unlock(lockId, code) {
return privateSectoralarmProtectedWrapper.call(this, this._site.unlock, lockId, code);
};
MyApp.prototype.arm = async function arm(code) {
return privateSectoralarmProtectedWrapper.call(this, this._site.arm, code);
};
MyApp.prototype.disarm = async function disarm(code) {
return privateSectoralarmProtectedWrapper.call(this, this._site.disarm, code);
};
MyApp.prototype.partialArm = async function partialArm(code) {
return privateSectoralarmProtectedWrapper.call(this, this._site.partialArm, code);
};
module.exports = MyApp;