signalk-webhook-bridge 2.0.0 → 2.0.1
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/CHANGELOG.md +15 -0
- package/README.md +2 -0
- package/package.json +2 -2
- package/plugin/index.js +58 -17
- package/plugin/schedule.js +21 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,21 @@ All notable changes to Webhook Bridge will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/),
|
|
6
6
|
and this project uses [Semantic Versioning](https://semver.org/).
|
|
7
7
|
|
|
8
|
+
## [2.0.1] - 2026-09-13
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Webhook captures are now aligned to the server clock rather than being timed from when the plugin starts.
|
|
13
|
+
- A 60-minute interval now captures on the hour.
|
|
14
|
+
- A 30-minute interval captures at `:00` and `:30`.
|
|
15
|
+
- A 15-minute interval captures at `:00`, `:15`, `:30` and `:45`.
|
|
16
|
+
- The next capture time is recalculated after every snapshot to avoid gradual timer drift.
|
|
17
|
+
- Updated configuration help text to explain clock-aligned scheduling.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- Automated tests for clock-aligned capture scheduling, including hourly, half-hourly, quarter-hourly, five-minute and day-boundary behaviour.
|
|
22
|
+
|
|
8
23
|
## [2.0.0] - 2026-09-13
|
|
9
24
|
|
|
10
25
|
### Added
|
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ Choose the Signal K data paths you want to send, assign simple field names, sele
|
|
|
6
6
|
|
|
7
7
|
If the webhook is unavailable, data is stored locally in a persistent queue and automatically delivered in order when the connection returns.
|
|
8
8
|
|
|
9
|
+
Captures are aligned to the Signal K server's local clock. For example, a 60-minute interval captures on the hour, while a 15-minute interval captures at :00, :15, :30 and :45.
|
|
10
|
+
|
|
9
11
|
## Features
|
|
10
12
|
|
|
11
13
|
- Send selected Signal K data to any HTTP webhook
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-webhook-bridge",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Send selected Signal K data paths to an external webhook with unit conversion, persistent queueing and automatic retry.",
|
|
5
5
|
"main": "plugin/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -60,4 +60,4 @@
|
|
|
60
60
|
"webpack": "^5.110.3",
|
|
61
61
|
"webpack-cli": "^7.2.3"
|
|
62
62
|
}
|
|
63
|
-
}
|
|
63
|
+
}
|
package/plugin/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Signal K Webhook Bridge.
|
|
5
5
|
*
|
|
6
|
-
* Captures configured Signal K values at
|
|
6
|
+
* Captures configured Signal K values at clock-aligned intervals,
|
|
7
7
|
* stores every snapshot in a persistent SQLite FIFO queue,
|
|
8
8
|
* and delivers queued entries to the configured webhook in order.
|
|
9
9
|
* Adds output-unit labels, retries backlog on startup and after each capture,
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
const { getAvailablePaths } = require("./paths");
|
|
14
14
|
const { convertValue, getOutputUnitLabel } = require("./units");
|
|
15
15
|
const { sendWebhook } = require("./webhook");
|
|
16
|
+
const { getNextAlignedCaptureTime } = require("./schedule");
|
|
16
17
|
const storage = require("./storage");
|
|
17
18
|
|
|
18
19
|
module.exports = function (app) {
|
|
@@ -124,6 +125,36 @@ module.exports = function (app) {
|
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Schedule one capture at the next clock boundary.
|
|
130
|
+
*
|
|
131
|
+
* A new timeout is calculated after every capture rather than using
|
|
132
|
+
* setInterval(), preventing gradual timing drift.
|
|
133
|
+
*/
|
|
134
|
+
function scheduleNextCapture() {
|
|
135
|
+
if (stopped || !currentSettings) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const intervalMinutes = Number(currentSettings.sendFreq) || 10;
|
|
140
|
+
|
|
141
|
+
const nextCapture = getNextAlignedCaptureTime(intervalMinutes);
|
|
142
|
+
|
|
143
|
+
const delay = Math.max(0, nextCapture.getTime() - Date.now());
|
|
144
|
+
|
|
145
|
+
app.debug(
|
|
146
|
+
`Next webhook capture scheduled for ${nextCapture.toISOString()}`,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
captureTimer = setTimeout(() => {
|
|
150
|
+
captureTimer = null;
|
|
151
|
+
|
|
152
|
+
captureSnapshot();
|
|
153
|
+
|
|
154
|
+
scheduleNextCapture();
|
|
155
|
+
}, delay);
|
|
156
|
+
}
|
|
157
|
+
|
|
127
158
|
/**
|
|
128
159
|
* Deliver queued entries in strict FIFO order.
|
|
129
160
|
*
|
|
@@ -187,7 +218,6 @@ module.exports = function (app) {
|
|
|
187
218
|
* This preserves FIFO order and prevents newer entries
|
|
188
219
|
* overtaking an entry that has not been delivered.
|
|
189
220
|
*/
|
|
190
|
-
|
|
191
221
|
lastError = error.message;
|
|
192
222
|
deliveryState = "waiting";
|
|
193
223
|
|
|
@@ -213,17 +243,22 @@ module.exports = function (app) {
|
|
|
213
243
|
}
|
|
214
244
|
}
|
|
215
245
|
|
|
216
|
-
|
|
217
|
-
|
|
246
|
+
/*
|
|
247
|
+
* Complete deferred cleanup after any in-flight request
|
|
248
|
+
* has settled.
|
|
249
|
+
*/
|
|
218
250
|
if (stopped) {
|
|
219
251
|
storage.close();
|
|
220
252
|
}
|
|
221
253
|
}
|
|
222
254
|
}
|
|
223
255
|
|
|
224
|
-
|
|
256
|
+
/**
|
|
257
|
+
* Open storage, attempt backlog delivery and schedule the first
|
|
258
|
+
* clock-aligned capture.
|
|
259
|
+
*/
|
|
225
260
|
plugin.start = function (settings) {
|
|
226
|
-
app.debug("Starting
|
|
261
|
+
app.debug("Starting Webhook Bridge");
|
|
227
262
|
|
|
228
263
|
currentSettings = settings || {};
|
|
229
264
|
stopped = false;
|
|
@@ -248,34 +283,36 @@ module.exports = function (app) {
|
|
|
248
283
|
* sendFreq is configured in minutes.
|
|
249
284
|
*/
|
|
250
285
|
const sendFreq = Number(currentSettings.sendFreq) || 10;
|
|
251
|
-
const intervalMilliseconds = sendFreq * 60 * 1000;
|
|
252
286
|
|
|
253
287
|
app.debug(
|
|
254
288
|
`Webhook capture interval set to ${sendFreq} minute${
|
|
255
289
|
sendFreq === 1 ? "" : "s"
|
|
256
|
-
}`,
|
|
290
|
+
}, aligned to the clock`,
|
|
257
291
|
);
|
|
258
292
|
|
|
259
293
|
/*
|
|
260
294
|
* Immediately attempt to clear any backlog left from a previous
|
|
261
295
|
* offline period or server restart.
|
|
262
296
|
*
|
|
263
|
-
* We do NOT capture a new snapshot immediately.
|
|
264
|
-
*
|
|
297
|
+
* We do NOT capture a new snapshot immediately. The first new
|
|
298
|
+
* snapshot is taken at the next clock-aligned boundary.
|
|
265
299
|
*/
|
|
266
300
|
processQueue();
|
|
267
301
|
|
|
268
|
-
|
|
302
|
+
scheduleNextCapture();
|
|
269
303
|
};
|
|
270
304
|
|
|
271
|
-
|
|
305
|
+
/**
|
|
306
|
+
* Stop future captures; an in-flight delivery finishes before its
|
|
307
|
+
* worker closes storage.
|
|
308
|
+
*/
|
|
272
309
|
plugin.stop = function () {
|
|
273
|
-
app.debug("Stopping
|
|
310
|
+
app.debug("Stopping Webhook Bridge");
|
|
274
311
|
|
|
275
312
|
stopped = true;
|
|
276
313
|
|
|
277
314
|
if (captureTimer) {
|
|
278
|
-
|
|
315
|
+
clearTimeout(captureTimer);
|
|
279
316
|
captureTimer = null;
|
|
280
317
|
}
|
|
281
318
|
|
|
@@ -325,6 +362,7 @@ module.exports = function (app) {
|
|
|
325
362
|
});
|
|
326
363
|
}
|
|
327
364
|
});
|
|
365
|
+
|
|
328
366
|
router.get("/status", (req, res) => {
|
|
329
367
|
try {
|
|
330
368
|
const queueCount = storage.count();
|
|
@@ -371,8 +409,11 @@ module.exports = function (app) {
|
|
|
371
409
|
};
|
|
372
410
|
|
|
373
411
|
/**
|
|
374
|
-
* Describe webhook settings and path mappings for the schema-based
|
|
375
|
-
*
|
|
412
|
+
* Describe webhook settings and path mappings for the schema-based
|
|
413
|
+
* configuration form.
|
|
414
|
+
*
|
|
415
|
+
* Path choices are discovered when requested; the custom panel
|
|
416
|
+
* filters units by metadata.
|
|
376
417
|
*/
|
|
377
418
|
plugin.schema = function () {
|
|
378
419
|
const availablePaths = getAvailablePaths(app);
|
|
@@ -405,7 +446,7 @@ module.exports = function (app) {
|
|
|
405
446
|
type: "number",
|
|
406
447
|
title: "Send Interval",
|
|
407
448
|
description:
|
|
408
|
-
"
|
|
449
|
+
"Capture interval in minutes, aligned to the server clock. For example, 60 sends on the hour and 15 sends at :00, :15, :30 and :45.",
|
|
409
450
|
default: 10,
|
|
410
451
|
minimum: 1,
|
|
411
452
|
},
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
function getNextAlignedCaptureTime(intervalMinutes, now = new Date()) {
|
|
2
|
+
const interval = Number(intervalMinutes);
|
|
3
|
+
|
|
4
|
+
if (!Number.isFinite(interval) || interval <= 0) {
|
|
5
|
+
throw new Error("Capture interval must be greater than zero");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const startOfDay = new Date(now);
|
|
9
|
+
startOfDay.setHours(0, 0, 0, 0);
|
|
10
|
+
|
|
11
|
+
const intervalMs = interval * 60 * 1000;
|
|
12
|
+
const elapsedToday = now.getTime() - startOfDay.getTime();
|
|
13
|
+
|
|
14
|
+
const nextElapsed = Math.floor(elapsedToday / intervalMs + 1) * intervalMs;
|
|
15
|
+
|
|
16
|
+
return new Date(startOfDay.getTime() + nextElapsed);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
getNextAlignedCaptureTime,
|
|
21
|
+
};
|