@meri-imperiumi/signalk-triplogger 1.3.0 → 1.3.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/.editorconfig ADDED
@@ -0,0 +1,5 @@
1
+ [*]
2
+ end_of_line = lf
3
+ insert_final_newline = true
4
+ indent_style = space
5
+ indent_size = 2
package/.eslintrc.json CHANGED
@@ -1,3 +1,9 @@
1
1
  {
2
- "extends": "airbnb-base"
2
+ "extends": "airbnb-base",
3
+ "parserOptions": {
4
+ "ecmaVersion": 2022
5
+ },
6
+ "rules": {
7
+ "linebreak-style": "warn"
8
+ }
3
9
  }
@@ -5,14 +5,19 @@ on:
5
5
  tags:
6
6
  - "*"
7
7
 
8
+ permissions:
9
+ id-token: write # Required for OIDC
10
+ contents: read
11
+
8
12
  jobs:
9
13
  build:
10
14
  runs-on: ubuntu-latest
11
15
  steps:
12
16
  - uses: actions/checkout@v3.0.2
13
- - uses: actions/setup-node@v3.4.1
17
+ - uses: actions/setup-node@v3.1.1
14
18
  with:
15
- node-version: 12
19
+ node-version: 24
20
+ package-manager-cache: false # never use caching in release builds
16
21
  - run: npm install
17
22
  - run: npm test
18
23
 
@@ -21,10 +26,9 @@ jobs:
21
26
  runs-on: ubuntu-latest
22
27
  steps:
23
28
  - uses: actions/checkout@v3.0.2
24
- - uses: actions/setup-node@v3.4.1
29
+ - uses: actions/setup-node@v3.1.1
25
30
  with:
26
- node-version: 12
31
+ node-version: 24
27
32
  registry-url: https://registry.npmjs.org/
33
+ package-manager-cache: false # never use caching in release builds
28
34
  - run: npm publish
29
- env:
30
- NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
@@ -0,0 +1,11 @@
1
+ name: SignalK Plugin CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, master]
6
+ pull_request:
7
+ branches: [main, master]
8
+
9
+ jobs:
10
+ test:
11
+ uses: SignalK/signalk-server/.github/workflows/plugin-ci.yml@master
@@ -8,22 +8,14 @@ jobs:
8
8
  runs-on: ubuntu-latest
9
9
  strategy:
10
10
  matrix:
11
- node-version: [12.x]
11
+ node-version: [24.x]
12
12
  steps:
13
13
  - uses: actions/checkout@v3.0.2
14
14
  - name: Use Node.js ${{ matrix.node-version }}
15
- uses: actions/setup-node@v3.4.1
15
+ uses: actions/setup-node@v3.1.1
16
16
  with:
17
17
  node-version: ${{ matrix.node-version }}
18
18
  - run: npm install
19
19
  - run: npm test
20
20
  env:
21
21
  CI: true
22
- merge-me:
23
- name: Auto-merge dependency updates
24
- needs: test
25
- runs-on: ubuntu-latest
26
- steps:
27
- - uses: ridedott/merge-me-action@v2.10.22
28
- with:
29
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
package/CHANGELOG.md ADDED
@@ -0,0 +1,42 @@
1
+ # Changelog
2
+
3
+ ## [1.3.2] - 2026-08-23
4
+
5
+ ### Added
6
+ - Added smoke tests for the plugin entry point (start/stop, subscription,
7
+ state-change resets, position logging, totals)
8
+
9
+ ## [1.3.1] - 2026-06-16
10
+
11
+ ### Added
12
+ - Added application icon for SK appstore
13
+
14
+ ## [1.3.0] - 2026-06-10
15
+
16
+ ### Fixed
17
+ - Don't start a new trip at startup if there's an ongoing one
18
+
19
+ ## [1.2.0] - 2023-04-27
20
+
21
+ ### Added
22
+ - Added support for populating `navigation.log` for totals
23
+
24
+ ## [1.1.1] - 2023-02-28
25
+
26
+ ### Fixed
27
+ - Fix issue when `navigation.position` doesn't contain coordinates
28
+
29
+ ## [1.1.0] - 2022-03-04
30
+
31
+ ### Added
32
+ - Persist logs for current day/month/year, also by sailing/motoring
33
+
34
+ ## [1.0.1] - 2022-02-18
35
+
36
+ ### Fixed
37
+ - Fix issue with reading lat and lon
38
+
39
+ ## [1.0.0] - 2022-02-18
40
+
41
+ ### Added
42
+ - Initial version, logging in-memory only
package/README.md CHANGED
@@ -6,15 +6,4 @@ The plugin monitors vessel `navigation.state` as set by the [signalk-autostate](
6
6
 
7
7
  ## Changes
8
8
 
9
- * 1.3.0 (June 10th 2026)
10
- - Don't start a new trip at startup if there's an ongoing one
11
- * 1.2.0 (April 27th 2023)
12
- - Added support for populating `navigation.log` for totals
13
- * 1.1.1 (February 28th 2023)
14
- - Fix issue when `navigation.position` doesn't contain coordinates
15
- * 1.1.0 (March 4th 2022)
16
- - Persist logs for current day/month/year, also by sailing/motoring
17
- * 1.0.1 (February 18th 2022)
18
- - Fix issue with reading lat and lon
19
- * 1.0.0 (February 18th 2022)
20
- - Initial version, logging in-memory only
9
+ See [Changelog](CHANGELOG.md)
Binary file
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@meri-imperiumi/signalk-triplogger",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Log the length of the current trip",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
- "lint": "eslint .",
8
- "test": "npm run lint"
7
+ "test": "node --test --test-force-exit \"tests/*.test.js\" && eslint .",
8
+ "lint": "eslint ."
9
9
  },
10
10
  "repository": {
11
11
  "type": "git",
@@ -16,6 +16,13 @@
16
16
  "signalk-category-utility"
17
17
  ],
18
18
  "signalk-plugin-enabled-by-default": true,
19
+ "signalk": {
20
+ "appIcon": "./doc/appicon.png",
21
+ "displayName": "Automatic Trip Length Logger",
22
+ "requires": [
23
+ "@meri-imperiumi/signalk-autostate"
24
+ ]
25
+ },
19
26
  "author": "Henri Bergius <henri.bergius@iki.fi>",
20
27
  "license": "MIT",
21
28
  "bugs": {
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Shared test fakes: a mock Signal K app matching the patterns used by the
3
+ * signalk-aprsfi-ais-reporter test suite, adapted for the triplogger's
4
+ * needs (subscriptionmanager + handleMessage + setPluginStatus + data dir).
5
+ *
6
+ * @file fake-app.js
7
+ */
8
+
9
+ /* eslint-disable class-methods-use-this */
10
+
11
+ const { join } = require('node:path');
12
+ const { mkdtempSync } = require('node:fs');
13
+ const { tmpdir } = require('node:os');
14
+
15
+ /**
16
+ * Minimal Signal K app fake with subscriptionmanager + handleMessage +
17
+ * setPluginStatus + setProviderStatus + getDataDirPath + error.
18
+ *
19
+ * The data path is a fresh temp directory per instance so the Logger can
20
+ * write log files without touching the project tree.
21
+ */
22
+ class FakeSignalKApp {
23
+ constructor() {
24
+ this.selfId = 'urn:mrn:imo:mmsi:230999999';
25
+ this.subscriptionmanager = {
26
+ subscriptions: [],
27
+ subscribe(subscription, unsubscribes, onError, onDelta) {
28
+ this.subscriptions.push({ subscription, onDelta });
29
+ unsubscribes.push(() => {
30
+ const idx = this.subscriptions.findIndex(
31
+ (s) => s.subscription === subscription,
32
+ );
33
+ if (idx >= 0) this.subscriptions.splice(idx, 1);
34
+ });
35
+ },
36
+ };
37
+ this.statusMessages = [];
38
+ this.errors = [];
39
+ this.messages = [];
40
+ this.dataPath = mkdtempSync(join(tmpdir(), 'triplogger-test-'));
41
+ }
42
+
43
+ getDataDirPath() {
44
+ return this.dataPath;
45
+ }
46
+
47
+ setPluginStatus = (msg) => {
48
+ this.statusMessages.push({ type: 'status', msg });
49
+ };
50
+
51
+ setProviderStatus = (msg) => {
52
+ this.statusMessages.push({ type: 'status', msg });
53
+ };
54
+
55
+ handleMessage = (sourceId, message) => {
56
+ this.messages.push({ sourceId, message });
57
+ };
58
+
59
+ debug() {}
60
+
61
+ error(msg) {
62
+ this.errors.push(msg);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Emits a delta into the subscriptionmanager's registered handlers, for
68
+ * tests that want to feed navigation values.
69
+ *
70
+ * @param {FakeSignalKApp} app
71
+ * @param {object} delta
72
+ * @returns {void}
73
+ */
74
+ function emitDelta(app, delta) {
75
+ app.subscriptionmanager.subscriptions.forEach(({ onDelta }) => onDelta(delta));
76
+ }
77
+
78
+ module.exports = { FakeSignalKApp, emitDelta };
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Smoke tests for the plugin entry point (plugin shape, start/stop,
3
+ * subscription, state-change resets, position logging, totals).
4
+ * @file plugin.test.js
5
+ */
6
+
7
+ const test = require('node:test');
8
+ const assert = require('node:assert/strict');
9
+ const { readFileSync, writeFileSync } = require('node:fs');
10
+ const { join } = require('node:path');
11
+
12
+ const makePlugin = require('../index');
13
+ const { FakeSignalKApp, emitDelta } = require('./fake-app');
14
+
15
+ /**
16
+ * Poll until `predicate` returns truthy, retrying every ~10ms up to `timeout`
17
+ * milliseconds. Resolves true on success, false on timeout. Useful for
18
+ * awaiting async plugin side-effects (file I/O) without fixed sleeps.
19
+ *
20
+ * @param {() => boolean} predicate
21
+ * @param {number} [timeout]
22
+ * @returns {Promise<boolean>}
23
+ */
24
+ function waitFor(predicate, timeout = 500) {
25
+ return new Promise((resolve) => {
26
+ const start = Date.now();
27
+ const tick = () => {
28
+ if (predicate()) {
29
+ resolve(true);
30
+ return;
31
+ }
32
+ if (Date.now() - start >= timeout) {
33
+ resolve(false);
34
+ return;
35
+ }
36
+ setTimeout(tick, 10);
37
+ };
38
+ tick();
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Build a started plugin instance wired to a fresh fake app with the
44
+ * given settings (defaulted).
45
+ *
46
+ * @param {object} [overrides]
47
+ * @returns {{app: FakeSignalKApp, plugin: object}}
48
+ */
49
+ function makeStarted(overrides) {
50
+ const app = new FakeSignalKApp();
51
+ const plugin = makePlugin(app);
52
+ plugin.start({
53
+ update_interval: 10000,
54
+ totals: true,
55
+ totals_base: 0,
56
+ ...overrides,
57
+ });
58
+ return { app, plugin };
59
+ }
60
+
61
+ /**
62
+ * Read a log file written by the Logger from the app's data dir.
63
+ *
64
+ * @param {FakeSignalKApp} app
65
+ * @param {string} name
66
+ * @returns {object}
67
+ */
68
+ function readLog(app, name) {
69
+ return JSON.parse(readFileSync(join(app.dataPath, `${name}.json`), 'utf-8'));
70
+ }
71
+
72
+ /**
73
+ * Pre-seed a log file in the app's data dir before start, so the Logger
74
+ * loads an existing state instead of starting fresh. Used to make the
75
+ * "new trip" transition deterministic without racing two state emits.
76
+ *
77
+ * @param {FakeSignalKApp} app
78
+ * @param {string} name
79
+ * @param {object} contents
80
+ * @returns {void}
81
+ */
82
+ function seedLog(app, name, contents) {
83
+ writeFileSync(join(app.dataPath, `${name}.json`), JSON.stringify(contents, null, 2));
84
+ }
85
+
86
+ test('creates a plugin object with the right id/name', () => {
87
+ const app = new FakeSignalKApp();
88
+ const plugin = makePlugin(app);
89
+ assert.strictEqual(plugin.id, 'signalk-triplogger');
90
+ assert.strictEqual(plugin.name, 'Trip logger');
91
+ assert.ok(plugin.description);
92
+ });
93
+
94
+ test('schema is a JSON object with the expected options', () => {
95
+ const app = new FakeSignalKApp();
96
+ const plugin = makePlugin(app);
97
+ assert.strictEqual(plugin.schema.type, 'object');
98
+ assert.ok(plugin.schema.properties.update_interval);
99
+ assert.ok(plugin.schema.properties.totals);
100
+ assert.ok(plugin.schema.properties.totals_base);
101
+ });
102
+
103
+ test('start subscribes to navigation.state and navigation.position', () => {
104
+ const { app, plugin } = makeStarted();
105
+ assert.strictEqual(app.subscriptionmanager.subscriptions.length, 1);
106
+ const sub = app.subscriptionmanager.subscriptions[0].subscription;
107
+ assert.strictEqual(sub.context, 'vessels.self');
108
+ const paths = sub.subscribe.map((s) => s.path);
109
+ assert.ok(paths.includes('navigation.state'));
110
+ assert.ok(paths.includes('navigation.position'));
111
+ plugin.stop();
112
+ });
113
+
114
+ test('start sets an initial status message', () => {
115
+ const { app, plugin } = makeStarted();
116
+ assert.ok(
117
+ app.statusMessages.some(
118
+ (m) => m.type === 'status' && /waiting/i.test(m.msg),
119
+ ),
120
+ `got ${JSON.stringify(app.statusMessages)}`,
121
+ );
122
+ plugin.stop();
123
+ });
124
+
125
+ test('transitioning into a trip resets the log and posts a status', async () => {
126
+ const app = new FakeSignalKApp();
127
+ // A fresh current log is considered already in-trip. To make the
128
+ // transition into a trip deterministic (instead of racing two async
129
+ // state emits), seed an *ended* current log so inTrip() starts false,
130
+ // then emit a single trip-starting state.
131
+ seedLog(app, 'current', {
132
+ started: '2026-01-01T00:00:00.000Z',
133
+ ended: '2026-01-01T01:00:00.000Z',
134
+ total: 0,
135
+ states: {},
136
+ });
137
+ const plugin = makePlugin(app);
138
+ plugin.start({});
139
+ emitDelta(app, {
140
+ context: 'vessels.self',
141
+ updates: [
142
+ { values: [{ path: 'navigation.state', value: 'sailing' }] },
143
+ ],
144
+ });
145
+ await waitFor(
146
+ () => app.statusMessages.some((m) => /new trip/i.test(m.msg)),
147
+ );
148
+ plugin.stop();
149
+ assert.ok(
150
+ app.statusMessages.some(
151
+ (m) => m.type === 'status' && /new trip/i.test(m.msg),
152
+ ),
153
+ `got ${JSON.stringify(app.statusMessages)}`,
154
+ );
155
+ });
156
+
157
+ test('position updates append distance to the trip log and emit deltas', async () => {
158
+ const app = new FakeSignalKApp();
159
+ // Seed an ended current log so start isn't considered in-trip, then emit
160
+ // a single trip-starting state. This avoids racing two async state emits.
161
+ seedLog(app, 'current', {
162
+ started: '2026-01-01T00:00:00.000Z',
163
+ ended: '2026-01-01T01:00:00.000Z',
164
+ total: 0,
165
+ states: {},
166
+ });
167
+ const plugin = makePlugin(app);
168
+ plugin.start({ totals: true });
169
+ emitDelta(app, {
170
+ context: 'vessels.self',
171
+ updates: [{ values: [{ path: 'navigation.state', value: 'sailing' }] }],
172
+ });
173
+ // Wait for the new-trip reset to settle before sending positions.
174
+ await waitFor(
175
+ () => app.statusMessages.some((m) => /new trip/i.test(m.msg)),
176
+ );
177
+
178
+ // First position just establishes lastPosition; no distance yet.
179
+ emitDelta(app, {
180
+ context: 'vessels.self',
181
+ updates: [
182
+ { values: [{ path: 'navigation.position', value: { latitude: 60.0, longitude: 24.0 } }] },
183
+ ],
184
+ });
185
+
186
+ // Second position ~111m east of the first → appendTrip writes log files.
187
+ emitDelta(app, {
188
+ context: 'vessels.self',
189
+ updates: [
190
+ { values: [{ path: 'navigation.position', value: { latitude: 60.001, longitude: 24.0 } }] },
191
+ ],
192
+ });
193
+ await waitFor(() => {
194
+ const tripValue = app.messages
195
+ .flatMap(({ message }) => message.updates.flatMap((u) => u.values))
196
+ .find((v) => v.path === 'navigation.trip.log' && v.value > 0);
197
+ return !!tripValue;
198
+ });
199
+
200
+ plugin.stop();
201
+
202
+ // A trip.log delta should have been published to the app.
203
+ const tripMsgs = app.messages.filter(
204
+ ({ message }) => message.updates.some((u) => u.values.some((v) => v.path === 'navigation.trip.log')),
205
+ );
206
+ assert.ok(tripMsgs.length > 0, 'no navigation.trip.log delta was emitted');
207
+
208
+ // The current trip log file should be persisted with a positive total.
209
+ const current = readLog(app, 'current');
210
+ assert.ok(current.total > 0, `current.total was ${current.total}`);
211
+ });
212
+
213
+ test('totals option also publishes navigation.log', async () => {
214
+ const app = new FakeSignalKApp();
215
+ seedLog(app, 'current', {
216
+ started: '2026-01-01T00:00:00.000Z',
217
+ ended: '2026-01-01T01:00:00.000Z',
218
+ total: 0,
219
+ states: {},
220
+ });
221
+ const plugin = makePlugin(app);
222
+ plugin.start({ totals: true, totals_base: 100 });
223
+ emitDelta(app, {
224
+ context: 'vessels.self',
225
+ updates: [{ values: [{ path: 'navigation.state', value: 'motoring' }] }],
226
+ });
227
+ await waitFor(
228
+ () => app.statusMessages.some((m) => /new trip/i.test(m.msg)),
229
+ );
230
+
231
+ emitDelta(app, {
232
+ context: 'vessels.self',
233
+ updates: [
234
+ { values: [{ path: 'navigation.position', value: { latitude: 60.0, longitude: 24.0 } }] },
235
+ ],
236
+ });
237
+ emitDelta(app, {
238
+ context: 'vessels.self',
239
+ updates: [
240
+ { values: [{ path: 'navigation.position', value: { latitude: 60.002, longitude: 24.0 } }] },
241
+ ],
242
+ });
243
+ await waitFor(() => {
244
+ const logValue = app.messages
245
+ .flatMap(({ message }) => message.updates.flatMap((u) => u.values))
246
+ .find((v) => v.path === 'navigation.log' && v.value > 100);
247
+ return !!logValue;
248
+ });
249
+ plugin.stop();
250
+
251
+ const logMsgs = app.messages.filter(
252
+ ({ message }) => message.updates.some((u) => u.values.some((v) => v.path === 'navigation.log')),
253
+ );
254
+ assert.ok(logMsgs.length > 0, 'no navigation.log delta was emitted');
255
+ const last = logMsgs[logMsgs.length - 1];
256
+ const { value } = last.message.updates
257
+ .flatMap((u) => u.values)
258
+ .find((v) => v.path === 'navigation.log');
259
+ // totals_base of 100 should be added on top of the logged distance.
260
+ assert.ok(value > 100, `navigation.log value was ${value}`);
261
+ });
262
+
263
+ test('stop clears subscriptions', () => {
264
+ const { app, plugin } = makeStarted();
265
+ assert.strictEqual(app.subscriptionmanager.subscriptions.length, 1);
266
+ plugin.stop();
267
+ assert.strictEqual(app.subscriptionmanager.subscriptions.length, 0);
268
+ });
@@ -1,11 +0,0 @@
1
- version: 2
2
- updates:
3
- - package-ecosystem: npm
4
- directory: "/"
5
- schedule:
6
- interval: daily
7
- versioning-strategy: increase-if-necessary
8
- - package-ecosystem: github-actions
9
- directory: "/"
10
- schedule:
11
- interval: weekly