signalk-to-noforeignland 0.1.15 → 0.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +516 -468
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -10,478 +10,526 @@ const fetch = require('node-fetch');
10
10
  const isReachable = require('is-reachable');
11
11
  const sendEmail = require('./sendEmail');
12
12
  const createGPX = require('./createGPX');
13
- const apiUrl = 'https://www.noforeignland.com/home/api/v1/boat/tracking/track';
13
+ const defaultApiUrl = 'https://www.noforeignland.com/home/api/v1/boat/tracking/track';
14
14
  const pluginApiKey = '0ede6cb6-5213-45f5-8ab4-b4836b236f97';
15
15
  // const msToKn = 1.944;
16
16
 
17
17
 
18
- module.exports = function (app) {
19
- var plugin = {};
20
- plugin.id = 'signalk-to-noforeignland';
21
- plugin.name = 'SignalK to Noforeignland';
22
- plugin.description = 'SignalK track logger to noforeignland.com';
23
-
24
- plugin.schema = {
25
- "title": plugin.name,
26
- "description": "Some parameters need for use",
27
- "type": "object",
28
- "required": ["emailCron", "boatApiKey"],
29
- "properties": {
30
- "trackFrequency": {
31
- "type": "integer",
32
- "title": "Position tracking frequency in seconds.",
33
- "description": "To keep file sizes small we only log positions once in a while (unless you set this value to 0)",
34
- "default": 60
35
- },
36
- "minMove": {
37
- "type": "number",
38
- "title": "Minimum boat move to log in meters",
39
- "description": "To keep file sizes small we only log positions if a move larger than this size is noted (if set to 0 will log every move)",
40
- "default": 50
41
- },
42
- "minSpeed": {
43
- "type": "number",
44
- "title": "Minimum boat speed to log in knots",
45
- "description": "To keep file sizes small we only log positions if boat speed goes above this value to minimize recording position on anchor or mooring (if set to 0 will log every move)",
46
- "default": 1.5
47
- },
48
- "emailCron": {
49
- "type": "string",
50
- "title": "Send attempt CRON",
51
- "description": "We send the tracking data to NFL once in a while, you can set the schedule with this setting. CRON format: https://crontab.guru/",
52
- "default": '*/10 * * * *',
53
- },
54
- 'boatApiKey': {
55
- "type": "string",
56
- "title": "Boat API key",
57
- "description": "Boat API key from noforeignland.com. Can be found in Account > Settings > Boat tracking > API Key. *required only in API method is set*",
58
- },
59
- "internetTestTimeout": {
60
- "type": "number",
61
- "title": "Timeout for testing internet connection in ms",
62
- "description": "Set this number higher for slower computers and internet connections",
63
- "default": 2000,
64
- },
65
- "sendWhileMoving": {
66
- "type": "boolean",
67
- "title": "Attempt sending location while moving",
68
- "description": "Should the plugin attempt to send tracking data to NFL while detecting the vessel is moving or only when stopped?",
69
- "default": false
70
- },
71
- "filterSource": {
72
- "type": "string",
73
- "title": "Position source device",
74
- "description": "Set this value to the name of a source if you want to only use the position given by that source.",
75
- },
76
- "trackDir": {
77
- "type": "string",
78
- "title": "Directory to cache tracks.",
79
- "description": "Path in server filesystem, absolute or from plugin directory. optional param (only used to keep file cache).",
80
- },
81
- "keepFiles": {
82
- "type": "boolean",
83
- "title": "Should keep track files on disk?",
84
- "description": "If you have a lot of hard drive space you can keep the track files for logging purposes.",
85
- "default": false
86
- }
87
- }
88
- };
89
-
90
- var unsubscribes = [];
91
- var unsubscribesControl = [];
92
- var routeSaveName = 'nfl-track.jsonl';
93
- let lastPosition;
94
- let upSince;
95
- let cron;
96
- const creator = 'signalk-track-logger';
97
- const defaultTracksDir = 'track';
98
- // const maxAllowedSpeed = 100;
99
-
100
- plugin.start = function (options, restartPlugin) {
101
- if (!options.trackDir) options.trackDir = defaultTracksDir;
102
- if (!path.isAbsolute(options.trackDir)) options.trackDir = path.join(__dirname, options.trackDir);
103
- //app.debug('options.trackDir=',options.trackDir);
104
- if (!createDir(options.trackDir)) {
105
- plugin.stop();
106
- return;
107
- }
108
-
109
- app.debug('track logger started, now logging to', options.trackDir);
110
- app.setPluginStatus(`Started`);
111
-
112
- doLogging();
113
-
114
- function doLogging() {
115
- let shouldDoLog = true
116
- //subscribe for position
117
- app.subscriptionmanager.subscribe({
118
- "context": "vessels.self",
119
- "subscribe": [
120
- {
121
- "path": "navigation.position",
122
- "format": "delta",
123
- "policy": "instant",
124
- "minPeriod": options.trackFrequency ? options.trackFrequency * 1000 : 0,
125
- }
126
- ]
127
- },
128
- unsubscribes,
129
- subscriptionError => {
130
- app.debug('Error subscription to data:' + subscriptionError);
131
- app.setPluginError('Error subscription to data:' + subscriptionError.message);
132
- },
133
- doOnValue
134
- );
135
-
136
- //subscribe for speed
137
- if (options.minSpeed) {
138
- app.subscriptionmanager.subscribe({
139
- "context": "vessels.self",
140
- "subscribe": [
141
- {
142
- "path": "navigation.speedOverGround",
143
- "format": "delta",
144
- "policy": "instant",
145
- }
146
- ]
147
- },
148
- unsubscribes,
149
- subscriptionError => {
150
- app.debug('Error subscription to data:' + subscriptionError);
151
- app.setPluginError('Error subscription to data:' + subscriptionError.message);
152
- },
153
- delta => {
154
- // app.debug('got speed delta', delta);
155
- delta.updates.forEach(update => {
156
- // app.debug(`update:`, update);
157
- if (options.filterSource && update.$source !== options.filterSource) {
158
- return;
159
- }
160
- update.values.forEach(value => {
161
- // value.value is sog in m/s so 'sog*2' is in knots
162
- if (!shouldDoLog && options.minSpeed < value.value * 2) {
163
- app.debug('setting shouldDoLog to true');
164
- shouldDoLog = true;
165
- }
166
- })
167
- })
168
- }
169
- );
170
- }
171
-
172
- async function doOnValue(delta) {
173
-
174
- for (update of delta.updates) {
175
- // app.debug(`update:`, update);
176
- if (options.filterSource && update.$source !== options.filterSource) {
177
- return;
178
- }
179
- let timestamp = update.timestamp;
180
- for (value of update.values) {
181
-
182
-
183
- if (
184
- Math.abs(value.value.latitude) <= 0.01 &&
185
- Math.abs(value.value.longitude) <= 0.01
186
- ) {
187
- // Coordinates are within ±0.1 of (0,0)
188
- return;
18
+ module.exports = function(app) {
19
+ var plugin = {};
20
+ plugin.id = 'signalk-to-noforeignland-beta';
21
+ plugin.name = 'SignalK to Noforeignland-beta';
22
+ plugin.description = 'SignalK track logger to noforeignland.com';
23
+
24
+ plugin.schema = {
25
+ "title": plugin.name,
26
+ "description": "Some parameters need for use",
27
+ "type": "object",
28
+ "required": ["emailCron", "boatApiKey"],
29
+ "properties": {
30
+ "trackFrequency": {
31
+ "type": "integer",
32
+ "title": "Position tracking frequency in seconds.",
33
+ "description": "To keep file sizes small we only log positions once in a while (unless you set this value to 0)",
34
+ "default": 60
35
+ },
36
+ "minMove": {
37
+ "type": "number",
38
+ "title": "Minimum boat move to log in meters",
39
+ "description": "To keep file sizes small we only log positions if a move larger than this size is noted (if set to 0 will log every move)",
40
+ "default": 50
41
+ },
42
+ "minSpeed": {
43
+ "type": "number",
44
+ "title": "Minimum boat speed to log in knots",
45
+ "description": "To keep file sizes small we only log positions if boat speed goes above this value to minimize recording position on anchor or mooring (if set to 0 will log every move)",
46
+ "default": 1.5
47
+ },
48
+ "emailCron": {
49
+ "type": "string",
50
+ "title": "Send attempt CRON",
51
+ "description": "We send the tracking data to NFL once in a while, you can set the schedule with this setting. CRON format: https://crontab.guru/",
52
+ "default": '*/10 * * * *',
53
+ },
54
+ 'boatApiKey': {
55
+ "type": "string",
56
+ "title": "Boat API key",
57
+ "description": "Boat API key from noforeignland.com. Can be found in Account > Settings > Boat tracking > API Key. *required only in API method is set*",
58
+ },
59
+ "internetTestTimeout": {
60
+ "type": "number",
61
+ "title": "Timeout for testing internet connection in ms",
62
+ "description": "Set this number higher for slower computers and internet connections",
63
+ "default": 2000,
64
+ },
65
+ "sendWhileMoving": {
66
+ "type": "boolean",
67
+ "title": "Attempt sending location while moving",
68
+ "description": "Should the plugin attempt to send tracking data to NFL while detecting the vessel is moving or only when stopped?",
69
+ "default": false
70
+ },
71
+ "filterSource": {
72
+ "type": "string",
73
+ "title": "Position source device",
74
+ "description": "Set this value to the name of a source if you want to only use the position given by that source.",
75
+ },
76
+ "trackDir": {
77
+ "type": "string",
78
+ "title": "Directory to cache tracks.",
79
+ "description": "Path in server filesystem, absolute or from plugin directory. optional param (only used to keep file cache).",
80
+ },
81
+ "keepFiles": {
82
+ "type": "boolean",
83
+ "title": "Should keep track files on disk?",
84
+ "description": "If you have a lot of hard drive space you can keep the track files for logging purposes.",
85
+ "default": false
86
+ },
87
+ "ping": {
88
+ "type": "boolean",
89
+ "title": "Should I force a send every 24 hours",
90
+ "description": "Keeps your boat active on NFL in your current location even if you do not move",
91
+ "default": true
92
+ },
93
+ "defaultApiUrl": {
94
+ "type": "string",
95
+ "title": "NFL tracking API endpoint",
96
+ "description": "Change only if NFL gives you a different endpoint.",
97
+ "default": defaultApiUrl
189
98
  }
190
-
191
- // app.debug(`value:`, value);
192
-
193
- if (!shouldDoLog) {
194
- return;
195
- }
196
- if (!isValidLatitude(value.value.latitude) || !isValidLongitude(value.value.longitude)) {
197
- return;
198
- }
199
-
200
-
201
- if (lastPosition) {
202
- if (new Date(lastPosition.timestamp).getTime() > new Date(timestamp).getTime()) {
203
- app.debug('got error in timestamp:', timestamp, 'is earlier than previous:', lastPosition.timestamp);
204
- // SK sometimes messes up timestamps, when that happens we throw the update
205
- return;
206
- }
207
- const distance = equirectangularDistance(lastPosition.pos, value.value)
208
- if (options.minMove && distance < options.minMove) {
209
- return;
210
- }
211
- // if (calculatedSpeed(distance, (timestamp - lastPosition.timestamp) / 1000) > maxAllowedSpeed) {
212
- // app.debug('got error position', value.value, 'ignoring...');
213
- // return;
214
- // }
215
- }
216
- lastPosition = { pos: value.value, timestamp, currentTime: new Date().getTime() };
217
-
218
- await savePoint(lastPosition);
219
- if (options.minSpeed) {
220
- app.debug('setting shouldDoLog to false');
221
- shouldDoLog = false;
222
- }
223
- };
224
- };
225
- }
226
- }
227
-
228
- async function savePoint(point) {
229
- //{pos: {latitude, longitude}, timestamp}
230
- // Date.parse(timestamp)
231
- const obj = {
232
- lat: point.pos.latitude,
233
- lon: point.pos.longitude,
234
- t: point.timestamp,
235
- }
236
- app.debug(`save data point:`, obj);
237
- await fs.appendFile(path.join(options.trackDir, routeSaveName), JSON.stringify(obj) + EOL);
238
- }
239
-
240
- function isValidLatitude(obj) {
241
- return isDefinedNumber(obj) && obj > -90 && obj < 90
242
- }
243
-
244
- function isValidLongitude(obj) {
245
- return isDefinedNumber(obj) && obj > -180 && obj < 180
246
- }
247
-
248
- function isDefinedNumber(obj) {
249
- return (obj !== undefined && obj !== null && typeof obj === 'number');
250
- }
251
-
252
- // function calculatedSpeed(distance, timeSecs) {
253
- // // m/s to knots ~= speedinms * 1.944
254
- // return (distance / timeSecs) * msToKn
255
- // }
256
-
257
- function equirectangularDistance(from, to) {
258
- // https://www.movable-type.co.uk/scripts/latlong.html
259
- // from,to: {longitude: xx, latitude: xx}
260
- const rad = Math.PI / 180;
261
- const φ1 = from.latitude * rad;
262
- const φ2 = to.latitude * rad;
263
- const Δλ = (to.longitude - from.longitude) * rad;
264
- const R = 6371e3;
265
- const x = Δλ * Math.cos((φ1 + φ2) / 2);
266
- const y = (φ2 - φ1);
267
- const d = Math.sqrt(x * x + y * y) * R;
268
- return d;
269
- }
270
-
271
- function createDir(dir) {
272
- let res = true;
273
- if (fs.existsSync(dir)) {
274
- try {
275
- fs.accessSync(dir, fs.constants.R_OK | fs.constants.W_OK);
276
- }
277
- catch (error) {
278
- app.debug('[createDir]', error.message);
279
- app.setPluginError(`No rights to directory ${dir}`);
280
- res = false;
281
- }
282
- }
283
- else {
284
- try {
285
- fs.mkdirSync(dir, { recursive: true });
286
- }
287
- catch (error) {
288
- switch (error.code) {
289
- case 'EACCES': // Permission denied
290
- case 'EPERM': // Operation not permitted
291
- app.debug(`False to create ${dir} by Permission denied`);
292
- app.setPluginError(`False to create ${dir} by Permission denied`);
293
- res = false;
294
- break;
295
- case 'ETIMEDOUT': // Operation timed out
296
- app.debug(`False to create ${dir} by Operation timed out`);
297
- app.setPluginError(`False to create ${dir} by Operation timed out`);
298
- res = false;
299
- break;
300
- }
301
- }
302
- }
303
- return res;
304
- } // end function createDir
305
-
306
- async function interval() {
307
- if ((checkBoatMoving()) && await checkTrack() && await testInternet()) {
308
- await sendData();
309
- }
310
- }
311
-
312
- function checkBoatMoving() {
313
- if (options.sendWhileMoving || !options.trackFrequency) {
314
- return true;
315
- }
316
- const time = lastPosition ? lastPosition.currentTime : upSince;
317
-
318
- const secsSinceLastPoint = (new Date().getTime() - time)/1000
319
- if (secsSinceLastPoint > (options.trackFrequency * 2)) {
320
- app.debug('Boat stopped moving, last move at least', secsSinceLastPoint,'seconds ago');
321
- return true;
322
- } else {
323
- app.debug('Boat is still moving, last move', secsSinceLastPoint,'seconds ago');
324
- return false;
325
- }
326
- }
327
-
328
- async function testInternet() {
329
- app.debug('testing internet connection');
330
- const check = await isReachable(internetTestAddress, { timeout: options.internetTestTimeout || internetTestTimeout });
331
- app.debug('internet connection = ', check);
332
- return check;
333
- }
334
-
335
- async function checkTrack() {
336
- const trackFile = path.join(options.trackDir, routeSaveName);
337
- app.debug('checking the track', trackFile, 'if should send');
338
- const exists = await fs.pathExists(trackFile);
339
- const size = exists ? (await fs.lstat(trackFile)).size : 0;
340
- app.debug(`'${trackFile}'.size=${size} ${trackFile}'.exists=${exists}`);
341
- return size > 0;
342
- }
343
-
344
- async function sendData() {
345
- if (options.boatApiKey) {
346
- sendApiData();
347
- } else {
348
- sendEmailData();
349
- }
350
- }
351
-
352
- async function sendApiData() {
353
- app.debug('sending the data');
354
- const trackData = await createTrack(path.join(options.trackDir, routeSaveName));
355
- if (!trackData) {
356
- app.debug('Recorded track did not contain any valid track points, aborting sending.');
357
- return;
358
- }
359
- app.debug('created track data with timestamp:', new Date(trackData.timestamp));
360
-
361
- const params = new URLSearchParams();
362
- params.append('timestamp', trackData.timestamp);
363
- params.append('track', JSON.stringify(trackData.track));
364
- params.append('boatApiKey', options.boatApiKey);
365
-
366
- const headers = {
367
- 'X-NFL-API-Key': pluginApiKey
368
- }
369
-
370
- app.debug('sending track to API');
371
- try {
372
- const response = await fetch(apiUrl, { method: 'POST', body: params, headers: new fetch.Headers(headers) });
373
- if (response.ok) {
374
- const responseBody = await response.json();
375
- if (responseBody.status === 'ok') {
376
- app.debug('Track successfully sent to API');
377
- if (options.keepFiles) {
378
- const filename = new Date().toJSON().slice(0, 19).replace(/:/g, '') + '-nfl-track.jsonl';
379
- app.debug('moving and keeping track file: ', filename);
380
- await fs.move(path.join(options.trackDir, routeSaveName), path.join(options.trackDir, filename));
381
- } else {
382
- app.debug('Deleting track file');
383
- await fs.remove(path.join(options.trackDir, routeSaveName));
384
- }
385
- } else {
386
- app.debug('Could not send track to API, returned response json:', responseBody);
387
- }
388
- } else {
389
- app.debug('Could not send track to API, returned response code:', response.status, response.statusText);
390
- }
391
- } catch (err) {
392
- app.debug('Could not send track to API due to error:', err);
393
- }
394
- }
395
-
396
- async function createTrack(inputPath) {
397
- const fileStream = fs.createReadStream(inputPath);
398
-
399
- const rl = readline.createInterface({
400
- input: fileStream,
401
- crlfDelay: Infinity
402
- });
403
- const track = []
404
- let lastTimestamp;
405
- for await (const line of rl) {
406
- if (line) {
407
- try {
408
- const point = JSON.parse(line);
409
-
410
- const timestamp = new Date(point.t).getTime();
411
- if (!isNaN(timestamp) && isValidLatitude(point.lat) && isValidLongitude(point.lon)) {
412
- track.push([timestamp, point.lat, point.lon]);
413
- lastTimestamp = timestamp;
99
+
100
+ }
101
+ };
102
+
103
+ var unsubscribes = [];
104
+ var unsubscribesControl = [];
105
+ var routeSaveName = 'nfl-track.jsonl';
106
+ let lastPosition;
107
+ let upSince;
108
+ let cron;
109
+ const creator = 'signalk-track-logger';
110
+ const defaultTracksDir = 'track';
111
+
112
+ let initialSent = false;
113
+ let lastSentTime = 0;
114
+
115
+ // const maxAllowedSpeed = 100;
116
+
117
+ plugin.start = function(options, restartPlugin) {
118
+ if (!options.trackDir) options.trackDir = defaultTracksDir;
119
+ if (!path.isAbsolute(options.trackDir)) options.trackDir = path.join(__dirname, options.trackDir);
120
+ //app.debug('options.trackDir=',options.trackDir);
121
+ if (!createDir(options.trackDir)) {
122
+ plugin.stop();
123
+ return;
124
+ }
125
+
126
+ app.debug('track logger started, now logging to', options.trackDir);
127
+ app.setPluginStatus(`Started`);
128
+
129
+ doLogging();
130
+
131
+ function doLogging() {
132
+ let shouldDoLog = true
133
+ //subscribe for position
134
+ app.subscriptionmanager.subscribe({
135
+ "context": "vessels.self",
136
+ "subscribe": [
137
+ {
138
+ "path": "navigation.position",
139
+ "format": "delta",
140
+ "policy": "instant",
141
+ "minPeriod": options.trackFrequency ? options.trackFrequency * 1000 : 0,
142
+ }
143
+ ]
144
+ },
145
+ unsubscribes,
146
+ subscriptionError => {
147
+ app.debug('Error subscription to data:' + subscriptionError);
148
+ app.setPluginError('Error subscription to data:' + subscriptionError.message);
149
+ },
150
+ doOnValue
151
+ );
152
+
153
+ //subscribe for speed
154
+ if (options.minSpeed) {
155
+ app.subscriptionmanager.subscribe({
156
+ "context": "vessels.self",
157
+ "subscribe": [
158
+ {
159
+ "path": "navigation.speedOverGround",
160
+ "format": "delta",
161
+ "policy": "instant",
162
+ }
163
+ ]
164
+ },
165
+ unsubscribes,
166
+ subscriptionError => {
167
+ app.debug('Error subscription to data:' + subscriptionError);
168
+ app.setPluginError('Error subscription to data:' + subscriptionError.message);
169
+ },
170
+ delta => {
171
+ // app.debug('got speed delta', delta);
172
+ delta.updates.forEach(update => {
173
+ // app.debug(`update:`, update);
174
+ if (options.filterSource && update.$source !== options.filterSource) {
175
+ return;
176
+ }
177
+ update.values.forEach(value => {
178
+ // value.value is sog in m/s so 'sog*2' is in knots
179
+ if (!shouldDoLog && options.minSpeed < value.value * 2) {
180
+ app.debug('setting shouldDoLog to true');
181
+ shouldDoLog = true;
182
+ }
183
+ })
184
+ })
185
+ }
186
+ );
187
+ }
188
+
189
+ async function doOnValue(delta) {
190
+
191
+ for (update of delta.updates) {
192
+ // app.debug(`update:`, update);
193
+ if (options.filterSource && update.$source !== options.filterSource) {
194
+ return;
195
+ }
196
+ let timestamp = update.timestamp;
197
+ for (value of update.values) {
198
+
199
+
200
+ if (
201
+ Math.abs(value.value.latitude) <= 0.01 &&
202
+ Math.abs(value.value.longitude) <= 0.01
203
+ ) {
204
+ // Coordinates are within ±0.1 of (0,0)
205
+ return;
206
+ }
207
+
208
+ // app.debug(`value:`, value);
209
+
210
+ if (!shouldDoLog) {
211
+ return;
212
+ }
213
+ if (!isValidLatitude(value.value.latitude) || !isValidLongitude(value.value.longitude)) {
214
+ return;
215
+ }
216
+
217
+
218
+ if (lastPosition) {
219
+ if (new Date(lastPosition.timestamp).getTime() > new Date(timestamp).getTime()) {
220
+ app.debug('got error in timestamp:', timestamp, 'is earlier than previous:', lastPosition.timestamp);
221
+ // SK sometimes messes up timestamps, when that happens we throw the update
222
+ return;
223
+ }
224
+ const distance = equirectangularDistance(lastPosition.pos, value.value)
225
+ if (options.minMove && distance < options.minMove) {
226
+ return;
227
+ }
228
+ // if (calculatedSpeed(distance, (timestamp - lastPosition.timestamp) / 1000) > maxAllowedSpeed) {
229
+ // app.debug('got error position', value.value, 'ignoring...');
230
+ // return;
231
+ // }
232
+ }
233
+ lastPosition = { pos: value.value, timestamp, currentTime: new Date().getTime() };
234
+
235
+ await savePoint(lastPosition);
236
+ if (options.minSpeed) {
237
+ app.debug('setting shouldDoLog to false');
238
+ shouldDoLog = false;
239
+ }
240
+ if (!initialSent) {
241
+ initialSent = true;
242
+ app.debug('sending initial fix');
243
+ if (await testInternet()) {
244
+ await sendData();
245
+ lastSentTime = Date.now();
246
+ }
247
+ }
248
+
249
+
250
+
251
+
252
+ };
253
+ };
254
+ }
255
+ }
256
+
257
+ async function savePoint(point) {
258
+ //{pos: {latitude, longitude}, timestamp}
259
+ // Date.parse(timestamp)
260
+ const obj = {
261
+ lat: point.pos.latitude,
262
+ lon: point.pos.longitude,
263
+ t: point.timestamp,
264
+ }
265
+ app.debug(`save data point:`, obj);
266
+ await fs.appendFile(path.join(options.trackDir, routeSaveName), JSON.stringify(obj) + EOL);
267
+ }
268
+
269
+ function isValidLatitude(obj) {
270
+ return isDefinedNumber(obj) && obj > -90 && obj < 90
271
+ }
272
+
273
+ function isValidLongitude(obj) {
274
+ return isDefinedNumber(obj) && obj > -180 && obj < 180
275
+ }
276
+
277
+ function isDefinedNumber(obj) {
278
+ return (obj !== undefined && obj !== null && typeof obj === 'number');
279
+ }
280
+
281
+ // function calculatedSpeed(distance, timeSecs) {
282
+ // // m/s to knots ~= speedinms * 1.944
283
+ // return (distance / timeSecs) * msToKn
284
+ // }
285
+
286
+ function equirectangularDistance(from, to) {
287
+ // https://www.movable-type.co.uk/scripts/latlong.html
288
+ // from,to: {longitude: xx, latitude: xx}
289
+ const rad = Math.PI / 180;
290
+ const φ1 = from.latitude * rad;
291
+ const φ2 = to.latitude * rad;
292
+ const Δλ = (to.longitude - from.longitude) * rad;
293
+ const R = 6371e3;
294
+ const x = Δλ * Math.cos((φ1 + φ2) / 2);
295
+ const y = (φ2 - φ1);
296
+ const d = Math.sqrt(x * x + y * y) * R;
297
+ return d;
298
+ }
299
+
300
+ function createDir(dir) {
301
+ let res = true;
302
+ if (fs.existsSync(dir)) {
303
+ try {
304
+ fs.accessSync(dir, fs.constants.R_OK | fs.constants.W_OK);
305
+ }
306
+ catch (error) {
307
+ app.debug('[createDir]', error.message);
308
+ app.setPluginError(`No rights to directory ${dir}`);
309
+ res = false;
310
+ }
311
+ }
312
+ else {
313
+ try {
314
+ fs.mkdirSync(dir, { recursive: true });
315
+ }
316
+ catch (error) {
317
+ switch (error.code) {
318
+ case 'EACCES': // Permission denied
319
+ case 'EPERM': // Operation not permitted
320
+ app.debug(`False to create ${dir} by Permission denied`);
321
+ app.setPluginError(`False to create ${dir} by Permission denied`);
322
+ res = false;
323
+ break;
324
+ case 'ETIMEDOUT': // Operation timed out
325
+ app.debug(`False to create ${dir} by Operation timed out`);
326
+ app.setPluginError(`False to create ${dir} by Operation timed out`);
327
+ res = false;
328
+ break;
329
+ }
330
+ }
331
+ }
332
+ return res;
333
+ } // end function createDir
334
+
335
+ async function interval() {
336
+ const now = Date.now();
337
+ const twentyFourHrs = 24 * 3600 * 1000;
338
+
339
+ if (options.ping && (!lastSentTime || now - lastSentTime >= twentyFourHrs)) {
340
+ app.debug('24 hrs elapsed since last send, pushing periodic fix');
341
+ if (await testInternet()) {
342
+ await sendData();
343
+ lastSentTime = now;
344
+ }
345
+ return;
346
+ }
347
+
348
+ if (checkBoatMoving() && await checkTrack() && await testInternet()) {
349
+ await sendData();
350
+ lastSentTime = now;
351
+ }
352
+ }
353
+
354
+
355
+
356
+
357
+
358
+ function checkBoatMoving() {
359
+ if (options.sendWhileMoving || !options.trackFrequency) {
360
+ return true;
361
+ }
362
+ const time = lastPosition ? lastPosition.currentTime : upSince;
363
+
364
+ const secsSinceLastPoint = (new Date().getTime() - time) / 1000
365
+ if (secsSinceLastPoint > (options.trackFrequency * 2)) {
366
+ app.debug('Boat stopped moving, last move at least', secsSinceLastPoint, 'seconds ago');
367
+ return true;
368
+ } else {
369
+ app.debug('Boat is still moving, last move', secsSinceLastPoint, 'seconds ago');
370
+ return false;
371
+ }
372
+ }
373
+
374
+ async function testInternet() {
375
+ app.debug('testing internet connection');
376
+ const check = await isReachable(internetTestAddress, { timeout: options.internetTestTimeout || internetTestTimeout });
377
+ app.debug('internet connection = ', check);
378
+ return check;
379
+ }
380
+
381
+ async function checkTrack() {
382
+ const trackFile = path.join(options.trackDir, routeSaveName);
383
+ app.debug('checking the track', trackFile, 'if should send');
384
+ const exists = await fs.pathExists(trackFile);
385
+ const size = exists ? (await fs.lstat(trackFile)).size : 0;
386
+ app.debug(`'${trackFile}'.size=${size} ${trackFile}'.exists=${exists}`);
387
+ return size > 0;
388
+ }
389
+
390
+ async function sendData() {
391
+ if (options.boatApiKey) {
392
+ sendApiData();
393
+ } else {
394
+ sendEmailData();
395
+ }
396
+ }
397
+
398
+ async function sendApiData() {
399
+ const url = options.apiUrl || defaultApiUrl;
400
+
401
+ app.debug('sending the data');
402
+ const trackData = await createTrack(path.join(options.trackDir, routeSaveName));
403
+ if (!trackData) {
404
+ app.debug('Recorded track did not contain any valid track points, aborting sending.');
405
+ return;
406
+ }
407
+ app.debug('created track data with timestamp:', new Date(trackData.timestamp));
408
+
409
+ const params = new URLSearchParams();
410
+ params.append('timestamp', trackData.timestamp);
411
+ params.append('track', JSON.stringify(trackData.track));
412
+ params.append('boatApiKey', options.boatApiKey);
413
+
414
+ const headers = {
415
+ 'X-NFL-API-Key': pluginApiKey
416
+ }
417
+
418
+ app.debug('sending track to API');
419
+ try {
420
+ const response = await fetch(url, { method: 'POST', body: params, headers: new fetch.Headers(headers) });
421
+ if (response.ok) {
422
+ const responseBody = await response.json();
423
+ if (responseBody.status === 'ok') {
424
+ app.debug('Track successfully sent to API');
425
+ if (options.keepFiles) {
426
+ const filename = new Date().toJSON().slice(0, 19).replace(/:/g, '') + '-nfl-track.jsonl';
427
+ app.debug('moving and keeping track file: ', filename);
428
+ await fs.move(path.join(options.trackDir, routeSaveName), path.join(options.trackDir, filename));
429
+ } else {
430
+ app.debug('Deleting track file');
431
+ await fs.remove(path.join(options.trackDir, routeSaveName));
432
+ }
433
+ } else {
434
+ app.debug('Could not send track to API, returned response json:', responseBody);
435
+ }
436
+ } else {
437
+ app.debug('Could not send track to API, returned response code:', response.status, response.statusText);
438
+ }
439
+ } catch (err) {
440
+ app.debug('Could not send track to API due to error:', err);
441
+ }
442
+ }
443
+
444
+ async function createTrack(inputPath) {
445
+ const fileStream = fs.createReadStream(inputPath);
446
+
447
+ const rl = readline.createInterface({
448
+ input: fileStream,
449
+ crlfDelay: Infinity
450
+ });
451
+ const track = []
452
+ let lastTimestamp;
453
+ for await (const line of rl) {
454
+ if (line) {
455
+ try {
456
+ const point = JSON.parse(line);
457
+
458
+ const timestamp = new Date(point.t).getTime();
459
+ if (!isNaN(timestamp) && isValidLatitude(point.lat) && isValidLongitude(point.lon)) {
460
+ track.push([timestamp, point.lat, point.lon]);
461
+ lastTimestamp = timestamp;
462
+ }
463
+
464
+ } catch (error) {
465
+ app.debug('could not parse line from track file:', line);
466
+ }
467
+ }
468
+ }
469
+ if (track.length > 0) {
470
+ return { timestamp: new Date(lastTimestamp).getTime(), track };
471
+ }
472
+ }
473
+
474
+ async function sendEmailData() {
475
+ app.debug('sending the data');
476
+ const gpxFiles = await createGPX({ input: path.join(options.trackDir, routeSaveName), outputDir: options.trackDir, creator });
477
+ app.debug('created GPX files', gpxFiles);
478
+ try {
479
+ for (let file of gpxFiles) {
480
+ app.debug('sending', file);
481
+ try {
482
+ !await sendEmail({
483
+ emailService: options.emailService,
484
+ user: options.emailUser,
485
+ password: options.emailPassword,
486
+ from: options.emailFrom,
487
+ to: options.emailTo,
488
+ trackFile: file
489
+ })
490
+ } catch (err) {
491
+ app.debug('Sending email failed:', err);
492
+ return;
493
+ }
494
+ }
495
+ } finally {
496
+ for (let file of gpxFiles) {
497
+ app.debug('deleting', file);
498
+ await fs.rm(file);
499
+ }
414
500
  }
415
-
416
- } catch (error) {
417
- app.debug('could not parse line from track file:', line);
418
- }
419
- }
420
- }
421
- if (track.length > 0) {
422
- return { timestamp: new Date(lastTimestamp).getTime(), track };
423
- }
424
- }
425
-
426
- async function sendEmailData() {
427
- app.debug('sending the data');
428
- const gpxFiles = await createGPX({ input: path.join(options.trackDir, routeSaveName), outputDir: options.trackDir, creator });
429
- app.debug('created GPX files', gpxFiles);
430
- try {
431
- for (let file of gpxFiles) {
432
- app.debug('sending', file);
433
- try {
434
- !await sendEmail({
435
- emailService: options.emailService,
436
- user: options.emailUser,
437
- password: options.emailPassword,
438
- from: options.emailFrom,
439
- to: options.emailTo,
440
- trackFile: file
441
- })
442
- } catch (err) {
443
- app.debug('Sending email failed:', err);
444
- return;
445
- }
446
- }
447
- } finally {
448
- for (let file of gpxFiles) {
449
- app.debug('deleting', file);
450
- await fs.rm(file);
451
- }
452
- }
453
- await fs.rm(path.join(options.trackDir, routeSaveName));
454
- }
455
- //every 10 minute but staggered to the second so we don't all send at once.
456
- if (!options.emailCron || options.emailCron === '*/10 * * * *') {
457
- const startMinute = Math.floor(Math.random() * 10); // Random minute within the 10-minute range
458
- const startSecond = Math.floor(Math.random() * 60); // Random second within the minute
459
- options.emailCron = `${startSecond} ${startMinute}/10 * * * *`; // Every 10 minutes, starting at a random minute and second within each 10-minute block
460
- }
461
-
462
- upSince = new Date().getTime();
463
-
464
- app.debug('Setting CRON to ', options.emailCron);
465
- cron = new CronJob(
466
- options.emailCron,
467
- interval
468
- );
469
- cron.start();
470
- }; // end plugin.start
471
-
472
- plugin.stop = function () {
473
- app.debug('plugin stopped');
474
- if (cron) {
475
- cron.stop();
476
- cron = undefined;
477
- }
478
- unsubscribesControl.forEach(f => f());
479
- unsubscribesControl = [];
480
- unsubscribes.forEach(f => f());
481
- unsubscribes = [];
482
- app.setPluginStatus('Plugin stopped');
483
- }; // end plugin.stop
484
-
485
-
486
- return plugin;
501
+ await fs.rm(path.join(options.trackDir, routeSaveName));
502
+ }
503
+ //every 10 minute but staggered to the second so we don't all send at once.
504
+ if (!options.emailCron || options.emailCron === '*/10 * * * *') {
505
+ const startMinute = Math.floor(Math.random() * 10); // Random minute within the 10-minute range
506
+ const startSecond = Math.floor(Math.random() * 60); // Random second within the minute
507
+ options.emailCron = `${startSecond} ${startMinute}/10 * * * *`; // Every 10 minutes, starting at a random minute and second within each 10-minute block
508
+ }
509
+
510
+ upSince = new Date().getTime();
511
+
512
+ app.debug('Setting CRON to ', options.emailCron);
513
+ cron = new CronJob(
514
+ options.emailCron,
515
+ interval
516
+ );
517
+ cron.start();
518
+ }; // end plugin.start
519
+
520
+ plugin.stop = function() {
521
+ app.debug('plugin stopped');
522
+ if (cron) {
523
+ cron.stop();
524
+ cron = undefined;
525
+ }
526
+ unsubscribesControl.forEach(f => f());
527
+ unsubscribesControl = [];
528
+ unsubscribes.forEach(f => f());
529
+ unsubscribes = [];
530
+ app.setPluginStatus('Plugin stopped');
531
+ }; // end plugin.stop
532
+
533
+
534
+ return plugin;
487
535
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-to-noforeignland",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "SignalK track logger to noforeignland.com ",
5
5
  "main": "index.js",
6
6
  "keywords": [