@mxtommy/kip 4.6.0 → 4.7.0-beta.3
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 +91 -13
- package/plugin/index.js +148 -48
- package/plugin/kip-plugin/src/history-series.service.js +537 -0
- package/plugin/kip-plugin/src/index.js +1194 -0
- package/plugin/kip-plugin/src/openApi.json +787 -0
- package/plugin/kip-plugin/src/sqlite-history-storage.service.js +1122 -0
- package/plugin/openApi.json +14 -0
- package/plugin/sqlite-history-storage.service.js +13 -19
- package/plugin/src/app/core/contracts/kip-series-contract.js +14 -0
- package/plugin/src/app/core/contracts/kip-series-contract.spec.js +68 -0
- 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-UYHRT3PR.js → chunk-7ZZK5KBC.js} +1 -1
- package/public/{chunk-MDNGWQNG.js → chunk-AB34PRGT.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-CRJ2S7XQ.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-PPF5S5CV.js → chunk-M37BLWHF.js} +5 -5
- package/public/chunk-MXUEYEZU.js +5 -0
- package/public/chunk-OXKOGBYN.js +50 -0
- package/public/{chunk-NJISHUGY.js → chunk-PTLDR7X7.js} +1 -1
- package/public/{chunk-5SAXWR6Z.js → chunk-QV7OHVD5.js} +8 -8
- package/public/{chunk-ZXO4VMEH.js → chunk-SDPSUOCJ.js} +1 -1
- package/public/{chunk-MGLD6QDJ.js → chunk-YY4ZUJFI.js} +5 -5
- package/public/index.html +1 -1
- package/public/{main-I33LH3HC.js → main-5HQ6OYM7.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.3",
|
|
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",
|
|
@@ -6,11 +6,14 @@ exports.HistorySeriesService = void 0;
|
|
|
6
6
|
*/
|
|
7
7
|
class HistorySeriesService {
|
|
8
8
|
nowProvider;
|
|
9
|
+
selfContext;
|
|
9
10
|
seriesById = new Map();
|
|
11
|
+
enabledSeriesKeysByPath = new Map();
|
|
10
12
|
lastAcceptedTimestampBySeriesKey = new Map();
|
|
11
13
|
sampleSink = null;
|
|
12
|
-
constructor(nowProvider = () => Date.now()) {
|
|
14
|
+
constructor(nowProvider = () => Date.now(), selfContext = null) {
|
|
13
15
|
this.nowProvider = nowProvider;
|
|
16
|
+
this.selfContext = selfContext;
|
|
14
17
|
}
|
|
15
18
|
/**
|
|
16
19
|
* Returns all configured series sorted by `seriesId`.
|
|
@@ -56,6 +59,7 @@ class HistorySeriesService {
|
|
|
56
59
|
const normalized = this.normalizeSeries(input);
|
|
57
60
|
const key = normalized.seriesId;
|
|
58
61
|
this.seriesById.set(key, normalized);
|
|
62
|
+
this.rebuildEnabledPathIndex();
|
|
59
63
|
return normalized;
|
|
60
64
|
}
|
|
61
65
|
/**
|
|
@@ -90,6 +94,7 @@ class HistorySeriesService {
|
|
|
90
94
|
this.seriesById.delete(key);
|
|
91
95
|
this.lastAcceptedTimestampBySeriesKey.delete(key);
|
|
92
96
|
});
|
|
97
|
+
this.rebuildEnabledPathIndex();
|
|
93
98
|
return true;
|
|
94
99
|
}
|
|
95
100
|
/**
|
|
@@ -103,7 +108,7 @@ class HistorySeriesService {
|
|
|
103
108
|
* console.log(result.created, result.deleted);
|
|
104
109
|
*/
|
|
105
110
|
reconcileSeries(desiredSeries) {
|
|
106
|
-
const now =
|
|
111
|
+
const now = this.nowProvider();
|
|
107
112
|
const desiredById = new Map();
|
|
108
113
|
desiredSeries.forEach(entry => {
|
|
109
114
|
const normalized = this.normalizeSeries(entry);
|
|
@@ -122,7 +127,7 @@ class HistorySeriesService {
|
|
|
122
127
|
created += 1;
|
|
123
128
|
return;
|
|
124
129
|
}
|
|
125
|
-
if (
|
|
130
|
+
if (!this.areSeriesEquivalent(existing, desired)) {
|
|
126
131
|
this.seriesById.set(seriesKey, desiredWithReconcile);
|
|
127
132
|
updated += 1;
|
|
128
133
|
}
|
|
@@ -138,6 +143,7 @@ class HistorySeriesService {
|
|
|
138
143
|
deleted += 1;
|
|
139
144
|
}
|
|
140
145
|
});
|
|
146
|
+
this.rebuildEnabledPathIndex();
|
|
141
147
|
return {
|
|
142
148
|
created,
|
|
143
149
|
updated,
|
|
@@ -157,12 +163,10 @@ class HistorySeriesService {
|
|
|
157
163
|
* service.recordSample('abc', 12.4, Date.now());
|
|
158
164
|
*/
|
|
159
165
|
recordSample(seriesId, value, timestamp) {
|
|
160
|
-
|
|
161
|
-
.find(([, series]) => series.seriesId === seriesId);
|
|
162
|
-
if (!seriesEntry) {
|
|
166
|
+
if (!this.seriesById.has(seriesId)) {
|
|
163
167
|
return false;
|
|
164
168
|
}
|
|
165
|
-
return this.recordSampleByKey(
|
|
169
|
+
return this.recordSampleByKey(seriesId, value, timestamp);
|
|
166
170
|
}
|
|
167
171
|
recordSampleByKey(seriesKey, value, timestamp) {
|
|
168
172
|
const series = this.seriesById.get(seriesKey);
|
|
@@ -213,8 +217,13 @@ class HistorySeriesService {
|
|
|
213
217
|
}
|
|
214
218
|
let recorded = 0;
|
|
215
219
|
leafSamples.forEach(leaf => {
|
|
216
|
-
this.
|
|
217
|
-
|
|
220
|
+
const seriesKeys = this.enabledSeriesKeysByPath.get(leaf.path);
|
|
221
|
+
if (!seriesKeys || seriesKeys.length === 0) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
seriesKeys.forEach(seriesKey => {
|
|
225
|
+
const series = this.seriesById.get(seriesKey);
|
|
226
|
+
if (!series) {
|
|
218
227
|
return;
|
|
219
228
|
}
|
|
220
229
|
const seriesContext = series.context ?? 'vessels.self';
|
|
@@ -293,6 +302,68 @@ class HistorySeriesService {
|
|
|
293
302
|
});
|
|
294
303
|
return Array.from(contexts).sort();
|
|
295
304
|
}
|
|
305
|
+
rebuildEnabledPathIndex() {
|
|
306
|
+
this.enabledSeriesKeysByPath.clear();
|
|
307
|
+
this.seriesById.forEach((series, seriesKey) => {
|
|
308
|
+
if (series.enabled === false) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const keys = this.enabledSeriesKeysByPath.get(series.path) ?? [];
|
|
312
|
+
keys.push(seriesKey);
|
|
313
|
+
this.enabledSeriesKeysByPath.set(series.path, keys);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
areSeriesEquivalent(left, right) {
|
|
317
|
+
const leftComparable = this.toComparableSeries(left);
|
|
318
|
+
const rightComparable = this.toComparableSeries(right);
|
|
319
|
+
return leftComparable.seriesId === rightComparable.seriesId
|
|
320
|
+
&& leftComparable.datasetUuid === rightComparable.datasetUuid
|
|
321
|
+
&& leftComparable.ownerWidgetUuid === rightComparable.ownerWidgetUuid
|
|
322
|
+
&& leftComparable.ownerWidgetSelector === rightComparable.ownerWidgetSelector
|
|
323
|
+
&& leftComparable.path === rightComparable.path
|
|
324
|
+
&& leftComparable.expansionMode === rightComparable.expansionMode
|
|
325
|
+
&& this.areStringArraysEquivalent(leftComparable.allowedBatteryIds, rightComparable.allowedBatteryIds)
|
|
326
|
+
&& leftComparable.source === rightComparable.source
|
|
327
|
+
&& leftComparable.context === rightComparable.context
|
|
328
|
+
&& leftComparable.timeScale === rightComparable.timeScale
|
|
329
|
+
&& leftComparable.period === rightComparable.period
|
|
330
|
+
&& leftComparable.retentionDurationMs === rightComparable.retentionDurationMs
|
|
331
|
+
&& leftComparable.sampleTime === rightComparable.sampleTime
|
|
332
|
+
&& leftComparable.enabled === rightComparable.enabled
|
|
333
|
+
&& this.areStringArraysEquivalent(leftComparable.methods, rightComparable.methods);
|
|
334
|
+
}
|
|
335
|
+
toComparableSeries(series) {
|
|
336
|
+
const { reconcileTs, ...comparable } = series;
|
|
337
|
+
void reconcileTs;
|
|
338
|
+
return {
|
|
339
|
+
...comparable,
|
|
340
|
+
allowedBatteryIds: this.normalizeComparableStringArray(comparable.allowedBatteryIds),
|
|
341
|
+
methods: this.normalizeComparableStringArray(comparable.methods)
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
areStringArraysEquivalent(left, right) {
|
|
345
|
+
const normalizedLeft = this.normalizeComparableStringArray(left) ?? [];
|
|
346
|
+
const normalizedRight = this.normalizeComparableStringArray(right) ?? [];
|
|
347
|
+
if (normalizedLeft.length !== normalizedRight.length) {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
return normalizedLeft.every((value, index) => value === normalizedRight[index]);
|
|
351
|
+
}
|
|
352
|
+
normalizeComparableStringArray(values) {
|
|
353
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
354
|
+
return undefined;
|
|
355
|
+
}
|
|
356
|
+
return [...values]
|
|
357
|
+
.filter((value) => typeof value === 'string')
|
|
358
|
+
.sort((left, right) => left.localeCompare(right));
|
|
359
|
+
}
|
|
360
|
+
isChartWidget(ownerWidgetSelector, ownerWidgetUuid) {
|
|
361
|
+
if (ownerWidgetSelector === 'widget-data-chart' || ownerWidgetSelector === 'widget-windtrends-chart') {
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
return ownerWidgetUuid?.startsWith('widget-windtrends-chart') === true
|
|
365
|
+
|| ownerWidgetUuid?.startsWith('widget-data-chart') === true;
|
|
366
|
+
}
|
|
296
367
|
normalizeSeries(input) {
|
|
297
368
|
const seriesId = (input.seriesId || input.datasetUuid || '').trim();
|
|
298
369
|
if (!seriesId) {
|
|
@@ -310,8 +381,8 @@ class HistorySeriesService {
|
|
|
310
381
|
if (!path) {
|
|
311
382
|
throw new Error('path is required');
|
|
312
383
|
}
|
|
313
|
-
|
|
314
|
-
const isDataWidget =
|
|
384
|
+
const ownerWidgetSelector = typeof input.ownerWidgetSelector === 'string' ? input.ownerWidgetSelector.trim() : undefined;
|
|
385
|
+
const isDataWidget = this.isChartWidget(ownerWidgetSelector, ownerWidgetUuid);
|
|
315
386
|
const retentionMs = this.resolveRetentionMs(input);
|
|
316
387
|
let sampleTime;
|
|
317
388
|
if (isDataWidget) {
|
|
@@ -329,6 +400,7 @@ class HistorySeriesService {
|
|
|
329
400
|
seriesId,
|
|
330
401
|
datasetUuid,
|
|
331
402
|
ownerWidgetUuid,
|
|
403
|
+
ownerWidgetSelector,
|
|
332
404
|
path,
|
|
333
405
|
source: input.source ?? 'default',
|
|
334
406
|
context: input.context ?? 'vessels.self',
|
|
@@ -408,11 +480,17 @@ class HistorySeriesService {
|
|
|
408
480
|
if (seriesContext === sampleContext) {
|
|
409
481
|
return true;
|
|
410
482
|
}
|
|
411
|
-
if (seriesContext
|
|
412
|
-
return
|
|
483
|
+
if (this.isSelfContext(seriesContext) && this.isSelfContext(sampleContext)) {
|
|
484
|
+
return true;
|
|
413
485
|
}
|
|
414
486
|
return false;
|
|
415
487
|
}
|
|
488
|
+
isSelfContext(context) {
|
|
489
|
+
if (context === 'vessels.self') {
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
return !!this.selfContext && context === this.selfContext;
|
|
493
|
+
}
|
|
416
494
|
isSourceMatch(seriesSource, sampleSource) {
|
|
417
495
|
if (seriesSource === '*' || seriesSource === 'any') {
|
|
418
496
|
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,14 @@ const start = (server) => {
|
|
|
79
87
|
}
|
|
80
88
|
}
|
|
81
89
|
};
|
|
82
|
-
const historySeries = new history_series_service_1.HistorySeriesService(() => Date.now());
|
|
83
|
-
const storageService = new sqlite_history_storage_service_1.SqliteHistoryStorageService();
|
|
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 storageService = new sqlite_history_storage_service_1.SqliteHistoryStorageService(server.getDataDirPath());
|
|
84
92
|
let retentionSweepTimer = null;
|
|
85
93
|
let storageFlushTimer = null;
|
|
86
94
|
let sqliteInitializationPromise = null;
|
|
87
95
|
const SQLITE_INIT_WAIT_TIMEOUT_MS = 5000;
|
|
88
96
|
const MIN_NODE_SQLITE_VERSION = '22.5.0';
|
|
89
97
|
let streamUnsubscribes = [];
|
|
90
|
-
let historyApiRegistry = null;
|
|
91
98
|
let historyApiProviderRegistered = false;
|
|
92
99
|
let runtimeSqliteUnavailableMessage = null;
|
|
93
100
|
function logRuntimeDependencyVersions() {
|
|
@@ -95,16 +102,12 @@ const start = (server) => {
|
|
|
95
102
|
const sqliteAvailability = modeConfig && modeConfig.nodeSqliteAvailable ? 'available' : 'unavailable';
|
|
96
103
|
server.debug(`[KIP][RUNTIME] ${nodeIdentity} node:sqlite=${sqliteAvailability}`);
|
|
97
104
|
}
|
|
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
105
|
async function detectSqliteRuntime() {
|
|
107
|
-
const
|
|
106
|
+
const exportedStart = start;
|
|
107
|
+
const resolveSqliteModule = typeof exportedStart.getSqliteModule === 'function'
|
|
108
|
+
? exportedStart.getSqliteModule
|
|
109
|
+
: defaultGetSqliteModule;
|
|
110
|
+
const sqliteModule = await resolveSqliteModule();
|
|
108
111
|
if (!sqliteModule) {
|
|
109
112
|
runtimeSqliteUnavailableMessage = `node:sqlite requires Node ${MIN_NODE_SQLITE_VERSION}+`;
|
|
110
113
|
return false;
|
|
@@ -145,6 +148,97 @@ const start = (server) => {
|
|
|
145
148
|
nodeSqliteAvailable: nodeSqliteAvailable !== false
|
|
146
149
|
};
|
|
147
150
|
}
|
|
151
|
+
function slugify(value) {
|
|
152
|
+
return value
|
|
153
|
+
.toLowerCase()
|
|
154
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
155
|
+
.replace(/^-+|-+$/g, '');
|
|
156
|
+
}
|
|
157
|
+
function resolveBmsBatteryIdsFromSelfPath() {
|
|
158
|
+
const batteriesPath = server.getSelfPath('electrical.batteries');
|
|
159
|
+
const readCandidate = (node) => {
|
|
160
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
const root = node;
|
|
164
|
+
if (Object.prototype.hasOwnProperty.call(root, 'value')) {
|
|
165
|
+
const value = root.value;
|
|
166
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
167
|
+
return value;
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
return root;
|
|
172
|
+
};
|
|
173
|
+
const candidates = readCandidate(batteriesPath);
|
|
174
|
+
if (!candidates) {
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
return Object.keys(candidates)
|
|
178
|
+
.filter(id => /^[a-z0-9_-]+$/i.test(id))
|
|
179
|
+
.sort((left, right) => left.localeCompare(right));
|
|
180
|
+
}
|
|
181
|
+
function getExistingConcreteBmsSeries(templateSeries, existingSeries) {
|
|
182
|
+
return existingSeries
|
|
183
|
+
.filter(series => series.ownerWidgetUuid === templateSeries.ownerWidgetUuid)
|
|
184
|
+
.filter(series => series.expansionMode == null)
|
|
185
|
+
.filter(series => series.seriesId !== templateSeries.seriesId)
|
|
186
|
+
.map(series => ({ ...series }));
|
|
187
|
+
}
|
|
188
|
+
function mergeSeriesDefinitions(series) {
|
|
189
|
+
const mergedById = new Map();
|
|
190
|
+
series.forEach(item => {
|
|
191
|
+
mergedById.set(item.seriesId, item);
|
|
192
|
+
});
|
|
193
|
+
return Array.from(mergedById.values());
|
|
194
|
+
}
|
|
195
|
+
function expandTemplateSeriesDefinitions(payload, existingSeries = []) {
|
|
196
|
+
const bmsMetrics = ['capacity.stateOfCharge', 'current'];
|
|
197
|
+
const expandedById = new Map();
|
|
198
|
+
const discoveredBatteryIds = resolveBmsBatteryIdsFromSelfPath();
|
|
199
|
+
payload.forEach(series => {
|
|
200
|
+
const isBmsTemplate = series.expansionMode === 'bms-battery-tree' && series.ownerWidgetSelector === 'widget-bms';
|
|
201
|
+
if (!isBmsTemplate) {
|
|
202
|
+
expandedById.set(series.seriesId, series);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (discoveredBatteryIds.length === 0) {
|
|
206
|
+
getExistingConcreteBmsSeries(series, existingSeries).forEach(existing => {
|
|
207
|
+
expandedById.set(existing.seriesId, existing);
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const allowedBatteryIds = Array.isArray(series.allowedBatteryIds)
|
|
212
|
+
? series.allowedBatteryIds
|
|
213
|
+
.filter((id) => typeof id === 'string')
|
|
214
|
+
.map(id => id.trim())
|
|
215
|
+
.filter(id => id.length > 0)
|
|
216
|
+
: [];
|
|
217
|
+
const allowedSet = allowedBatteryIds.length > 0 ? new Set(allowedBatteryIds) : null;
|
|
218
|
+
const batteryIds = discoveredBatteryIds.filter(id => !allowedSet || allowedSet.has(id));
|
|
219
|
+
if (batteryIds.length === 0) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const source = series.source ?? 'default';
|
|
223
|
+
const sourceKey = slugify(source || 'default') || 'default';
|
|
224
|
+
batteryIds.forEach(batteryId => {
|
|
225
|
+
bmsMetrics.forEach(metric => {
|
|
226
|
+
const path = `self.electrical.batteries.${batteryId}.${metric}`;
|
|
227
|
+
const seriesId = `${series.ownerWidgetUuid}:bms:${batteryId}:${metric}:${sourceKey}`;
|
|
228
|
+
expandedById.set(seriesId, {
|
|
229
|
+
...series,
|
|
230
|
+
seriesId,
|
|
231
|
+
datasetUuid: `${series.ownerWidgetUuid}:bms:${batteryId}:${metric}:${sourceKey}`,
|
|
232
|
+
path,
|
|
233
|
+
retentionDurationMs: Number.isFinite(series.retentionDurationMs) ? series.retentionDurationMs : 24 * 60 * 60 * 1000,
|
|
234
|
+
expansionMode: null,
|
|
235
|
+
allowedBatteryIds: null
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
return Array.from(expandedById.values());
|
|
241
|
+
}
|
|
148
242
|
function getDisplaySelfPath(displayId, suffix) {
|
|
149
243
|
const tail = suffix ? `.${suffix}` : '';
|
|
150
244
|
const want = `displays.${displayId}${tail}`;
|
|
@@ -470,7 +564,17 @@ const start = (server) => {
|
|
|
470
564
|
server.debug('[KIP][HISTORY_PROVIDER] registration skipped reason=config-disabled');
|
|
471
565
|
return;
|
|
472
566
|
}
|
|
473
|
-
const
|
|
567
|
+
const serverWithHistoryApi = server;
|
|
568
|
+
const registerHistoryApiProvider = typeof serverWithHistoryApi.registerHistoryApiProvider === 'function'
|
|
569
|
+
? serverWithHistoryApi.registerHistoryApiProvider.bind(serverWithHistoryApi)
|
|
570
|
+
: (typeof serverWithHistoryApi.history?.registerHistoryApiProvider === 'function'
|
|
571
|
+
? serverWithHistoryApi.history.registerHistoryApiProvider.bind(serverWithHistoryApi.history)
|
|
572
|
+
: null);
|
|
573
|
+
// guard when running in SK variants that do not support History API registration
|
|
574
|
+
if (!registerHistoryApiProvider) {
|
|
575
|
+
server.debug('[KIP][HISTORY_PROVIDER] registration skipped reason=api-unavailable');
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
474
578
|
const apiProvider = {
|
|
475
579
|
getValues: async (query) => {
|
|
476
580
|
const resolved = await resolveHistoryValues(buildHistoryQueryFromValuesRequest(query));
|
|
@@ -485,26 +589,9 @@ const start = (server) => {
|
|
|
485
589
|
getPaths: (query) => resolveHistoryPaths(buildHistoryQueryFromRangeRequest(query)),
|
|
486
590
|
getContexts: (query) => resolveHistoryContexts(buildHistoryQueryFromRangeRequest(query))
|
|
487
591
|
};
|
|
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');
|
|
592
|
+
registerHistoryApiProvider(apiProvider);
|
|
593
|
+
historyApiProviderRegistered = true;
|
|
594
|
+
server.debug('[KIP][HISTORY_PROVIDER] registration success provider=kip');
|
|
508
595
|
}
|
|
509
596
|
function rebuildSeriesCaptureSubscriptions() {
|
|
510
597
|
stopSeriesCapture();
|
|
@@ -603,9 +690,6 @@ const start = (server) => {
|
|
|
603
690
|
if (!modeConfig.nodeSqliteAvailable) {
|
|
604
691
|
server.error(`[KIP][RUNTIME] node:sqlite unavailable. ${runtimeSqliteUnavailableMessage}`);
|
|
605
692
|
}
|
|
606
|
-
const serverWithApp = server;
|
|
607
|
-
const dataDirPath = serverWithApp.app?.getDataDirPath?.();
|
|
608
|
-
storageService.setDataDirPath(typeof dataDirPath === 'string' ? dataDirPath : null);
|
|
609
693
|
storageService.setRuntimeAvailability(modeConfig.nodeSqliteAvailable, runtimeSqliteUnavailableMessage ?? undefined);
|
|
610
694
|
logRuntimeDependencyVersions();
|
|
611
695
|
logOperationalMode('start-configured');
|
|
@@ -742,16 +826,16 @@ const start = (server) => {
|
|
|
742
826
|
.catch(() => undefined)
|
|
743
827
|
.then(() => storageService.close(storageLifecycleToken))
|
|
744
828
|
.catch(() => undefined);
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
historyApiRegistry = null;
|
|
829
|
+
const serverWithHistoryApi = server;
|
|
830
|
+
const unregisterHistoryApiProvider = typeof serverWithHistoryApi.unregisterHistoryApiProvider === 'function'
|
|
831
|
+
? serverWithHistoryApi.unregisterHistoryApiProvider.bind(serverWithHistoryApi)
|
|
832
|
+
: (typeof serverWithHistoryApi.history?.unregisterHistoryApiProvider === 'function'
|
|
833
|
+
? serverWithHistoryApi.history.unregisterHistoryApiProvider.bind(serverWithHistoryApi.history)
|
|
834
|
+
: null);
|
|
835
|
+
if (unregisterHistoryApiProvider) {
|
|
836
|
+
unregisterHistoryApiProvider();
|
|
754
837
|
}
|
|
838
|
+
historyApiProviderRegistered = false;
|
|
755
839
|
sqliteInitializationPromise = null;
|
|
756
840
|
const msg = 'Stopped.';
|
|
757
841
|
server.setPluginStatus(msg);
|
|
@@ -1057,14 +1141,28 @@ const start = (server) => {
|
|
|
1057
1141
|
if (!Array.isArray(payload)) {
|
|
1058
1142
|
return sendFail(res, 400, 'Body must be an array of series definitions');
|
|
1059
1143
|
}
|
|
1060
|
-
const simulated = new history_series_service_1.HistorySeriesService(() => Date.now());
|
|
1061
|
-
|
|
1144
|
+
const simulated = new history_series_service_1.HistorySeriesService(() => Date.now(), typeof server.selfId === 'string' && server.selfId.trim().length > 0 ? `vessels.${server.selfId.trim()}` : null);
|
|
1145
|
+
const currentSeries = mergeSeriesDefinitions([
|
|
1146
|
+
...(await storageService.getSeriesDefinitions()),
|
|
1147
|
+
...historySeries.listSeries()
|
|
1148
|
+
]);
|
|
1149
|
+
currentSeries.forEach(series => {
|
|
1062
1150
|
simulated.upsertSeries(series);
|
|
1063
1151
|
});
|
|
1064
1152
|
const scopedPayload = payload.map(series => ({
|
|
1065
1153
|
...series
|
|
1066
1154
|
}));
|
|
1067
|
-
const
|
|
1155
|
+
const isBatteryDiscoveryUnavailable = resolveBmsBatteryIdsFromSelfPath().length === 0;
|
|
1156
|
+
const preservedBmsSeries = isBatteryDiscoveryUnavailable
|
|
1157
|
+
? scopedPayload
|
|
1158
|
+
.filter(series => series.expansionMode === 'bms-battery-tree' && series.ownerWidgetSelector === 'widget-bms')
|
|
1159
|
+
.flatMap(series => currentSeries.filter(current => current.ownerWidgetUuid === series.ownerWidgetUuid && current.expansionMode == null && current.seriesId !== series.seriesId))
|
|
1160
|
+
: [];
|
|
1161
|
+
const expandedPayload = mergeSeriesDefinitions([
|
|
1162
|
+
...expandTemplateSeriesDefinitions(scopedPayload, currentSeries),
|
|
1163
|
+
...preservedBmsSeries
|
|
1164
|
+
]);
|
|
1165
|
+
const result = simulated.reconcileSeries(expandedPayload);
|
|
1068
1166
|
const nextSeries = simulated.listSeries();
|
|
1069
1167
|
await storageService.replaceSeriesDefinitions(nextSeries);
|
|
1070
1168
|
const seriesOutsideScope = historySeries.listSeries();
|
|
@@ -1093,4 +1191,6 @@ const start = (server) => {
|
|
|
1093
1191
|
};
|
|
1094
1192
|
return plugin;
|
|
1095
1193
|
};
|
|
1194
|
+
const startWithHooks = start;
|
|
1195
|
+
startWithHooks.getSqliteModule = defaultGetSqliteModule;
|
|
1096
1196
|
module.exports = start;
|