signalk-to-noforeignland 0.1.26 → 0.1.27

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.
@@ -0,0 +1,30 @@
1
+ name: Publish Package
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ permissions:
9
+ id-token: write # Required for OIDC authentication
10
+ contents: read
11
+
12
+ jobs:
13
+ publish:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - uses: actions/setup-node@v4
19
+ with:
20
+ node-version: '20'
21
+ registry-url: 'https://registry.npmjs.org'
22
+
23
+ - name: Install dependencies
24
+ run: npm ci
25
+
26
+ - name: Run tests if available
27
+ run: npm test --if-present
28
+
29
+ - name: Publish to NPM
30
+ run: npm publish
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ 0.1.27
2
+ * Final version after successful testing SV MOIN and SV KIAPA NUI
3
+
4
+ 0.1.27-beta.1
5
+ * NEW: NPMJS requires new method for publishing. The old tokens will expire Nov 19th, 2025, so moving to OIDC authentication.
6
+ * NEW: Check the GPS status in navigation.position, else retry and throw PluginError on Dashboard
7
+ * CHANGE: Keep track data on disk rewritten and migrate old files to new structure, so nfl-track-sent.jsonl becomes a continuous archive of all sent track data over time, when enabled. New Logic:
8
+ * * New points accumulate in nfl-track-pending.jsonl
9
+ * * Send succeeds → API confirms receipt
10
+ * * If keepFiles=true: The content of pending file is appended to nfl-track-sent.jsonl (line 588)
11
+ * * Pending file is deleted
12
+ * * Next GPS points → create a new pending file
13
+ * * Next successful send → appends again to the same nfl-track-sent.jsonl
14
+
1
15
  0.1.26
2
16
  * Same as 0.1.26-beta.1
3
17
 
@@ -0,0 +1,62 @@
1
+ How-to install the latest beta on your device?
2
+
3
+ This guide assumes you have a default install with default folders.
4
+ This is written for a Cerbo GX or a RPI, jump to the section you need want to go to.
5
+ It is recommended to enable the Debug Log for this plugin in Server -> Plugin Config before updating.
6
+
7
+ **For Raspberry PI:**
8
+
9
+ 1. Backup old data
10
+
11
+ ```
12
+ cd ~
13
+ mkdir nfl-backup
14
+ cp -a .signalk/node_modules/signalk-to-noforeignland/* nfl-backup/
15
+ ```
16
+
17
+ 2. Get new files from repo (main for latest)
18
+
19
+ ```
20
+ cd ~/.signalk/node_modules/signalk-to-noforeignland/
21
+ rm *
22
+ #NOTE: Trackdir will not be deleted, this is what we want.
23
+ wget https://github.com/noforeignland/nfl-signalk/archive/refs/heads/main.zip
24
+ unzip main.zip
25
+ cp -r nfl-signalk-main/* .
26
+ rm main.zip
27
+ rm -rf nfl-signalk-main/
28
+ ```
29
+
30
+ 3. Restart Server & Check logs
31
+
32
+ ```
33
+ sudo systemctl restart signalk.service && sudo journalctl -u signalk.service -f
34
+ ```
35
+
36
+
37
+ **For Cerbo GX with Image Large:**
38
+ 1. Backup old data
39
+
40
+ ```
41
+ cd ~
42
+ mkdir nfl-backup
43
+ cp -a /data/conf/signalk/node_modules/signalk-to-noforeignland/* nfl-backup
44
+ ```
45
+
46
+ 2. Get new files from repo (main for latest)
47
+
48
+ ```
49
+ cd /data/conf/signalk/node_modules/signalk-to-noforeignland/
50
+ rm *
51
+ # NOTE: Trackdir will not be deleted, this is what we want.
52
+ wget https://github.com/noforeignland/nfl-signalk/archive/refs/heads/main.zip
53
+ unzip main.zip
54
+ cp -r nfl-signalk-main/* .
55
+ rm main.zip
56
+ rm -rf nfl-signalk-main/
57
+ ```
58
+
59
+ 3. Restart Server & Check logs
60
+ ```
61
+ Restart Server vom Webgui and check logs in Webgui
62
+ ```
package/index.js CHANGED
@@ -11,7 +11,8 @@ const isReachable = require('is-reachable');
11
11
  const apiUrl = 'https://www.noforeignland.com/home/api/v1/boat/tracking/track';
12
12
  const pluginApiKey = '0ede6cb6-5213-45f5-8ab4-b4836b236f97';
13
13
  const defaultTracksDir = 'track';
14
- const routeSaveName = 'nfl-track.jsonl';
14
+ const routeSaveName = 'nfl-track-pending.jsonl'; // Changed: separate pending file
15
+ const routeSentName = 'nfl-track-sent.jsonl'; // New: archive for sent data
15
16
 
16
17
  class SignalkToNoforeignland {
17
18
  constructor(app) {
@@ -148,6 +149,10 @@ getSchema() {
148
149
 
149
150
  async start(options = {}, restartPlugin) {
150
151
 
152
+ // Position data health check
153
+ this.positionCheckInterval = null;
154
+ this.lastPositionReceived = null;
155
+
151
156
  // Backward compatibility: migrate old flat structure to new nested structure
152
157
  let needsSave = false;
153
158
  if (options.boatApiKey && !options.mandatory) {
@@ -227,6 +232,9 @@ getSchema() {
227
232
  return;
228
233
  }
229
234
 
235
+ // NEW: Migrate old track file to new naming scheme on startup
236
+ await this.migrateOldTrackFile();
237
+
230
238
  this.app.debug('track logger started, now logging to', this.options.trackDir);
231
239
  this.app.setPluginStatus(`Started${needsSave ? ' (config migrated)' : ''}`);
232
240
  this.upSince = new Date().getTime();
@@ -247,10 +255,38 @@ getSchema() {
247
255
  // start cron job
248
256
  this.cron = new CronJob(this.options.apiCron, this.interval.bind(this));
249
257
  this.cron.start();
258
+
259
+ // Start position health check (every 5 minutes)
260
+ this.startPositionHealthCheck();
250
261
  }
251
262
 
263
+ // NEW: Migrate old track file naming to new scheme
264
+ async migrateOldTrackFile() {
265
+ const oldTrackFile = path.join(this.options.trackDir, 'nfl-track.jsonl');
266
+ const newPendingFile = path.join(this.options.trackDir, routeSaveName);
267
+
268
+ try {
269
+ // Check if old file exists and new pending file doesn't
270
+ if (await fs.pathExists(oldTrackFile) && !(await fs.pathExists(newPendingFile))) {
271
+ this.app.debug('Migrating old track file to new naming scheme...');
272
+ await fs.move(oldTrackFile, newPendingFile);
273
+ this.app.debug('Successfully migrated old track file to:', routeSaveName);
274
+ }
275
+ } catch (err) {
276
+ this.app.debug('Error during track file migration:', err.message);
277
+ // Non-fatal error, continue startup
278
+ }
279
+ }
280
+
252
281
  stop() {
253
282
  this.app.debug('plugin stopped');
283
+
284
+ // Stop position health check
285
+ if (this.positionCheckInterval) {
286
+ clearInterval(this.positionCheckInterval);
287
+ this.positionCheckInterval = null;
288
+ }
289
+
254
290
  if (this.cron) {
255
291
  this.cron.stop();
256
292
  this.cron = undefined;
@@ -360,6 +396,7 @@ getSchema() {
360
396
 
361
397
  // Punkt speichern
362
398
  this.lastPosition = { pos: value.value, timestamp, currentTime: new Date().getTime() };
399
+ this.lastPositionReceived = new Date().getTime(); // Track for health check
363
400
  await this.savePoint(this.lastPosition);
364
401
 
365
402
  // shouldDoLog zurücksetzen wenn minSpeed aktiv ist
@@ -378,6 +415,7 @@ getSchema() {
378
415
  t: point.timestamp
379
416
  };
380
417
  this.app.debug(`save data point:`, obj);
418
+ // CHANGED: Save to pending file
381
419
  await fs.appendFile(path.join(this.options.trackDir, routeSaveName), JSON.stringify(obj) + EOL);
382
420
 
383
421
  const lastSaveTime = new Date().toISOString();
@@ -443,6 +481,57 @@ getSchema() {
443
481
  return res;
444
482
  }
445
483
 
484
+ // NEW: Position health check
485
+ startPositionHealthCheck() {
486
+ // Check every 5 minutes if we're receiving position data
487
+ this.positionCheckInterval = setInterval(() => {
488
+ const now = new Date().getTime();
489
+ const timeSinceLastPosition = this.lastPositionReceived
490
+ ? (now - this.lastPositionReceived) / 1000
491
+ : null;
492
+
493
+ // Build appropriate error message based on filterSource setting
494
+ const filterMsg = this.options.filterSource
495
+ ? ` from source '${this.options.filterSource}'`
496
+ : '';
497
+
498
+ if (!this.lastPositionReceived) {
499
+ // Never received any position data
500
+ const errorMsg = this.options.filterSource
501
+ ? `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.`
502
+ : 'No GPS position data received. Check that your GPS is connected and SignalK is receiving navigation.position data.';
503
+ this.app.setPluginError(errorMsg);
504
+ this.app.debug('Position health check: No position data ever received' + filterMsg);
505
+ } else if (timeSinceLastPosition > 300) {
506
+ // No position data for more than 5 minutes
507
+ const errorMsg = this.options.filterSource
508
+ ? `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.`
509
+ : `No GPS position data for ${Math.floor(timeSinceLastPosition / 60)} minutes. Check your GPS connection.`;
510
+ this.app.setPluginError(errorMsg);
511
+ this.app.debug(`Position health check: No position for ${timeSinceLastPosition.toFixed(0)} seconds` + filterMsg);
512
+ } else {
513
+ // Position data is flowing normally
514
+ this.app.debug(`Position health check: OK (last position ${timeSinceLastPosition.toFixed(0)} seconds ago${filterMsg})`);
515
+ // Clear any previous error if position is now flowing
516
+ const lastSaveTime = this.lastPosition ? new Date(this.lastPosition.currentTime).toISOString() : 'Never';
517
+ const lastTransferTime = this.lastSuccessfulTransfer ? this.lastSuccessfulTransfer.toISOString() : 'None since start';
518
+ const sourceInfo = this.options.filterSource ? ` (source: ${this.options.filterSource})` : '';
519
+ this.app.setPluginStatus(`Active${sourceInfo} - Last save: ${lastSaveTime} | Last transfer: ${lastTransferTime}`);
520
+ }
521
+ }, 5 * 60 * 1000); // Check every 5 minutes
522
+
523
+ // Also do an initial check after 2 minutes
524
+ setTimeout(() => {
525
+ if (!this.lastPositionReceived) {
526
+ const errorMsg = this.options.filterSource
527
+ ? `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.`
528
+ : 'No GPS position data received after 2 minutes. Check that your GPS is connected and SignalK is receiving navigation.position data.';
529
+ this.app.setPluginError(errorMsg);
530
+ this.app.debug('Initial position check: No position data received' + (this.options.filterSource ? ` from source '${this.options.filterSource}'` : ''));
531
+ }
532
+ }, 2 * 60 * 1000);
533
+ }
534
+
446
535
  // periodic interval called by cron
447
536
  async interval() {
448
537
  if ((this.checkBoatMoving()) && await this.checkTrack() && await this.testInternet()) {
@@ -503,6 +592,7 @@ getSchema() {
503
592
  }
504
593
 
505
594
  async checkTrack() {
595
+ // CHANGED: Check pending file instead
506
596
  const trackFile = path.join(this.options.trackDir, routeSaveName);
507
597
  this.app.debug('checking the track', trackFile, 'if should send');
508
598
  const exists = await fs.pathExists(trackFile);
@@ -522,7 +612,9 @@ getSchema() {
522
612
 
523
613
  async sendApiData() {
524
614
  this.app.debug('sending the data');
525
- const trackData = await this.createTrack(path.join(this.options.trackDir, routeSaveName));
615
+ // CHANGED: Read from pending file
616
+ const pendingFile = path.join(this.options.trackDir, routeSaveName);
617
+ const trackData = await this.createTrack(pendingFile);
526
618
  if (!trackData) {
527
619
  this.app.debug('Recorded track did not contain any valid track points, aborting sending.');
528
620
  this.app.setPluginError(`Failed to send track - Recorded track did not contain any valid track points, aborting sending.`);
@@ -564,14 +656,9 @@ getSchema() {
564
656
  this.lastSuccessfulTransfer = new Date();
565
657
  this.app.debug('Track successfully sent to API');
566
658
  this.app.setPluginStatus(`Started - last Track sent successfully at ${new Date().toISOString()}`);
567
- if (this.options.keepFiles) {
568
- const filename = new Date().toJSON().slice(0, 19).replace(/:/g, '') + '-nfl-track.jsonl';
569
- this.app.debug('moving and keeping track file: ', filename);
570
- await fs.move(path.join(this.options.trackDir, routeSaveName), path.join(this.options.trackDir, filename));
571
- } else {
572
- this.app.debug('Deleting track file');
573
- await fs.remove(path.join(this.options.trackDir, routeSaveName));
574
- }
659
+
660
+ // CHANGED: New file handling logic
661
+ await this.handleSuccessfulSend(pendingFile);
575
662
  return; // Erfolg - beende Funktion
576
663
  } else {
577
664
  this.app.debug('Could not send track to API, returned response json:', responseBody);
@@ -605,6 +692,39 @@ getSchema() {
605
692
  }
606
693
  }
607
694
  }
695
+
696
+ // NEW: Handle file operations after successful send
697
+ async handleSuccessfulSend(pendingFile) {
698
+ const sentFile = path.join(this.options.trackDir, routeSentName);
699
+
700
+ try {
701
+ if (this.options.keepFiles) {
702
+ // Append pending data to sent archive
703
+ this.app.debug('Appending sent data to archive file:', routeSentName);
704
+
705
+ // Read pending file content
706
+ const pendingContent = await fs.readFile(pendingFile, 'utf8');
707
+
708
+ // Append to sent file (create if doesn't exist)
709
+ await fs.appendFile(sentFile, pendingContent);
710
+
711
+ this.app.debug('Successfully archived sent track data');
712
+ } else {
713
+ this.app.debug('keepFiles disabled, will delete pending file');
714
+ }
715
+
716
+ // Always delete the pending file after successful send
717
+ this.app.debug('Deleting pending track file');
718
+ await fs.remove(pendingFile);
719
+ this.app.debug('Successfully processed track files after send');
720
+
721
+ } catch (err) {
722
+ this.app.debug('Error handling files after successful send:', err.message);
723
+ // Non-fatal: Data was sent successfully, file handling is secondary
724
+ // Next save will create a new pending file anyway
725
+ }
726
+ }
727
+
608
728
  async createTrack(inputPath) {
609
729
  const fileStream = fs.createReadStream(inputPath);
610
730
  const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
@@ -635,4 +755,4 @@ getSchema() {
635
755
  module.exports = function (app) {
636
756
  const instance = new SignalkToNoforeignland(app);
637
757
  return instance.getPluginObject();
638
- };
758
+ };
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "signalk-to-noforeignland",
3
- "version": "0.1.26",
4
- "description": "SignalK track logger to noforeignland.com ",
3
+ "version": "0.1.27",
4
+ "description": "SignalK track logger to noforeignland.com",
5
5
  "main": "index.js",
6
6
  "keywords": [
7
7
  "signalk-node-server-plugin",
8
8
  "signalk-category-utility"
9
9
  ],
10
10
  "author": "Dirk Wahrheit",
11
+ "license": "MIT",
11
12
  "homepage": "https://github.com/noforeignland/nfl-signalk",
12
13
  "bugs": {
13
14
  "url": "https://github.com/noforeignland/nfl-signalk/issues"
@@ -16,8 +17,11 @@
16
17
  "type": "git",
17
18
  "url": "https://github.com/noforeignland/nfl-signalk.git"
18
19
  },
20
+ "engines": {
21
+ "node": ">=18.0.0"
22
+ },
19
23
  "scripts": {
20
- "test": "echo \"Error: no test specified\" && exit 1"
24
+ "test": "echo \"No tests specified\" && exit 0"
21
25
  },
22
26
  "dependencies": {
23
27
  "cron": "^2.1.0",
@@ -25,4 +29,4 @@
25
29
  "is-reachable": "^5.2.1",
26
30
  "node-fetch": "^2.6.7"
27
31
  }
28
- }
32
+ }
@@ -0,0 +1,2 @@
1
+ {"lat":-17.6813649,"lon":177.3838949,"t":"2025-11-09T17:38:20.841Z"}
2
+ {"lat":-17.6813779,"lon":177.3838402,"t":"2025-11-09T17:58:22.829Z"}