@mxtommy/kip 4.6.0 → 4.7.0-beta.10
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 +2 -2
- package/package.json +5 -5
- package/plugin/history-series.service.js +125 -17
- package/plugin/index.js +163 -49
- package/plugin/kip-series-contract.js +14 -0
- package/plugin/openApi.json +90 -2
- package/plugin/sqlite-history-storage.service.js +14 -20
- package/public/3rdpartylicenses.txt +36 -0
- package/public/assets/help-docs/dashboards.md +3 -3
- package/public/assets/help-docs/datainspector.md +3 -3
- package/public/assets/help-docs/history-api.md +31 -21
- package/public/assets/help-docs/menu.json +1 -1
- package/public/assets/help-docs/nodered-control-flows.md +4 -4
- package/public/assets/help-docs/putcontrols.md +6 -6
- package/public/assets/help-docs/widget-historical-series.md +55 -11
- package/public/assets/help-docs/zones.md +1 -1
- package/public/assets/svg/icons.svg +17 -0
- package/public/{chunk-AC6VD2FN.js → chunk-2GOHQZH5.js} +4 -4
- package/public/{chunk-3JA4CQ7T.js → chunk-4YDVZHMH.js} +4 -4
- package/public/{chunk-MDNGWQNG.js → chunk-6U74K6G4.js} +7 -7
- package/public/{chunk-356CW47X.js → chunk-AQROQY2F.js} +1 -1
- package/public/{chunk-ZY3U4H4Z.js → chunk-AZC2WKQI.js} +1 -1
- package/public/{chunk-CYTLQDGF.js → chunk-BQPPRM7O.js} +1 -1
- package/public/{chunk-BMHMHQFO.js → chunk-BTVGQ4ZG.js} +2 -2
- package/public/{chunk-QU3JR4YV.js → chunk-FYDLTNP4.js} +1 -1
- package/public/{chunk-B4NYOD6L.js → chunk-IENESD5Q.js} +1 -1
- package/public/chunk-LS6AJ3JI.js +50 -0
- package/public/{chunk-PPF5S5CV.js → chunk-M37BLWHF.js} +5 -5
- package/public/chunk-MXUEYEZU.js +5 -0
- package/public/{chunk-NJISHUGY.js → chunk-PTLDR7X7.js} +1 -1
- package/public/{chunk-UYHRT3PR.js → chunk-X44BRNVL.js} +1 -1
- package/public/{chunk-ZXO4VMEH.js → chunk-Y6JCNR3H.js} +1 -1
- package/public/{chunk-MGLD6QDJ.js → chunk-YY4ZUJFI.js} +5 -5
- package/public/{chunk-5SAXWR6Z.js → chunk-Z4K5KE3I.js} +8 -8
- package/public/index.html +1 -1
- package/public/{main-I33LH3HC.js → main-775NFBN3.js} +1 -1
- package/public/chunk-6A4NRSCL.js +0 -5
- package/public/chunk-P3M6SJQT.js +0 -50
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# v4.6.0
|
|
2
2
|
## Improvements
|
|
3
|
-
* Built-in Time-Series storage and History-API provider now use the native node:sqlite feature, eliminating
|
|
3
|
+
* Built-in Time-Series storage and History-API provider now use the native node:sqlite feature, eliminating binary and external dependencies.
|
|
4
4
|
* Requires Node.js 22.5.0 or newer. If you use an older Node.js version, you must select an alternative History-API provider.
|
|
5
|
-
* **IMPORTANT:** Before upgrading Node.js, always confirm your Signal K server
|
|
5
|
+
* **IMPORTANT:** Before upgrading Node.js, always confirm your Signal K server and OS supports the required Node.js version. See the [Signal K installation documentation](https://demo.signalk.org/documentation/Installation.html).
|
|
6
6
|
## Fixes
|
|
7
7
|
* Extending v4.5.x features to VenusOS (32bit OS) - Error: Failed to start: Error loading duckdb native binding: unsupported arch 'arm' for platform 'linux'. Fixes #979
|
|
8
8
|
* Uninstallation does not remove all files. Fixes #981
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mxtommy/kip",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.0-beta.10",
|
|
4
4
|
"description": "An advanced and versatile marine instrumentation package to display Signal K data.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -76,10 +76,10 @@
|
|
|
76
76
|
"@angular/platform-browser": "21.2.1",
|
|
77
77
|
"@angular/platform-browser-dynamic": "21.2.1",
|
|
78
78
|
"@angular/router": "21.2.1",
|
|
79
|
-
"@angular-devkit/build-angular": "^21.1
|
|
79
|
+
"@angular-devkit/build-angular": "^21.2.1",
|
|
80
80
|
"@angular-devkit/schematics-cli": "^20.1.6",
|
|
81
|
-
"@angular/build": "^21.1
|
|
82
|
-
"@angular/cli": "^21.1
|
|
81
|
+
"@angular/build": "^21.2.1",
|
|
82
|
+
"@angular/cli": "^21.2.1",
|
|
83
83
|
"@angular/compiler-cli": "21.2.1",
|
|
84
84
|
"@angular/language-service": "21.2.1",
|
|
85
85
|
"@types/canvas-gauges": "^2.1.8",
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
"@types/js-quantities": "^1.6.6",
|
|
90
90
|
"@types/lodash-es": "^4.17.9",
|
|
91
91
|
"@types/node": "^24.1.0",
|
|
92
|
-
"angular-eslint": "21.
|
|
92
|
+
"angular-eslint": "21.3.0",
|
|
93
93
|
"codelyzer": "^6.0.0",
|
|
94
94
|
"eslint": "^9.29.0",
|
|
95
95
|
"jasmine-core": "~4.0.1",
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.HistorySeriesService = void 0;
|
|
3
|
+
exports.HistorySeriesService = exports.isKipTemplateSeriesDefinition = exports.isKipSeriesEnabled = exports.isKipConcreteSeriesDefinition = void 0;
|
|
4
|
+
const kip_series_contract_1 = require("./kip-series-contract");
|
|
5
|
+
Object.defineProperty(exports, "isKipConcreteSeriesDefinition", { enumerable: true, get: function () { return kip_series_contract_1.isKipConcreteSeriesDefinition; } });
|
|
6
|
+
Object.defineProperty(exports, "isKipSeriesEnabled", { enumerable: true, get: function () { return kip_series_contract_1.isKipSeriesEnabled; } });
|
|
7
|
+
Object.defineProperty(exports, "isKipTemplateSeriesDefinition", { enumerable: true, get: function () { return kip_series_contract_1.isKipTemplateSeriesDefinition; } });
|
|
4
8
|
/**
|
|
5
9
|
* Manages history capture series definitions and serves History API-compatible query results.
|
|
6
10
|
*/
|
|
7
11
|
class HistorySeriesService {
|
|
8
12
|
nowProvider;
|
|
13
|
+
selfContext;
|
|
9
14
|
seriesById = new Map();
|
|
15
|
+
enabledSeriesKeysByPath = new Map();
|
|
10
16
|
lastAcceptedTimestampBySeriesKey = new Map();
|
|
11
17
|
sampleSink = null;
|
|
12
|
-
constructor(nowProvider = () => Date.now()) {
|
|
18
|
+
constructor(nowProvider = () => Date.now(), selfContext = null) {
|
|
13
19
|
this.nowProvider = nowProvider;
|
|
20
|
+
this.selfContext = selfContext;
|
|
14
21
|
}
|
|
15
22
|
/**
|
|
16
23
|
* Returns all configured series sorted by `seriesId`.
|
|
@@ -56,6 +63,7 @@ class HistorySeriesService {
|
|
|
56
63
|
const normalized = this.normalizeSeries(input);
|
|
57
64
|
const key = normalized.seriesId;
|
|
58
65
|
this.seriesById.set(key, normalized);
|
|
66
|
+
this.rebuildEnabledPathIndex();
|
|
59
67
|
return normalized;
|
|
60
68
|
}
|
|
61
69
|
/**
|
|
@@ -90,6 +98,7 @@ class HistorySeriesService {
|
|
|
90
98
|
this.seriesById.delete(key);
|
|
91
99
|
this.lastAcceptedTimestampBySeriesKey.delete(key);
|
|
92
100
|
});
|
|
101
|
+
this.rebuildEnabledPathIndex();
|
|
93
102
|
return true;
|
|
94
103
|
}
|
|
95
104
|
/**
|
|
@@ -103,7 +112,7 @@ class HistorySeriesService {
|
|
|
103
112
|
* console.log(result.created, result.deleted);
|
|
104
113
|
*/
|
|
105
114
|
reconcileSeries(desiredSeries) {
|
|
106
|
-
const now =
|
|
115
|
+
const now = this.nowProvider();
|
|
107
116
|
const desiredById = new Map();
|
|
108
117
|
desiredSeries.forEach(entry => {
|
|
109
118
|
const normalized = this.normalizeSeries(entry);
|
|
@@ -122,7 +131,7 @@ class HistorySeriesService {
|
|
|
122
131
|
created += 1;
|
|
123
132
|
return;
|
|
124
133
|
}
|
|
125
|
-
if (
|
|
134
|
+
if (!this.areSeriesEquivalent(existing, desired)) {
|
|
126
135
|
this.seriesById.set(seriesKey, desiredWithReconcile);
|
|
127
136
|
updated += 1;
|
|
128
137
|
}
|
|
@@ -138,6 +147,7 @@ class HistorySeriesService {
|
|
|
138
147
|
deleted += 1;
|
|
139
148
|
}
|
|
140
149
|
});
|
|
150
|
+
this.rebuildEnabledPathIndex();
|
|
141
151
|
return {
|
|
142
152
|
created,
|
|
143
153
|
updated,
|
|
@@ -157,12 +167,10 @@ class HistorySeriesService {
|
|
|
157
167
|
* service.recordSample('abc', 12.4, Date.now());
|
|
158
168
|
*/
|
|
159
169
|
recordSample(seriesId, value, timestamp) {
|
|
160
|
-
|
|
161
|
-
.find(([, series]) => series.seriesId === seriesId);
|
|
162
|
-
if (!seriesEntry) {
|
|
170
|
+
if (!this.seriesById.has(seriesId)) {
|
|
163
171
|
return false;
|
|
164
172
|
}
|
|
165
|
-
return this.recordSampleByKey(
|
|
173
|
+
return this.recordSampleByKey(seriesId, value, timestamp);
|
|
166
174
|
}
|
|
167
175
|
recordSampleByKey(seriesKey, value, timestamp) {
|
|
168
176
|
const series = this.seriesById.get(seriesKey);
|
|
@@ -213,8 +221,13 @@ class HistorySeriesService {
|
|
|
213
221
|
}
|
|
214
222
|
let recorded = 0;
|
|
215
223
|
leafSamples.forEach(leaf => {
|
|
216
|
-
this.
|
|
217
|
-
|
|
224
|
+
const seriesKeys = this.enabledSeriesKeysByPath.get(leaf.path);
|
|
225
|
+
if (!seriesKeys || seriesKeys.length === 0) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
seriesKeys.forEach(seriesKey => {
|
|
229
|
+
const series = this.seriesById.get(seriesKey);
|
|
230
|
+
if (!series) {
|
|
218
231
|
return;
|
|
219
232
|
}
|
|
220
233
|
const seriesContext = series.context ?? 'vessels.self';
|
|
@@ -293,6 +306,68 @@ class HistorySeriesService {
|
|
|
293
306
|
});
|
|
294
307
|
return Array.from(contexts).sort();
|
|
295
308
|
}
|
|
309
|
+
rebuildEnabledPathIndex() {
|
|
310
|
+
this.enabledSeriesKeysByPath.clear();
|
|
311
|
+
this.seriesById.forEach((series, seriesKey) => {
|
|
312
|
+
if (series.enabled === false) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const keys = this.enabledSeriesKeysByPath.get(series.path) ?? [];
|
|
316
|
+
keys.push(seriesKey);
|
|
317
|
+
this.enabledSeriesKeysByPath.set(series.path, keys);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
areSeriesEquivalent(left, right) {
|
|
321
|
+
const leftComparable = this.toComparableSeries(left);
|
|
322
|
+
const rightComparable = this.toComparableSeries(right);
|
|
323
|
+
return leftComparable.seriesId === rightComparable.seriesId
|
|
324
|
+
&& leftComparable.datasetUuid === rightComparable.datasetUuid
|
|
325
|
+
&& leftComparable.ownerWidgetUuid === rightComparable.ownerWidgetUuid
|
|
326
|
+
&& leftComparable.ownerWidgetSelector === rightComparable.ownerWidgetSelector
|
|
327
|
+
&& leftComparable.path === rightComparable.path
|
|
328
|
+
&& leftComparable.expansionMode === rightComparable.expansionMode
|
|
329
|
+
&& this.areStringArraysEquivalent(leftComparable.allowedBatteryIds, rightComparable.allowedBatteryIds)
|
|
330
|
+
&& leftComparable.source === rightComparable.source
|
|
331
|
+
&& leftComparable.context === rightComparable.context
|
|
332
|
+
&& leftComparable.timeScale === rightComparable.timeScale
|
|
333
|
+
&& leftComparable.period === rightComparable.period
|
|
334
|
+
&& leftComparable.retentionDurationMs === rightComparable.retentionDurationMs
|
|
335
|
+
&& leftComparable.sampleTime === rightComparable.sampleTime
|
|
336
|
+
&& leftComparable.enabled === rightComparable.enabled
|
|
337
|
+
&& this.areStringArraysEquivalent(leftComparable.methods, rightComparable.methods);
|
|
338
|
+
}
|
|
339
|
+
toComparableSeries(series) {
|
|
340
|
+
const { reconcileTs, ...comparable } = series;
|
|
341
|
+
void reconcileTs;
|
|
342
|
+
return {
|
|
343
|
+
...comparable,
|
|
344
|
+
allowedBatteryIds: this.normalizeComparableStringArray(comparable.allowedBatteryIds),
|
|
345
|
+
methods: this.normalizeComparableStringArray(comparable.methods)
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
areStringArraysEquivalent(left, right) {
|
|
349
|
+
const normalizedLeft = this.normalizeComparableStringArray(left) ?? [];
|
|
350
|
+
const normalizedRight = this.normalizeComparableStringArray(right) ?? [];
|
|
351
|
+
if (normalizedLeft.length !== normalizedRight.length) {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
return normalizedLeft.every((value, index) => value === normalizedRight[index]);
|
|
355
|
+
}
|
|
356
|
+
normalizeComparableStringArray(values) {
|
|
357
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
return [...values]
|
|
361
|
+
.filter((value) => typeof value === 'string')
|
|
362
|
+
.sort((left, right) => left.localeCompare(right));
|
|
363
|
+
}
|
|
364
|
+
isChartWidget(ownerWidgetSelector, ownerWidgetUuid) {
|
|
365
|
+
if (ownerWidgetSelector === 'widget-data-chart' || ownerWidgetSelector === 'widget-windtrends-chart') {
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
return ownerWidgetUuid?.startsWith('widget-windtrends-chart') === true
|
|
369
|
+
|| ownerWidgetUuid?.startsWith('widget-data-chart') === true;
|
|
370
|
+
}
|
|
296
371
|
normalizeSeries(input) {
|
|
297
372
|
const seriesId = (input.seriesId || input.datasetUuid || '').trim();
|
|
298
373
|
if (!seriesId) {
|
|
@@ -310,8 +385,16 @@ class HistorySeriesService {
|
|
|
310
385
|
if (!path) {
|
|
311
386
|
throw new Error('path is required');
|
|
312
387
|
}
|
|
313
|
-
|
|
314
|
-
const
|
|
388
|
+
const ownerWidgetSelector = typeof input.ownerWidgetSelector === 'string' ? input.ownerWidgetSelector.trim() : null;
|
|
389
|
+
const expansionMode = input.expansionMode ?? null;
|
|
390
|
+
if (expansionMode === 'bms-battery-tree' && ownerWidgetSelector !== 'widget-bms') {
|
|
391
|
+
throw new Error('BMS template series must use ownerWidgetSelector "widget-bms"');
|
|
392
|
+
}
|
|
393
|
+
const normalizedMethods = this.normalizeComparableStringArray(input.methods);
|
|
394
|
+
const normalizedAllowedBatteryIds = expansionMode === 'bms-battery-tree'
|
|
395
|
+
? this.normalizeComparableStringArray(input.allowedBatteryIds)
|
|
396
|
+
: undefined;
|
|
397
|
+
const isDataWidget = this.isChartWidget(ownerWidgetSelector, ownerWidgetUuid);
|
|
315
398
|
const retentionMs = this.resolveRetentionMs(input);
|
|
316
399
|
let sampleTime;
|
|
317
400
|
if (isDataWidget) {
|
|
@@ -324,18 +407,37 @@ class HistorySeriesService {
|
|
|
324
407
|
// a good median amount of samples for the dynamically queryable chart display range (15 min up to 24h).
|
|
325
408
|
sampleTime = 15000; // ms
|
|
326
409
|
}
|
|
327
|
-
|
|
328
|
-
...input,
|
|
410
|
+
const normalizedBase = {
|
|
329
411
|
seriesId,
|
|
330
412
|
datasetUuid,
|
|
331
413
|
ownerWidgetUuid,
|
|
414
|
+
ownerWidgetSelector,
|
|
332
415
|
path,
|
|
333
416
|
source: input.source ?? 'default',
|
|
334
417
|
context: input.context ?? 'vessels.self',
|
|
418
|
+
timeScale: input.timeScale ?? null,
|
|
419
|
+
period: Number.isFinite(input.period) ? input.period ?? null : null,
|
|
335
420
|
enabled: input.enabled !== false,
|
|
336
421
|
retentionDurationMs: retentionMs,
|
|
337
|
-
sampleTime
|
|
422
|
+
sampleTime,
|
|
423
|
+
methods: normalizedMethods,
|
|
424
|
+
reconcileTs: input.reconcileTs
|
|
338
425
|
};
|
|
426
|
+
if (expansionMode === 'bms-battery-tree') {
|
|
427
|
+
const templateSeries = {
|
|
428
|
+
...normalizedBase,
|
|
429
|
+
ownerWidgetSelector: 'widget-bms',
|
|
430
|
+
expansionMode,
|
|
431
|
+
allowedBatteryIds: normalizedAllowedBatteryIds ?? null
|
|
432
|
+
};
|
|
433
|
+
return templateSeries;
|
|
434
|
+
}
|
|
435
|
+
const concreteSeries = {
|
|
436
|
+
...normalizedBase,
|
|
437
|
+
expansionMode: null,
|
|
438
|
+
allowedBatteryIds: null
|
|
439
|
+
};
|
|
440
|
+
return concreteSeries;
|
|
339
441
|
}
|
|
340
442
|
resolveRetentionMs(series) {
|
|
341
443
|
if (Number.isFinite(series.retentionDurationMs) && series.retentionDurationMs > 0) {
|
|
@@ -408,11 +510,17 @@ class HistorySeriesService {
|
|
|
408
510
|
if (seriesContext === sampleContext) {
|
|
409
511
|
return true;
|
|
410
512
|
}
|
|
411
|
-
if (seriesContext
|
|
412
|
-
return
|
|
513
|
+
if (this.isSelfContext(seriesContext) && this.isSelfContext(sampleContext)) {
|
|
514
|
+
return true;
|
|
413
515
|
}
|
|
414
516
|
return false;
|
|
415
517
|
}
|
|
518
|
+
isSelfContext(context) {
|
|
519
|
+
if (context === 'vessels.self') {
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
return !!this.selfContext && context === this.selfContext;
|
|
523
|
+
}
|
|
416
524
|
isSourceMatch(seriesSource, sampleSource) {
|
|
417
525
|
if (seriesSource === '*' || seriesSource === 'any') {
|
|
418
526
|
return true;
|
package/plugin/index.js
CHANGED
|
@@ -37,6 +37,14 @@ const server_api_1 = require("@signalk/server-api");
|
|
|
37
37
|
const openapi = __importStar(require("./openApi.json"));
|
|
38
38
|
const history_series_service_1 = require("./history-series.service");
|
|
39
39
|
const sqlite_history_storage_service_1 = require("./sqlite-history-storage.service");
|
|
40
|
+
async function defaultGetSqliteModule() {
|
|
41
|
+
try {
|
|
42
|
+
return await Promise.resolve().then(() => __importStar(require('node:sqlite')));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
40
48
|
const start = (server) => {
|
|
41
49
|
const mutableOpenApi = JSON.parse(JSON.stringify(openapi.default ?? openapi));
|
|
42
50
|
const API_PATHS = {
|
|
@@ -79,15 +87,30 @@ const start = (server) => {
|
|
|
79
87
|
}
|
|
80
88
|
}
|
|
81
89
|
};
|
|
82
|
-
const historySeries = new history_series_service_1.HistorySeriesService(() => Date.now());
|
|
83
|
-
const
|
|
90
|
+
const historySeries = new history_series_service_1.HistorySeriesService(() => Date.now(), typeof server.selfId === 'string' && server.selfId.trim().length > 0 ? `vessels.${server.selfId.trim()}` : null);
|
|
91
|
+
const resolveDataDirPath = () => {
|
|
92
|
+
const serverCompat = server;
|
|
93
|
+
const getter = typeof serverCompat.getDataDirPath === 'function'
|
|
94
|
+
? serverCompat.getDataDirPath.bind(serverCompat)
|
|
95
|
+
: (typeof serverCompat.app?.getDataDirPath === 'function'
|
|
96
|
+
? serverCompat.app.getDataDirPath.bind(serverCompat.app)
|
|
97
|
+
: null);
|
|
98
|
+
if (!getter) {
|
|
99
|
+
throw new Error('Signal K Server API does not expose getDataDirPath() on server or server.app');
|
|
100
|
+
}
|
|
101
|
+
const dataDirPath = getter();
|
|
102
|
+
if (typeof dataDirPath !== 'string' || dataDirPath.trim().length === 0) {
|
|
103
|
+
throw new Error('Signal K Server API returned an invalid data directory path');
|
|
104
|
+
}
|
|
105
|
+
return dataDirPath.trim();
|
|
106
|
+
};
|
|
107
|
+
const storageService = new sqlite_history_storage_service_1.SqliteHistoryStorageService(resolveDataDirPath());
|
|
84
108
|
let retentionSweepTimer = null;
|
|
85
109
|
let storageFlushTimer = null;
|
|
86
110
|
let sqliteInitializationPromise = null;
|
|
87
111
|
const SQLITE_INIT_WAIT_TIMEOUT_MS = 5000;
|
|
88
112
|
const MIN_NODE_SQLITE_VERSION = '22.5.0';
|
|
89
113
|
let streamUnsubscribes = [];
|
|
90
|
-
let historyApiRegistry = null;
|
|
91
114
|
let historyApiProviderRegistered = false;
|
|
92
115
|
let runtimeSqliteUnavailableMessage = null;
|
|
93
116
|
function logRuntimeDependencyVersions() {
|
|
@@ -95,16 +118,12 @@ const start = (server) => {
|
|
|
95
118
|
const sqliteAvailability = modeConfig && modeConfig.nodeSqliteAvailable ? 'available' : 'unavailable';
|
|
96
119
|
server.debug(`[KIP][RUNTIME] ${nodeIdentity} node:sqlite=${sqliteAvailability}`);
|
|
97
120
|
}
|
|
98
|
-
async function getSqliteModule() {
|
|
99
|
-
try {
|
|
100
|
-
return await Promise.resolve().then(() => __importStar(require('node:sqlite')));
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
121
|
async function detectSqliteRuntime() {
|
|
107
|
-
const
|
|
122
|
+
const exportedStart = start;
|
|
123
|
+
const resolveSqliteModule = typeof exportedStart.getSqliteModule === 'function'
|
|
124
|
+
? exportedStart.getSqliteModule
|
|
125
|
+
: defaultGetSqliteModule;
|
|
126
|
+
const sqliteModule = await resolveSqliteModule();
|
|
108
127
|
if (!sqliteModule) {
|
|
109
128
|
runtimeSqliteUnavailableMessage = `node:sqlite requires Node ${MIN_NODE_SQLITE_VERSION}+`;
|
|
110
129
|
return false;
|
|
@@ -145,6 +164,95 @@ const start = (server) => {
|
|
|
145
164
|
nodeSqliteAvailable: nodeSqliteAvailable !== false
|
|
146
165
|
};
|
|
147
166
|
}
|
|
167
|
+
function slugify(value) {
|
|
168
|
+
return value
|
|
169
|
+
.toLowerCase()
|
|
170
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
171
|
+
.replace(/^-+|-+$/g, '');
|
|
172
|
+
}
|
|
173
|
+
function resolveBmsBatteryIdsFromSelfPath() {
|
|
174
|
+
const batteriesPath = server.getSelfPath('electrical.batteries');
|
|
175
|
+
const readCandidate = (node) => {
|
|
176
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
const root = node;
|
|
180
|
+
if (Object.prototype.hasOwnProperty.call(root, 'value')) {
|
|
181
|
+
const value = root.value;
|
|
182
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
return root;
|
|
188
|
+
};
|
|
189
|
+
const candidates = readCandidate(batteriesPath);
|
|
190
|
+
if (!candidates) {
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
return Object.keys(candidates)
|
|
194
|
+
.filter(id => /^[a-z0-9_-]+$/i.test(id))
|
|
195
|
+
.sort((left, right) => left.localeCompare(right));
|
|
196
|
+
}
|
|
197
|
+
function getExistingConcreteBmsSeries(templateSeries, existingSeries) {
|
|
198
|
+
return existingSeries
|
|
199
|
+
.filter(series => series.ownerWidgetUuid === templateSeries.ownerWidgetUuid)
|
|
200
|
+
.filter(history_series_service_1.isKipConcreteSeriesDefinition)
|
|
201
|
+
.filter(series => series.seriesId !== templateSeries.seriesId)
|
|
202
|
+
.map(series => ({ ...series }));
|
|
203
|
+
}
|
|
204
|
+
function mergeSeriesDefinitions(series) {
|
|
205
|
+
const mergedById = new Map();
|
|
206
|
+
series.forEach(item => {
|
|
207
|
+
mergedById.set(item.seriesId, item);
|
|
208
|
+
});
|
|
209
|
+
return Array.from(mergedById.values());
|
|
210
|
+
}
|
|
211
|
+
function expandTemplateSeriesDefinitions(payload, existingSeries = []) {
|
|
212
|
+
const bmsMetrics = ['capacity.stateOfCharge', 'current'];
|
|
213
|
+
const expandedById = new Map();
|
|
214
|
+
const discoveredBatteryIds = resolveBmsBatteryIdsFromSelfPath();
|
|
215
|
+
payload.forEach(series => {
|
|
216
|
+
if (!(0, history_series_service_1.isKipTemplateSeriesDefinition)(series)) {
|
|
217
|
+
expandedById.set(series.seriesId, series);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (discoveredBatteryIds.length === 0) {
|
|
221
|
+
getExistingConcreteBmsSeries(series, existingSeries).forEach(existing => {
|
|
222
|
+
expandedById.set(existing.seriesId, existing);
|
|
223
|
+
});
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const allowedBatteryIds = Array.isArray(series.allowedBatteryIds)
|
|
227
|
+
? series.allowedBatteryIds
|
|
228
|
+
.filter((id) => typeof id === 'string')
|
|
229
|
+
.map(id => id.trim())
|
|
230
|
+
.filter(id => id.length > 0)
|
|
231
|
+
: [];
|
|
232
|
+
const allowedSet = allowedBatteryIds.length > 0 ? new Set(allowedBatteryIds) : null;
|
|
233
|
+
const batteryIds = discoveredBatteryIds.filter(id => !allowedSet || allowedSet.has(id));
|
|
234
|
+
if (batteryIds.length === 0) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const source = series.source ?? 'default';
|
|
238
|
+
const sourceKey = slugify(source || 'default') || 'default';
|
|
239
|
+
batteryIds.forEach(batteryId => {
|
|
240
|
+
bmsMetrics.forEach(metric => {
|
|
241
|
+
const path = `self.electrical.batteries.${batteryId}.${metric}`;
|
|
242
|
+
const seriesId = `${series.ownerWidgetUuid}:bms:${batteryId}:${metric}:${sourceKey}`;
|
|
243
|
+
expandedById.set(seriesId, {
|
|
244
|
+
...series,
|
|
245
|
+
seriesId,
|
|
246
|
+
datasetUuid: `${series.ownerWidgetUuid}:bms:${batteryId}:${metric}:${sourceKey}`,
|
|
247
|
+
path,
|
|
248
|
+
retentionDurationMs: Number.isFinite(series.retentionDurationMs) ? series.retentionDurationMs : 24 * 60 * 60 * 1000,
|
|
249
|
+
expansionMode: null
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
return Array.from(expandedById.values());
|
|
255
|
+
}
|
|
148
256
|
function getDisplaySelfPath(displayId, suffix) {
|
|
149
257
|
const tail = suffix ? `.${suffix}` : '';
|
|
150
258
|
const want = `displays.${displayId}${tail}`;
|
|
@@ -470,7 +578,17 @@ const start = (server) => {
|
|
|
470
578
|
server.debug('[KIP][HISTORY_PROVIDER] registration skipped reason=config-disabled');
|
|
471
579
|
return;
|
|
472
580
|
}
|
|
473
|
-
const
|
|
581
|
+
const serverWithHistoryApi = server;
|
|
582
|
+
const registerHistoryApiProvider = typeof serverWithHistoryApi.registerHistoryApiProvider === 'function'
|
|
583
|
+
? serverWithHistoryApi.registerHistoryApiProvider.bind(serverWithHistoryApi)
|
|
584
|
+
: (typeof serverWithHistoryApi.history?.registerHistoryApiProvider === 'function'
|
|
585
|
+
? serverWithHistoryApi.history.registerHistoryApiProvider.bind(serverWithHistoryApi.history)
|
|
586
|
+
: null);
|
|
587
|
+
// guard when running in SK variants that do not support History API registration
|
|
588
|
+
if (!registerHistoryApiProvider) {
|
|
589
|
+
server.debug('[KIP][HISTORY_PROVIDER] registration skipped reason=api-unavailable');
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
474
592
|
const apiProvider = {
|
|
475
593
|
getValues: async (query) => {
|
|
476
594
|
const resolved = await resolveHistoryValues(buildHistoryQueryFromValuesRequest(query));
|
|
@@ -485,26 +603,9 @@ const start = (server) => {
|
|
|
485
603
|
getPaths: (query) => resolveHistoryPaths(buildHistoryQueryFromRangeRequest(query)),
|
|
486
604
|
getContexts: (query) => resolveHistoryContexts(buildHistoryQueryFromRangeRequest(query))
|
|
487
605
|
};
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
? {
|
|
492
|
-
registerHistoryApiProvider: host.registerHistoryApiProvider.bind(host),
|
|
493
|
-
unregisterHistoryApiProvider: typeof host.unregisterHistoryApiProvider === 'function'
|
|
494
|
-
? host.unregisterHistoryApiProvider.bind(host)
|
|
495
|
-
: undefined
|
|
496
|
-
}
|
|
497
|
-
: null);
|
|
498
|
-
if (registry && typeof registry.registerHistoryApiProvider === 'function') {
|
|
499
|
-
registry.registerHistoryApiProvider(apiProvider);
|
|
500
|
-
historyApiProviderRegistered = true;
|
|
501
|
-
if (typeof registry.unregisterHistoryApiProvider === 'function') {
|
|
502
|
-
historyApiRegistry = { unregisterHistoryApiProvider: registry.unregisterHistoryApiProvider.bind(registry) };
|
|
503
|
-
}
|
|
504
|
-
server.debug('[KIP][HISTORY_PROVIDER] registration success provider=kip');
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
server.debug('[KIP][HISTORY_PROVIDER] registration skipped reason=api-unavailable');
|
|
606
|
+
registerHistoryApiProvider(apiProvider);
|
|
607
|
+
historyApiProviderRegistered = true;
|
|
608
|
+
server.debug('[KIP][HISTORY_PROVIDER] registration success provider=kip');
|
|
508
609
|
}
|
|
509
610
|
function rebuildSeriesCaptureSubscriptions() {
|
|
510
611
|
stopSeriesCapture();
|
|
@@ -530,7 +631,7 @@ const start = (server) => {
|
|
|
530
631
|
// If any series for this path requires non-self context, force generic bus subscription.
|
|
531
632
|
existing.allSelfContext = existing.allSelfContext && allSelfContext;
|
|
532
633
|
};
|
|
533
|
-
historySeries.listSeries().filter(
|
|
634
|
+
historySeries.listSeries().filter(history_series_service_1.isKipSeriesEnabled).forEach(series => {
|
|
534
635
|
const allSelfContext = (series.context ?? 'vessels.self') === 'vessels.self';
|
|
535
636
|
addCandidate(series.path, allSelfContext);
|
|
536
637
|
// Workaround: subscribe to immediate parent path so object deltas (e.g. navigation.attitude)
|
|
@@ -603,9 +704,6 @@ const start = (server) => {
|
|
|
603
704
|
if (!modeConfig.nodeSqliteAvailable) {
|
|
604
705
|
server.error(`[KIP][RUNTIME] node:sqlite unavailable. ${runtimeSqliteUnavailableMessage}`);
|
|
605
706
|
}
|
|
606
|
-
const serverWithApp = server;
|
|
607
|
-
const dataDirPath = serverWithApp.app?.getDataDirPath?.();
|
|
608
|
-
storageService.setDataDirPath(typeof dataDirPath === 'string' ? dataDirPath : null);
|
|
609
707
|
storageService.setRuntimeAvailability(modeConfig.nodeSqliteAvailable, runtimeSqliteUnavailableMessage ?? undefined);
|
|
610
708
|
logRuntimeDependencyVersions();
|
|
611
709
|
logOperationalMode('start-configured');
|
|
@@ -742,16 +840,16 @@ const start = (server) => {
|
|
|
742
840
|
.catch(() => undefined)
|
|
743
841
|
.then(() => storageService.close(storageLifecycleToken))
|
|
744
842
|
.catch(() => undefined);
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
historyApiRegistry = null;
|
|
843
|
+
const serverWithHistoryApi = server;
|
|
844
|
+
const unregisterHistoryApiProvider = typeof serverWithHistoryApi.unregisterHistoryApiProvider === 'function'
|
|
845
|
+
? serverWithHistoryApi.unregisterHistoryApiProvider.bind(serverWithHistoryApi)
|
|
846
|
+
: (typeof serverWithHistoryApi.history?.unregisterHistoryApiProvider === 'function'
|
|
847
|
+
? serverWithHistoryApi.history.unregisterHistoryApiProvider.bind(serverWithHistoryApi.history)
|
|
848
|
+
: null);
|
|
849
|
+
if (unregisterHistoryApiProvider) {
|
|
850
|
+
unregisterHistoryApiProvider();
|
|
754
851
|
}
|
|
852
|
+
historyApiProviderRegistered = false;
|
|
755
853
|
sqliteInitializationPromise = null;
|
|
756
854
|
const msg = 'Stopped.';
|
|
757
855
|
server.setPluginStatus(msg);
|
|
@@ -1057,14 +1155,28 @@ const start = (server) => {
|
|
|
1057
1155
|
if (!Array.isArray(payload)) {
|
|
1058
1156
|
return sendFail(res, 400, 'Body must be an array of series definitions');
|
|
1059
1157
|
}
|
|
1060
|
-
const simulated = new history_series_service_1.HistorySeriesService(() => Date.now());
|
|
1061
|
-
|
|
1158
|
+
const simulated = new history_series_service_1.HistorySeriesService(() => Date.now(), typeof server.selfId === 'string' && server.selfId.trim().length > 0 ? `vessels.${server.selfId.trim()}` : null);
|
|
1159
|
+
const currentSeries = mergeSeriesDefinitions([
|
|
1160
|
+
...(await storageService.getSeriesDefinitions()),
|
|
1161
|
+
...historySeries.listSeries()
|
|
1162
|
+
]);
|
|
1163
|
+
currentSeries.forEach(series => {
|
|
1062
1164
|
simulated.upsertSeries(series);
|
|
1063
1165
|
});
|
|
1064
1166
|
const scopedPayload = payload.map(series => ({
|
|
1065
1167
|
...series
|
|
1066
1168
|
}));
|
|
1067
|
-
const
|
|
1169
|
+
const isBatteryDiscoveryUnavailable = resolveBmsBatteryIdsFromSelfPath().length === 0;
|
|
1170
|
+
const preservedBmsSeries = isBatteryDiscoveryUnavailable
|
|
1171
|
+
? scopedPayload
|
|
1172
|
+
.filter(history_series_service_1.isKipTemplateSeriesDefinition)
|
|
1173
|
+
.flatMap(series => currentSeries.filter(current => current.ownerWidgetUuid === series.ownerWidgetUuid && (0, history_series_service_1.isKipConcreteSeriesDefinition)(current) && current.seriesId !== series.seriesId))
|
|
1174
|
+
: [];
|
|
1175
|
+
const expandedPayload = mergeSeriesDefinitions([
|
|
1176
|
+
...expandTemplateSeriesDefinitions(scopedPayload, currentSeries),
|
|
1177
|
+
...preservedBmsSeries
|
|
1178
|
+
]);
|
|
1179
|
+
const result = simulated.reconcileSeries(expandedPayload);
|
|
1068
1180
|
const nextSeries = simulated.listSeries();
|
|
1069
1181
|
await storageService.replaceSeriesDefinitions(nextSeries);
|
|
1070
1182
|
const seriesOutsideScope = historySeries.listSeries();
|
|
@@ -1093,4 +1205,6 @@ const start = (server) => {
|
|
|
1093
1205
|
};
|
|
1094
1206
|
return plugin;
|
|
1095
1207
|
};
|
|
1208
|
+
const startWithHooks = start;
|
|
1209
|
+
startWithHooks.getSqliteModule = defaultGetSqliteModule;
|
|
1096
1210
|
module.exports = start;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isKipTemplateSeriesDefinition = isKipTemplateSeriesDefinition;
|
|
4
|
+
exports.isKipConcreteSeriesDefinition = isKipConcreteSeriesDefinition;
|
|
5
|
+
exports.isKipSeriesEnabled = isKipSeriesEnabled;
|
|
6
|
+
function isKipTemplateSeriesDefinition(series) {
|
|
7
|
+
return series.expansionMode === 'bms-battery-tree';
|
|
8
|
+
}
|
|
9
|
+
function isKipConcreteSeriesDefinition(series) {
|
|
10
|
+
return series.expansionMode == null;
|
|
11
|
+
}
|
|
12
|
+
function isKipSeriesEnabled(series) {
|
|
13
|
+
return series.enabled;
|
|
14
|
+
}
|