@noforeignland/signalk-to-noforeignland 1.0.1-beta.5 → 1.1.0-beta.2

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.
package/index.js CHANGED
@@ -1,195 +1,47 @@
1
- const { EOL } = require('os');
2
- const fs = require('fs-extra');
3
- const path = require('path');
4
1
  const CronJob = require('cron').CronJob;
5
- const readline = require('readline');
6
- const fetch = require('node-fetch');
7
2
 
8
- const apiUrl = 'https://www.noforeignland.com/home/api/v1/boat/tracking/track';
9
- const pluginApiKey = '0ede6cb6-5213-45f5-8ab4-b4836b236f97';
10
- const defaultTracksDir = 'nfl-track';
11
- const routeSaveName = 'pending.jsonl';
12
- const routeSentName = 'sent.jsonl';
3
+ // Import all modules
4
+ const ConfigManager = require('./lib/ConfigManager');
5
+ const PluginCleanup = require('./lib/PluginCleanup');
6
+ const TrackMigration = require('./lib/TrackMigration');
7
+ const TrackLogger = require('./lib/TrackLogger');
8
+ const TrackSender = require('./lib/TrackSender');
9
+ const HealthMonitor = require('./lib/HealthMonitor');
10
+ const DataPathEmitter = require('./lib/DataPathEmitter');
11
+ const DirectoryUtils = require('./lib/DirectoryUtils');
13
12
 
14
13
  class SignalkToNoforeignland {
15
14
  constructor(app) {
16
15
  this.app = app;
17
16
  this.pluginId = 'signalk-to-noforeignland';
18
17
  this.pluginName = 'Signal K to Noforeignland';
19
- this.creator = 'signalk-track-logger';
20
18
 
21
- // runtime state
22
- this.unsubscribes = [];
23
- this.unsubscribesControl = [];
24
- this.lastPosition = null;
19
+ // Runtime state
20
+ this.options = {};
25
21
  this.upSince = null;
26
22
  this.cron = null;
27
- this.options = {};
28
23
  this.lastSuccessfulTransfer = null;
29
24
 
30
- // Track status for data path updates
31
- this.currentStatus = '';
32
- this.currentError = null;
33
-
34
- // Track auto-selected source
35
- this.autoSelectedSource = null;
36
- }
37
-
38
- // Emit SignalK deltas for data paths
39
- emitDelta(path, value) {
40
- try {
41
- const delta = {
42
- context: 'vessels.self',
43
- updates: [{
44
- timestamp: new Date().toISOString(),
45
- values: [{
46
- path: path,
47
- value: value
48
- }]
49
- }]
50
- };
51
- this.app.handleMessage(this.pluginId, delta);
52
- } catch (err) {
53
- this.app.debug(`Failed to emit delta for ${path}:`, err.message);
54
- }
55
- }
56
-
57
- updateStatusPaths() {
58
- const hasError = this.currentError !== null;
59
-
60
- // SHORT format for data path
61
- if (!hasError) {
62
- const activeSource = this.options.filterSource || this.autoSelectedSource || '';
63
- const saveTime = this.lastPosition ? new Date(this.lastPosition.currentTime).toLocaleTimeString() : 'None since start';
64
- const transferTime = this.lastSuccessfulTransfer ? this.lastSuccessfulTransfer.toLocaleTimeString() : 'None since start';
65
- const shortStatus = `Save: ${saveTime} | Transfer: ${transferTime}`;
66
- this.emitDelta('noforeignland.status', shortStatus);
67
- this.emitDelta('noforeignland.source', activeSource);
68
- } else {
69
- this.emitDelta('noforeignland.status', `ERROR: ${this.currentError}`);
70
- }
71
- this.emitDelta('noforeignland.status_boolean', hasError ? 1 : 0);
72
- }
73
-
74
- // Override setPluginStatus to also emit data path
75
- setPluginStatus(status) {
76
- this.currentStatus = status;
77
- this.currentError = null;
78
- this.app.setPluginStatus(status);
79
- this.updateStatusPaths();
80
- }
81
-
82
- // Override setPluginError to also emit data path
83
- setPluginError(error) {
84
- this.currentError = error;
85
- this.app.setPluginError(error);
86
- this.updateStatusPaths();
25
+ // Module instances
26
+ this.configManager = new ConfigManager(app);
27
+ this.pluginCleanup = new PluginCleanup(app);
28
+ this.dataPathEmitter = new DataPathEmitter(app, this.pluginId);
29
+ this.trackLogger = null;
30
+ this.trackSender = null;
31
+ this.healthMonitor = null;
32
+ this.trackMigration = null;
87
33
  }
88
34
 
35
+ /**
36
+ * Get plugin schema
37
+ */
89
38
  getSchema() {
90
- return {
91
- title: this.pluginName,
92
- description: 'Some parameters need for use',
93
- type: 'object',
94
- properties: {
95
- // Mandatory Settings Group
96
- mandatory: {
97
- type: 'object',
98
- title: 'Mandatory Settings',
99
- properties: {
100
- boatApiKey: {
101
- type: 'string',
102
- title: 'Boat API Key',
103
- description: 'Boat API Key from noforeignland.com. Can be found in Account > Settings > Boat tracking > API Key.'
104
- }
105
- }
106
- },
107
-
108
- // Advanced Settings Group
109
- advanced: {
110
- type: 'object',
111
- title: 'Advanced Settings',
112
- properties: {
113
- minMove: {
114
- type: 'number',
115
- title: 'Minimum boat move to log in meters',
116
- description: 'To keep file sizes small we only log positions if a move larger than this size (if set to 0 will log every move)',
117
- default: 80
118
- },
119
- minSpeed: {
120
- type: 'number',
121
- title: 'Minimum boat speed to log in knots',
122
- 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)',
123
- default: 1.5
124
- },
125
- sendWhileMoving: {
126
- type: 'boolean',
127
- title: 'Attempt sending location while moving',
128
- description: 'Should the plugin attempt to send tracking data to NFL while detecting the vessel is moving or only when stopped?',
129
- default: true
130
- },
131
- ping_api_every_24h: {
132
- type: 'boolean',
133
- title: 'Force a send every 24 hours',
134
- description: 'Keeps your boat active on NFL in your current location even if you do not move',
135
- default: true
136
- }
137
- }
138
- },
139
-
140
- // Expert Settings Group
141
- expert: {
142
- type: 'object',
143
- title: 'Expert Settings',
144
- properties: {
145
- filterSource: {
146
- type: 'string',
147
- title: 'Position source device',
148
- description: 'EMPTY DEFAULT IS FINE - Set this value to the name of a source if you want to only use the position given by that source.'
149
- },
150
- trackDir: {
151
- type: 'string',
152
- title: 'Directory to cache tracks',
153
- description: 'EMPTY DEFAULT IS FINE - Path to store track data. Relative paths are stored in plugin data directory. Absolute paths can point anywhere.\nDefault: nfl-track'
154
- },
155
- keepFiles: {
156
- type: 'boolean',
157
- title: 'Keep track files on disk',
158
- description: 'If you have a lot of hard drive space you can keep the track files for logging purposes.',
159
- default: false
160
- },
161
- trackFrequency: {
162
- type: 'integer',
163
- title: 'Position tracking frequency in seconds',
164
- description: 'To keep file sizes small we only log positions once in a while (unless you set this value to 0)',
165
- default: 60
166
- },
167
- apiCron: {
168
- type: 'string',
169
- title: 'Send attempt CRON',
170
- description: 'We send the tracking data to NFL once in a while, you can set the schedule with this setting.\nCRON format: https://crontab.guru/',
171
- default: '*/10 * * * *'
172
- },
173
- internetTestTimeout: {
174
- type: 'number',
175
- title: 'Timeout for testing internet connection in ms',
176
- description: 'Set this number higher for slower computers and internet connections',
177
- default: 2000
178
- },
179
- apiTimeout: {
180
- type: 'integer',
181
- title: 'API request timeout in seconds',
182
- description: 'Timeout for sending data to NFL API. Increase for slow connections.',
183
- default: 30,
184
- minimum: 10,
185
- maximum: 180
186
- }
187
- }
188
- }
189
- }
190
- };
39
+ return ConfigManager.getSchema(this.pluginName);
191
40
  }
192
41
 
42
+ /**
43
+ * Get plugin object for SignalK
44
+ */
193
45
  getPluginObject() {
194
46
  return {
195
47
  id: this.pluginId,
@@ -201,749 +53,248 @@ class SignalkToNoforeignland {
201
53
  };
202
54
  }
203
55
 
56
+ /**
57
+ * Start the plugin
58
+ */
204
59
  async start(options = {}, restartPlugin) {
205
- // Position data health check
206
- this.positionCheckInterval = null;
207
- this.lastPositionReceived = null;
208
-
209
- // Backward compatibility: migrate old flat structure to new nested structure
210
- let needsSave = false;
211
- if (options.boatApiKey && !options.mandatory) {
212
- this.app.debug('Migrating old configuration to new grouped structure');
213
- needsSave = true;
60
+ try {
61
+ // 1. Migrate old config structure if needed
62
+ const { options: migratedOptions, migrated } = await this.configManager.migrateOldConfig(options);
63
+ options = migratedOptions;
214
64
 
215
- options = {
216
- mandatory: {
217
- boatApiKey: options.boatApiKey
218
- },
219
- advanced: {
220
- minMove: options.minMove !== undefined ? options.minMove : 50,
221
- minSpeed: options.minSpeed !== undefined ? options.minSpeed : 1.5,
222
- sendWhileMoving: options.sendWhileMoving !== undefined ? options.sendWhileMoving : true,
223
- ping_api_every_24h: options.ping_api_every_24h !== undefined ? options.ping_api_every_24h : true
224
- },
225
- expert: {
226
- filterSource: options.filterSource,
227
- trackDir: options.trackDir,
228
- keepFiles: options.keepFiles !== undefined ? options.keepFiles : false,
229
- trackFrequency: options.trackFrequency !== undefined ? options.trackFrequency : 60,
230
- internetTestTimeout: options.internetTestTimeout !== undefined ? options.internetTestTimeout : 2000,
231
- apiCron: options.apiCron || '*/10 * * * *',
232
- apiTimeout: options.apiTimeout !== undefined ? options.apiTimeout : 30
233
- }
234
- };
65
+ // 2. Flatten config and apply defaults
66
+ this.options = this.configManager.flattenConfig(options);
235
67
 
68
+ // 3. Validate API key
236
69
  try {
237
- this.app.debug('Saving migrated configuration...');
238
- await this.app.savePluginOptions(options, () => {
239
- this.app.debug('Configuration successfully migrated and saved');
240
- });
70
+ this.configManager.validateApiKey(this.options);
241
71
  } catch (err) {
242
- this.app.debug('Failed to save migrated configuration:', err.message);
72
+ this.app.debug(err.message);
73
+ this.setPluginError(err.message);
74
+ this.stop();
75
+ return;
243
76
  }
244
- }
245
-
246
- // Flatten the nested structure for easier access and apply defaults
247
- this.options = {
248
- boatApiKey: options.mandatory?.boatApiKey,
249
- minMove: options.advanced?.minMove !== undefined ? options.advanced.minMove : 80,
250
- minSpeed: options.advanced?.minSpeed !== undefined ? options.advanced.minSpeed : 1.5,
251
- sendWhileMoving: options.advanced?.sendWhileMoving !== undefined ? options.advanced.sendWhileMoving : true,
252
- ping_api_every_24h: options.advanced?.ping_api_every_24h !== undefined ? options.advanced.ping_api_every_24h : true,
253
- filterSource: options.expert?.filterSource,
254
- trackDir: options.expert?.trackDir || defaultTracksDir,
255
- keepFiles: options.expert?.keepFiles !== undefined ? options.expert.keepFiles : false,
256
- trackFrequency: options.expert?.trackFrequency !== undefined ? options.expert.trackFrequency : 60,
257
- internetTestTimeout: options.expert?.internetTestTimeout !== undefined ? options.expert.internetTestTimeout : 2000,
258
- apiCron: options.expert?.apiCron || '*/10 * * * *',
259
- apiTimeout: options.expert?.apiTimeout !== undefined ? options.expert.apiTimeout : 30
260
- };
261
-
262
- // Validate that boatApiKey is set
263
- if (!this.options.boatApiKey || this.options.boatApiKey.trim() === '') {
264
- const errorMsg = 'No boat API key configured. Please set your API key in plugin settings (Mandatory Settings > Boat API key). You can find your API key at noforeignland.com under Account > Settings > Boat tracking > API Key.';
265
- this.app.debug(errorMsg);
266
- this.setPluginError(errorMsg);
267
- this.stop();
268
- return;
269
- }
270
-
271
- // Resolve track directory path
272
- if (!path.isAbsolute(this.options.trackDir)) {
77
+
78
+ // 4. Resolve track directory path
273
79
  const dataDirPath = this.app.getDataDirPath();
274
- this.options.trackDir = path.join(dataDirPath, this.options.trackDir);
275
- }
276
-
277
- if (!this.createDir(this.options.trackDir)) {
80
+ this.options.trackDir = this.configManager.resolveTrackDir(this.options, dataDirPath);
81
+
82
+ // 5. Create track directory
83
+ try {
84
+ DirectoryUtils.createDir(this.options.trackDir, this.app);
85
+ } catch (err) {
86
+ this.setPluginError(err.message);
87
+ this.stop();
88
+ return;
89
+ }
90
+
91
+ // 6. Cleanup old plugin versions (with callback handling)
92
+ this.pluginCleanup.cleanup()
93
+ .then((result) => {
94
+ if (result === 'all_removed') {
95
+ this.app.debug('Old plugins successfully cleaned up');
96
+ // Update status if plugin started successfully
97
+ if (this.options.boatApiKey) {
98
+ this.setPluginStatus('Started (old plugins cleaned up)');
99
+ }
100
+ }
101
+ })
102
+ .catch(err => {
103
+ this.app.debug('Error in cleanupOldPlugin:', err.message);
104
+ });
105
+
106
+ // 7. Migrate old track files
107
+ this.trackMigration = new TrackMigration(this.app, this.options.trackDir);
108
+ await this.trackMigration.migrate();
109
+
110
+ // 8. Initialize modules
111
+ this.trackLogger = new TrackLogger(this.app, this.options, this.options.trackDir);
112
+ this.trackSender = new TrackSender(this.app, this.options, this.options.trackDir);
113
+ this.healthMonitor = new HealthMonitor(this.app, this.options);
114
+
115
+ // 9. Update startup time and randomize CRON
116
+ this.upSince = new Date().getTime();
117
+ this.options = this.configManager.randomizeCron(this.options);
118
+
119
+ this.app.debug('Setting CRON to', this.options.apiCron);
120
+ this.app.debug('trackFrequency is set to', this.options.trackFrequency, 'seconds');
121
+ this.app.debug('track logger started, now logging to', this.options.trackDir);
122
+
123
+ // 10. Start logging
124
+ this.trackLogger.startLogging((lastPosition) => {
125
+ this.handleSavePoint(lastPosition);
126
+ });
127
+
128
+ // 11. Start CRON job for sending data
129
+ this.cron = new CronJob(this.options.apiCron, this.interval.bind(this));
130
+ this.cron.start();
131
+
132
+ // 12. Start health monitoring
133
+ this.healthMonitor.start(
134
+ () => this.trackLogger.getLastPositionReceived(),
135
+ () => this.trackLogger.getAutoSelectedSource(),
136
+ (errorMsg) => this.setPluginError(errorMsg),
137
+ () => this.handleHealthy()
138
+ );
139
+
140
+ // 13. Set plugin status
141
+ this.setPluginStatus(`Started${migrated ? ' (config migrated)' : ''}`);
142
+
143
+ } catch (err) {
144
+ this.app.debug('Error during plugin start:', err.message);
145
+ this.setPluginError(`Failed to start: ${err.message}`);
278
146
  this.stop();
279
- return;
280
- }
281
-
282
- // NEW: Cleanup old plugin
283
- await this.cleanupOldPlugin();
284
-
285
- // Migrate old track files
286
- await this.migrateOldTrackFile();
287
-
288
- this.app.debug('track logger started, now logging to', this.options.trackDir);
289
- this.setPluginStatus(`Started${needsSave ? ' (config migrated)' : ''}`);
290
- this.upSince = new Date().getTime();
291
-
292
- // Adjust default CRON if unchanged
293
- if (!this.options.apiCron || this.options.apiCron === '*/10 * * * *') {
294
- const startMinute = Math.floor(Math.random() * 10);
295
- const startSecond = Math.floor(Math.random() * 60);
296
- this.options.apiCron = `${startSecond} ${startMinute}/10 * * * *`;
297
147
  }
298
-
299
- this.app.debug('Setting CRON to', this.options.apiCron);
300
- this.app.debug('trackFrequency is set to', this.options.trackFrequency, 'seconds');
301
-
302
- // Subscribe and start logging
303
- this.doLogging();
304
-
305
- // Start cron job
306
- this.cron = new CronJob(this.options.apiCron, this.interval.bind(this));
307
- this.cron.start();
308
-
309
- // Start position health check
310
- this.startPositionHealthCheck();
311
148
  }
312
149
 
313
- async cleanupOldPlugin() {
314
- try {
315
- // 1. Config Migration
316
- const configDir = process.env.SIGNALK_NODE_CONFIG_DIR ||
317
- path.join(process.env.HOME || process.env.USERPROFILE, '.signalk');
318
- const configPath = path.join(configDir, 'plugin-config-data');
319
- const oldConfigFile = path.join(configPath, 'signalk-to-noforeignland.json');
320
- const newConfigFile = path.join(configPath, '@noforeignland-signalk-to-noforeignland.json');
321
-
322
- if (fs.existsSync(oldConfigFile) && !fs.existsSync(newConfigFile)) {
323
- this.app.debug('Migrating configuration from old plugin...');
324
- fs.copyFileSync(oldConfigFile, newConfigFile);
325
- fs.copyFileSync(oldConfigFile, `${oldConfigFile}.backup`);
326
- this.app.debug('✓ Configuration migrated successfully');
327
- }
150
+ /**
151
+ * Handle savepoint event
152
+ */
153
+ handleSavePoint(lastPosition) {
154
+ const now = new Date();
155
+ this.dataPathEmitter.emitSavepoint();
328
156
 
329
- // 2. Check if old plugin still exists
330
- const oldPluginDir = path.join(configDir, 'node_modules', 'signalk-to-noforeignland');
157
+ // Update plugin status
158
+ const activeSource = this.options.filterSource || this.trackLogger.getAutoSelectedSource() || '';
159
+ const sourcePrefix = activeSource ? `${activeSource} | ` : '';
160
+ const saveTime = now.toISOString();
161
+ const transferTime = this.lastSuccessfulTransfer
162
+ ? this.lastSuccessfulTransfer.toISOString()
163
+ : 'None since start';
331
164
 
332
- if (fs.existsSync(oldPluginDir)) {
333
- this.app.debug('Old plugin "signalk-to-noforeignland" detected');
334
- this.app.setPluginError(
335
- 'Old plugin "signalk-to-noforeignland" is still installed. ' +
336
- 'Please uninstall it manually: cd ~/.signalk && npm uninstall signalk-to-noforeignland'
337
- );
165
+ this.setPluginStatus(`Save: ${saveTime} | Transfer: ${transferTime} | ${sourcePrefix}`);
166
+ }
167
+
168
+ /**
169
+ * Handle healthy status from health monitor
170
+ */
171
+ handleHealthy() {
172
+ // Clear error if position health is OK
173
+ if (this.dataPathEmitter.getError()) {
174
+ const lastPosition = this.trackLogger.getLastPosition();
175
+ const activeSource = this.options.filterSource || this.trackLogger.getAutoSelectedSource() || '';
176
+ const sourcePrefix = activeSource ? `${activeSource} | ` : '';
177
+ const saveTime = lastPosition
178
+ ? new Date(lastPosition.currentTime).toISOString()
179
+ : 'None since start';
180
+ const transferTime = this.lastSuccessfulTransfer
181
+ ? this.lastSuccessfulTransfer.toISOString()
182
+ : 'None since start';
338
183
 
339
- // Try to remove it after a delay (non-blocking)
340
- setTimeout(async () => {
341
- try {
342
- this.app.debug('Attempting to remove old plugin directory...');
343
- await fs.remove(oldPluginDir);
344
- this.app.debug('✓ Old plugin directory removed');
345
- // Clear error if removal successful
346
- this.setPluginStatus('Started (old plugin cleaned up)');
347
- } catch (err) {
348
- this.app.debug('Could not automatically remove old plugin:', err.message);
349
- this.app.debug('Please manually run: npm uninstall signalk-to-noforeignland');
350
- }
351
- }, 5000); // 5 Sekunden warten bis SignalK vollständig gestartet ist
184
+ this.setPluginStatus(`Save: ${saveTime} | Transfer: ${transferTime} | ${sourcePrefix}`);
352
185
  }
353
-
354
- } catch (err) {
355
- this.app.debug('Error during old plugin cleanup:', err.message);
356
186
  }
357
- }
358
187
 
359
-
360
- async migrateOldTrackFile() {
361
- const oldTrackFile = path.join(this.options.trackDir, 'nfl-track.jsonl');
362
- const oldPendingFile = path.join(this.options.trackDir, 'nfl-track-pending.jsonl');
363
- const oldSentFile = path.join(this.options.trackDir, 'nfl-track-sent.jsonl');
364
- const newPendingFile = path.join(this.options.trackDir, routeSaveName);
365
- const newSentFile = path.join(this.options.trackDir, routeSentName);
366
-
188
+ /**
189
+ * CRON interval - check and send data
190
+ */
191
+ async interval() {
367
192
  try {
368
- // Migrate old track file
369
- if (await fs.pathExists(oldTrackFile) && !(await fs.pathExists(newPendingFile))) {
370
- this.app.debug('Migrating old track file to new naming scheme...');
371
- await fs.move(oldTrackFile, newPendingFile);
372
- this.app.debug('Successfully migrated old track file to:', routeSaveName);
193
+ // Check if boat is moving
194
+ const lastPosition = this.trackLogger.getLastPosition();
195
+ const boatMoving = this.trackSender.isBoatMoving(lastPosition, this.upSince);
196
+ if (!boatMoving) {
197
+ return;
373
198
  }
374
199
 
375
- // Migrate old pending file
376
- if (await fs.pathExists(oldPendingFile) && !(await fs.pathExists(newPendingFile))) {
377
- this.app.debug('Migrating old pending file to new naming scheme...');
378
- await fs.move(oldPendingFile, newPendingFile);
379
- this.app.debug('Successfully migrated old pending file to:', routeSaveName);
200
+ // Check if we have track data
201
+ const hasTrack = await this.trackSender.hasTrackData();
202
+ if (!hasTrack) {
203
+ return;
380
204
  }
381
205
 
382
- // Migrate old sent file
383
- if (await fs.pathExists(oldSentFile) && !(await fs.pathExists(newSentFile))) {
384
- this.app.debug('Migrating old sent file to new naming scheme...');
385
- await fs.move(oldSentFile, newSentFile);
386
- this.app.debug('Successfully migrated old sent file to:', routeSentName);
206
+ // Test internet connection
207
+ const hasInternet = await this.trackSender.testInternet();
208
+ if (!hasInternet) {
209
+ const errorMsg = 'No internet connection detected. Unable to send tracking data to NFL. DNS lookups failed - check your internet connection.';
210
+ this.app.debug(errorMsg);
211
+ this.setPluginError(errorMsg);
212
+ return;
387
213
  }
388
214
 
389
- // Check old plugin directory location
390
- const oldPluginTrackDir = path.join(__dirname, 'track');
391
- if (await fs.pathExists(oldPluginTrackDir)) {
392
- this.app.debug('Found old track directory in plugin folder, migrating to new location...');
393
-
394
- const oldFiles = [
395
- 'nfl-track.jsonl',
396
- 'nfl-track-pending.jsonl',
397
- routeSaveName
398
- ];
399
-
400
- for (const oldFile of oldFiles) {
401
- const oldPath = path.join(oldPluginTrackDir, oldFile);
402
- if (await fs.pathExists(oldPath) && !(await fs.pathExists(newPendingFile))) {
403
- await fs.move(oldPath, newPendingFile);
404
- this.app.debug('Migrated pending track file from old plugin location');
405
- break;
406
- }
407
- }
215
+ // Send track data
216
+ const success = await this.trackSender.sendTrack();
217
+
218
+ if (success) {
219
+ this.lastSuccessfulTransfer = new Date();
220
+ this.dataPathEmitter.emitApiTransfer(this.lastSuccessfulTransfer);
408
221
 
409
- // Migrate sent archive
410
- const oldSentFiles = [routeSentName, 'nfl-track-sent.jsonl'];
411
- for (const oldFile of oldSentFiles) {
412
- const oldPath = path.join(oldPluginTrackDir, oldFile);
413
- if (await fs.pathExists(oldPath) && !(await fs.pathExists(newSentFile))) {
414
- await fs.move(oldPath, newSentFile);
415
- this.app.debug('Migrated sent track archive from old plugin location');
416
- break;
417
- }
418
- }
222
+ // Update status
223
+ const activeSource = this.options.filterSource || this.trackLogger.getAutoSelectedSource() || '';
224
+ const sourcePrefix = activeSource ? `${activeSource} | ` : '';
225
+ const saveTime = lastPosition
226
+ ? new Date(lastPosition.currentTime).toISOString()
227
+ : 'None since start';
228
+ const transferTime = this.lastSuccessfulTransfer.toISOString();
419
229
 
420
- // Try to remove old directory if empty
421
- try {
422
- const remainingFiles = await fs.readdir(oldPluginTrackDir);
423
- if (remainingFiles.length === 0) {
424
- await fs.rmdir(oldPluginTrackDir);
425
- this.app.debug('Removed empty old track directory');
426
- }
427
- } catch (err) {
428
- this.app.debug('Could not remove old track directory:', err.message);
429
- }
230
+ this.setPluginStatus(`Save: ${saveTime} | Transfer: ${transferTime} | ${sourcePrefix}`);
430
231
  }
232
+
431
233
  } catch (err) {
432
- this.app.debug('Error during track file migration:', err.message);
234
+ this.app.debug('Error during send interval:', err.message);
235
+ this.setPluginError(`Failed to send track - ${err.message}`);
433
236
  }
434
237
  }
435
238
 
239
+ /**
240
+ * Stop the plugin
241
+ */
436
242
  stop() {
437
243
  this.app.debug('plugin stopped');
438
244
 
439
- this.autoSelectedSource = null;
440
-
441
- if (this.positionCheckInterval) {
442
- clearInterval(this.positionCheckInterval);
443
- this.positionCheckInterval = null;
444
- }
445
-
245
+ // Stop CRON job
446
246
  if (this.cron) {
447
247
  this.cron.stop();
448
248
  this.cron = undefined;
449
249
  }
450
250
 
451
- this.unsubscribesControl.forEach(f => f());
452
- this.unsubscribesControl = [];
453
- this.unsubscribes.forEach(f => f());
454
- this.unsubscribes = [];
455
- this.app.setPluginStatus('Plugin stopped');
456
- }
457
-
458
- doLogging() {
459
- let shouldDoLog = true;
460
-
461
- this.app.subscriptionmanager.subscribe({
462
- context: 'vessels.self',
463
- subscribe: [{
464
- path: 'navigation.position',
465
- format: 'delta',
466
- policy: 'instant',
467
- minPeriod: this.options.trackFrequency ? this.options.trackFrequency * 1000 : 0
468
- }]
469
- }, this.unsubscribes, (subscriptionError) => {
470
- this.app.debug('Error subscription to data:' + subscriptionError);
471
- this.setPluginError('Error subscription to data:' + subscriptionError.message);
472
- }, this.doOnValue.bind(this, () => shouldDoLog, newShould => { shouldDoLog = newShould; }));
473
-
474
- // Subscribe for speed
475
- if (this.options.minSpeed) {
476
- this.app.subscriptionmanager.subscribe({
477
- context: 'vessels.self',
478
- subscribe: [{
479
- path: 'navigation.speedOverGround',
480
- format: 'delta',
481
- policy: 'instant'
482
- }]
483
- }, this.unsubscribes, (subscriptionError) => {
484
- this.app.debug('Error subscription to data:' + subscriptionError);
485
- this.setPluginError('Error subscription to data:' + subscriptionError.message);
486
- }, (delta) => {
487
- delta.updates.forEach(update => {
488
- if (this.options.filterSource && update.$source !== this.options.filterSource) {
489
- return;
490
- }
491
- update.values.forEach(value => {
492
- const speedInKnots = value.value * 1.94384;
493
- if (!shouldDoLog && this.options.minSpeed < speedInKnots) {
494
- this.app.debug('setting shouldDoLog to true, speed:', speedInKnots.toFixed(2), 'knots');
495
- shouldDoLog = true;
496
- }
497
- });
498
- });
499
- });
500
- }
501
- }
502
-
503
- // FIXED: Use continue instead of return to handle multiple updates properly
504
- async doOnValue(getShouldDoLog, setShouldDoLog, delta) {
505
- for (const update of delta.updates) {
506
- // Auto-select source logic
507
- if (!this.options.filterSource) {
508
- const timeSinceLastPosition = this.lastPositionReceived
509
- ? (new Date().getTime() - this.lastPositionReceived) / 1000
510
- : null;
511
-
512
- if (!this.autoSelectedSource) {
513
- this.autoSelectedSource = update.$source;
514
- this.lastPositionReceived = new Date().getTime();
515
- this.app.debug(`Auto-selected GPS source: '${this.autoSelectedSource}'`);
516
- } else if (update.$source !== this.autoSelectedSource) {
517
- if (timeSinceLastPosition && timeSinceLastPosition > 300) {
518
- this.app.debug(`Switching from stale source '${this.autoSelectedSource}' to '${update.$source}' (no data for ${timeSinceLastPosition.toFixed(0)}s)`);
519
- this.autoSelectedSource = update.$source;
520
- this.lastPositionReceived = new Date().getTime();
521
- } else {
522
- this.app.debug(`Ignoring position from '${update.$source}', using auto-selected source '${this.autoSelectedSource}'`);
523
- continue;
524
- }
525
- } else {
526
- this.lastPositionReceived = new Date().getTime();
527
- }
528
- } else if (update.$source !== this.options.filterSource) {
529
- this.app.debug(`Ignoring position from '${update.$source}', filterSource is set to '${this.options.filterSource}'`);
530
- continue;
531
- } else {
532
- this.lastPositionReceived = new Date().getTime();
533
- }
534
-
535
- const timestamp = update.timestamp;
536
- for (const value of update.values) {
537
- // Validation: GPS near (0,0)
538
- if (Math.abs(value.value.latitude) <= 0.01 && Math.abs(value.value.longitude) <= 0.01) {
539
- this.app.debug('GPS coordinates near (0,0), ignoring point to avoid invalid data logging.');
540
- continue;
541
- }
542
-
543
- // Validate lat/lon
544
- if (!this.isValidLatitude(value.value.latitude) || !this.isValidLongitude(value.value.longitude)) {
545
- this.app.debug('got invalid position, ignoring...', value.value);
546
- continue;
547
- }
548
-
549
- // 24h ping check
550
- let force24hSave = false;
551
- if (this.options.ping_api_every_24h && this.lastPosition) {
552
- const timeSinceLastPoint = (new Date().getTime() - this.lastPosition.currentTime);
553
- if (timeSinceLastPoint >= 24 * 60 * 60 * 1000) {
554
- this.app.debug('24h since last point, forcing save of point to keep boat active on NFL');
555
- force24hSave = true;
556
- }
557
- }
558
-
559
- // Check if we should log
560
- if (!force24hSave && !getShouldDoLog()) {
561
- this.app.debug('shouldDoLog is false, not logging position');
562
- continue;
563
- }
564
-
565
- // Check timestamp and distance
566
- if (this.lastPosition && !force24hSave) {
567
- if (new Date(this.lastPosition.timestamp).getTime() > new Date(timestamp).getTime()) {
568
- this.app.debug('got error in timestamp:', timestamp, 'is earlier than previous:', this.lastPosition.timestamp);
569
- continue;
570
- }
571
-
572
- const distance = this.equirectangularDistance(this.lastPosition.pos, value.value);
573
- if (this.options.minMove && distance < this.options.minMove) {
574
- this.app.debug('Distance', distance.toFixed(2), 'm is less than minMove', this.options.minMove, 'm - skipping');
575
- continue;
576
- }
577
-
578
- this.app.debug('Distance', distance.toFixed(2), 'm is greater than minMove', this.options.minMove, 'm - logging');
579
- }
580
-
581
- // Save point
582
- this.app.debug('Saving position from source:', update.$source, 'lat:', value.value.latitude, 'lon:', value.value.longitude);
583
- this.lastPosition = { pos: value.value, timestamp, currentTime: new Date().getTime() };
584
- await this.savePoint(this.lastPosition);
585
-
586
- // Reset shouldDoLog if minSpeed is active
587
- if (this.options.minSpeed) {
588
- this.app.debug('options.minSpeed - setting shouldDoLog to false');
589
- setShouldDoLog(false);
590
- }
591
- }
592
- }
593
- }
594
-
595
- async savePoint(point) {
596
- const obj = {
597
- lat: point.pos.latitude,
598
- lon: point.pos.longitude,
599
- t: point.timestamp
600
- };
601
- this.app.debug(`save data point:`, obj);
602
- await fs.appendFile(path.join(this.options.trackDir, routeSaveName), JSON.stringify(obj) + EOL);
603
-
604
- const now = new Date();
605
- this.emitDelta('noforeignland.savepoint', now.toISOString());
606
- this.emitDelta('noforeignland.savepoint_local', now.toLocaleString());
607
-
608
- // ISO8601 format for Dashboard
609
- const activeSource = this.options.filterSource || this.autoSelectedSource || '';
610
- const sourcePrefix = activeSource ? `${activeSource} | ` : '';
611
- const saveTime = now.toISOString();
612
- const transferTime = this.lastSuccessfulTransfer ? this.lastSuccessfulTransfer.toISOString() : 'None since start';
613
-
614
- this.setPluginStatus(`Save: ${saveTime} | Transfer: ${transferTime} | ${sourcePrefix}`);
615
- }
616
-
617
- isValidLatitude(obj) {
618
- return this.isDefinedNumber(obj) && obj > -90 && obj < 90;
619
- }
620
-
621
- isValidLongitude(obj) {
622
- return this.isDefinedNumber(obj) && obj > -180 && obj < 180;
623
- }
624
-
625
- isDefinedNumber(obj) {
626
- return (obj !== undefined && obj !== null && typeof obj === 'number');
627
- }
628
-
629
- equirectangularDistance(from, to) {
630
- const rad = Math.PI / 180;
631
- const φ1 = from.latitude * rad;
632
- const φ2 = to.latitude * rad;
633
- const Δλ = (to.longitude - from.longitude) * rad;
634
- const R = 6371e3;
635
- const x = Δλ * Math.cos((φ1 + φ2) / 2);
636
- const y = (φ2 - φ1);
637
- const d = Math.sqrt(x * x + y * y) * R;
638
- return d;
639
- }
640
-
641
- createDir(dir) {
642
- let res = true;
643
- if (fs.existsSync(dir)) {
644
- try {
645
- fs.accessSync(dir, fs.constants.R_OK | fs.constants.W_OK);
646
- } catch (error) {
647
- this.app.debug('[createDir]', error.message);
648
- this.setPluginError(`No rights to directory ${dir}`);
649
- res = false;
650
- }
651
- } else {
652
- try {
653
- fs.mkdirSync(dir, { recursive: true });
654
- } catch (error) {
655
- switch (error.code) {
656
- case 'EACCES':
657
- case 'EPERM':
658
- this.app.debug(`Failed to create ${dir} by Permission denied`);
659
- this.setPluginError(`Failed to create ${dir} by Permission denied`);
660
- res = false;
661
- break;
662
- case 'ETIMEDOUT':
663
- this.app.debug(`Failed to create ${dir} by Operation timed out`);
664
- this.setPluginError(`Failed to create ${dir} by Operation timed out`);
665
- res = false;
666
- break;
667
- default:
668
- this.app.debug(`Error creating directory ${dir}: ${error.message}`);
669
- this.setPluginError(`Error creating directory ${dir}: ${error.message}`);
670
- res = false;
671
- }
672
- }
673
- }
674
- return res;
675
- }
676
-
677
- startPositionHealthCheck() {
678
- this.positionCheckInterval = setInterval(() => {
679
- const now = new Date().getTime();
680
- const timeSinceLastPosition = this.lastPositionReceived
681
- ? (now - this.lastPositionReceived) / 1000
682
- : null;
683
-
684
- const activeSource = this.options.filterSource || this.autoSelectedSource || 'any';
685
- const filterMsg = activeSource !== 'any' ? ` from source '${activeSource}'` : '';
686
-
687
- if (!this.lastPositionReceived) {
688
- const errorMsg = this.options.filterSource
689
- ? `No GPS position data received from filtered source '${this.options.filterSource}'. Check Expert Settings > Position source device, or leave empty to use any GPS source.`
690
- : 'No GPS position data received. Check that your GPS is connected and SignalK is receiving navigation.position data.';
691
- this.setPluginError(errorMsg);
692
- this.app.debug('Position health check: No position data ever received' + filterMsg);
693
- } else if (timeSinceLastPosition > 300) {
694
- const errorMsg = this.options.filterSource
695
- ? `No GPS position data${filterMsg} for ${Math.floor(timeSinceLastPosition / 60)} minutes. Check that source '${this.options.filterSource}' is active, or change/clear Position source device in Expert Settings.`
696
- : `No GPS position data${filterMsg} for ${Math.floor(timeSinceLastPosition / 60)} minutes. Check your GPS connection.`;
697
- this.setPluginError(errorMsg);
698
- this.app.debug(`Position health check: No position for ${timeSinceLastPosition.toFixed(0)} seconds` + filterMsg);
699
- } else {
700
- this.app.debug(`Position health check: OK (last position ${timeSinceLastPosition.toFixed(0)} seconds ago${filterMsg})`);
701
-
702
- // Clear any previous errors when position health is OK
703
- if (this.currentError) {
704
- const activeSource = this.options.filterSource || this.autoSelectedSource || '';
705
- const sourcePrefix = activeSource ? `${activeSource} | ` : '';
706
- const saveTime = this.lastPosition ? new Date(this.lastPosition.currentTime).toISOString() : 'None since start';
707
- const transferTime = this.lastSuccessfulTransfer ? this.lastSuccessfulTransfer.toISOString() : 'None since start';
708
- this.setPluginStatus(`Save: ${saveTime} | Transfer: ${transferTime} | ${sourcePrefix}`);
709
- }
710
- }
711
- }, 5 * 60 * 1000);
712
-
713
- // Initial check after 2 minutes of startup
714
- setTimeout(() => {
715
- if (!this.lastPositionReceived) {
716
- const activeSource = this.options.filterSource || this.autoSelectedSource || 'any';
717
- const errorMsg = this.options.filterSource
718
- ? `No GPS position data received after 2 minutes from filtered source '${this.options.filterSource}'. Check Expert Settings > Position source device. You may need to leave it empty to use any available GPS source.`
719
- : 'No GPS position data received after 2 minutes. Check that your GPS is connected and SignalK is receiving navigation.position data.';
720
- this.setPluginError(errorMsg);
721
- this.app.debug('Initial position check: No position data received' + (activeSource !== 'any' ? ` from source '${activeSource}'` : ''));
722
- }
723
- }, 2 * 60 * 1000);
724
- }
725
-
726
- async interval() {
727
- const boatMoving = this.checkBoatMoving();
728
- if (!boatMoving) {
729
- return;
730
- }
731
-
732
- const hasTrack = await this.checkTrack();
733
- if (!hasTrack) {
734
- return;
251
+ // Stop track logger
252
+ if (this.trackLogger) {
253
+ this.trackLogger.stop();
735
254
  }
736
255
 
737
- const hasInternet = await this.testInternet();
738
- if (!hasInternet) {
739
- const errorMsg = 'No internet connection detected. Unable to send tracking data to NFL. DNS lookups failed - check your internet connection.';
740
- this.app.debug(errorMsg);
741
- this.setPluginError(errorMsg);
742
- return;
256
+ // Stop health monitor
257
+ if (this.healthMonitor) {
258
+ this.healthMonitor.stop();
743
259
  }
744
260
 
745
- await this.sendData();
746
- }
747
-
748
- checkBoatMoving() {
749
- if (!this.options.trackFrequency) {
750
- return true;
751
- }
752
- const time = this.lastPosition ? this.lastPosition.currentTime : this.upSince;
753
- const secsSinceLastPoint = (new Date().getTime() - time) / 1000;
754
- const isMoving = secsSinceLastPoint <= (this.options.trackFrequency * 2);
755
- if (isMoving) {
756
- this.app.debug('Boat is still moving, last move', secsSinceLastPoint, 'seconds ago');
757
- return this.options.sendWhileMoving;
758
- } else {
759
- this.app.debug('Boat stopped moving, last move at least', secsSinceLastPoint, 'seconds ago');
760
- return true;
761
- }
261
+ this.app.setPluginStatus('Plugin stopped');
762
262
  }
763
263
 
764
- async testInternet() {
765
- const dns = require('dns').promises;
766
-
767
- this.app.debug('testing internet connection');
768
-
769
- const timeoutMs = this.options.internetTestTimeout || 2000;
770
- this.app.debug(`Using internet test timeout: ${timeoutMs}ms`);
771
-
772
- const dnsServers = [
773
- { name: 'Google DNS', ip: '8.8.8.8' },
774
- { name: 'Cloudflare DNS', ip: '1.1.1.1' }
775
- ];
264
+ /**
265
+ * Set plugin status and update data paths
266
+ */
267
+ setPluginStatus(status) {
268
+ this.dataPathEmitter.clearError();
269
+ this.app.setPluginStatus(status);
776
270
 
777
- for (const server of dnsServers) {
778
- try {
779
- const startTime = Date.now();
780
- const result = await Promise.race([
781
- dns.reverse(server.ip),
782
- new Promise((_, reject) =>
783
- setTimeout(() => reject(new Error('DNS timeout')), timeoutMs)
784
- )
785
- ]);
786
- const elapsed = Date.now() - startTime;
787
-
788
- this.app.debug(`internet connection = true, ${server.name} (${server.ip}) is reachable (took ${elapsed}ms)`);
789
- return true;
790
- } catch (err) {
791
- this.app.debug(`${server.name} (${server.ip}) not reachable:`, err.message);
792
- }
793
- }
271
+ const lastPosition = this.trackLogger ? this.trackLogger.getLastPosition() : null;
272
+ const autoSelectedSource = this.trackLogger ? this.trackLogger.getAutoSelectedSource() : null;
794
273
 
795
- this.app.debug(`internet connection = false, no public DNS servers reachable (timeout was ${timeoutMs}ms)`);
796
- return false;
274
+ this.dataPathEmitter.updateStatusPaths(
275
+ this.options,
276
+ lastPosition,
277
+ this.lastSuccessfulTransfer,
278
+ autoSelectedSource
279
+ );
797
280
  }
798
281
 
799
- async checkTrack() {
800
- const trackFile = path.join(this.options.trackDir, routeSaveName);
801
- this.app.debug('checking the track', trackFile, 'if should send');
802
- const exists = await fs.pathExists(trackFile);
803
- const size = exists ? (await fs.lstat(trackFile)).size : 0;
804
- this.app.debug(`'${trackFile}'.size=${size} ${trackFile}'.exists=${exists}`);
805
- return size > 0;
806
- }
807
-
808
- async sendData() {
809
- if (this.options.boatApiKey) {
810
- await this.sendApiData();
811
- } else {
812
- this.app.debug('Failed to send track - no boat API key set in plugin settings.');
813
- this.setPluginError(`Failed to send track - no boat API key set in plugin settings.`);
814
- }
815
- }
816
-
817
- async sendApiData() {
818
- this.app.debug('sending the data');
819
- const pendingFile = path.join(this.options.trackDir, routeSaveName);
820
- const trackData = await this.createTrack(pendingFile);
821
- if (!trackData) {
822
- this.app.debug('Recorded track did not contain any valid track points, aborting sending.');
823
- this.setPluginError(`Failed to send track - Recorded track did not contain any valid track points, aborting sending.`);
824
- return;
825
- }
826
- this.app.debug('created track data with timestamp:', new Date(trackData.timestamp));
827
- const params = new URLSearchParams();
828
- params.append('timestamp', trackData.timestamp);
829
- params.append('track', JSON.stringify(trackData.track));
830
- params.append('boatApiKey', this.options.boatApiKey);
831
- const headers = { 'X-NFL-API-Key': pluginApiKey };
832
- this.app.debug('sending track to API');
833
-
834
- const maxRetries = 3;
835
- const baseTimeout = (this.options.apiTimeout || 30) * 1000;
282
+ /**
283
+ * Set plugin error and update data paths
284
+ */
285
+ setPluginError(error) {
286
+ this.dataPathEmitter.setError(error);
287
+ this.app.setPluginError(error);
836
288
 
837
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
838
- try {
839
- const currentTimeout = baseTimeout * attempt;
840
- this.app.debug(`Attempt ${attempt}/${maxRetries} with ${currentTimeout}ms timeout`);
841
-
842
- const controller = new AbortController();
843
- const timeoutId = setTimeout(() => controller.abort(), currentTimeout);
844
-
845
- const response = await fetch(apiUrl, {
846
- method: 'POST',
847
- body: params,
848
- headers: new fetch.Headers(headers),
849
- signal: controller.signal
850
- });
851
-
852
- clearTimeout(timeoutId);
853
-
854
- if (response.ok) {
855
- const responseBody = await response.json();
856
- if (responseBody.status === 'ok') {
857
- this.lastSuccessfulTransfer = new Date();
858
-
859
- this.emitDelta('noforeignland.sent_to_api', this.lastSuccessfulTransfer.toISOString());
860
- this.emitDelta('noforeignland.sent_to_api_local', this.lastSuccessfulTransfer.toLocaleString());
861
-
862
- this.app.debug('Track successfully sent to API');
863
-
864
- // ISO8601 format for Dashboard
865
- const activeSource = this.options.filterSource || this.autoSelectedSource || '';
866
- const sourcePrefix = activeSource ? `${activeSource} | ` : '';
867
- const saveTime = this.lastPosition ? new Date(this.lastPosition.currentTime).toISOString() : 'None since start';
868
- const transferTime = this.lastSuccessfulTransfer.toISOString();
869
- this.setPluginStatus(`Save: ${saveTime} | Transfer: ${transferTime} | ${sourcePrefix}`);
870
-
871
- await this.handleSuccessfulSend(pendingFile);
872
- return;
873
- } else {
874
- this.app.debug('Could not send track to API, returned response json:', responseBody);
875
- this.setPluginError(`Failed to send track - API returned error.`);
876
- return;
877
- }
878
- } else {
879
- this.app.debug('Could not send track to API, returned response code:', response.status, response.statusText);
880
- if (response.status >= 400 && response.status < 500) {
881
- this.setPluginError(`Failed to send track - HTTP ${response.status}.`);
882
- return;
883
- }
884
- throw new Error(`HTTP ${response.status}`);
885
- }
886
- } catch (err) {
887
- this.app.debug(`Attempt ${attempt} failed:`, err.message);
888
-
889
- if (attempt === maxRetries) {
890
- this.app.debug('Could not send track to API after', maxRetries, 'attempts:', err);
891
- this.setPluginError(`Failed to send track after ${maxRetries} attempts - check logs for details.`);
892
- } else {
893
- const waitTime = 2000 * attempt;
894
- this.app.debug(`Waiting ${waitTime}ms before retry...`);
895
- await new Promise(resolve => setTimeout(resolve, waitTime));
896
- }
897
- }
898
- }
899
- }
900
-
901
- async handleSuccessfulSend(pendingFile) {
902
- const sentFile = path.join(this.options.trackDir, routeSentName);
289
+ const lastPosition = this.trackLogger ? this.trackLogger.getLastPosition() : null;
290
+ const autoSelectedSource = this.trackLogger ? this.trackLogger.getAutoSelectedSource() : null;
903
291
 
904
- try {
905
- if (this.options.keepFiles) {
906
- this.app.debug('Appending sent data to archive file:', routeSentName);
907
- const pendingContent = await fs.readFile(pendingFile, 'utf8');
908
- await fs.appendFile(sentFile, pendingContent);
909
- this.app.debug('Successfully archived sent track data');
910
- } else {
911
- this.app.debug('keepFiles disabled, will delete pending file');
912
- }
913
-
914
- this.app.debug('Deleting pending track file');
915
- await fs.remove(pendingFile);
916
- this.app.debug('Successfully processed track files after send');
917
-
918
- } catch (err) {
919
- this.app.debug('Error handling files after successful send:', err.message);
920
- }
921
- }
922
-
923
- async createTrack(inputPath) {
924
- const fileStream = fs.createReadStream(inputPath);
925
- const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
926
- const track = [];
927
- let lastTimestamp;
928
- for await (const line of rl) {
929
- if (line) {
930
- try {
931
- const point = JSON.parse(line);
932
- const timestamp = new Date(point.t).getTime();
933
- if (!isNaN(timestamp) && this.isValidLatitude(point.lat) && this.isValidLongitude(point.lon)) {
934
- track.push([timestamp, point.lat, point.lon]);
935
- lastTimestamp = timestamp;
936
- }
937
- } catch (error) {
938
- this.app.debug('could not parse line from track file:', line);
939
- this.setPluginError(`Failed could not parse line from track file - check logs for details.`);
940
- }
941
- }
942
- }
943
- if (track.length > 0) {
944
- return { timestamp: new Date(lastTimestamp).getTime(), track };
945
- }
946
- return null;
292
+ this.dataPathEmitter.updateStatusPaths(
293
+ this.options,
294
+ lastPosition,
295
+ this.lastSuccessfulTransfer,
296
+ autoSelectedSource
297
+ );
947
298
  }
948
299
  }
949
300