@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.
Files changed (38) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/package.json +5 -5
  3. package/plugin/history-series.service.js +125 -17
  4. package/plugin/index.js +163 -49
  5. package/plugin/kip-series-contract.js +14 -0
  6. package/plugin/openApi.json +90 -2
  7. package/plugin/sqlite-history-storage.service.js +14 -20
  8. package/public/3rdpartylicenses.txt +36 -0
  9. package/public/assets/help-docs/dashboards.md +3 -3
  10. package/public/assets/help-docs/datainspector.md +3 -3
  11. package/public/assets/help-docs/history-api.md +31 -21
  12. package/public/assets/help-docs/menu.json +1 -1
  13. package/public/assets/help-docs/nodered-control-flows.md +4 -4
  14. package/public/assets/help-docs/putcontrols.md +6 -6
  15. package/public/assets/help-docs/widget-historical-series.md +55 -11
  16. package/public/assets/help-docs/zones.md +1 -1
  17. package/public/assets/svg/icons.svg +17 -0
  18. package/public/{chunk-AC6VD2FN.js → chunk-2GOHQZH5.js} +4 -4
  19. package/public/{chunk-3JA4CQ7T.js → chunk-4YDVZHMH.js} +4 -4
  20. package/public/{chunk-MDNGWQNG.js → chunk-6U74K6G4.js} +7 -7
  21. package/public/{chunk-356CW47X.js → chunk-AQROQY2F.js} +1 -1
  22. package/public/{chunk-ZY3U4H4Z.js → chunk-AZC2WKQI.js} +1 -1
  23. package/public/{chunk-CYTLQDGF.js → chunk-BQPPRM7O.js} +1 -1
  24. package/public/{chunk-BMHMHQFO.js → chunk-BTVGQ4ZG.js} +2 -2
  25. package/public/{chunk-QU3JR4YV.js → chunk-FYDLTNP4.js} +1 -1
  26. package/public/{chunk-B4NYOD6L.js → chunk-IENESD5Q.js} +1 -1
  27. package/public/chunk-LS6AJ3JI.js +50 -0
  28. package/public/{chunk-PPF5S5CV.js → chunk-M37BLWHF.js} +5 -5
  29. package/public/chunk-MXUEYEZU.js +5 -0
  30. package/public/{chunk-NJISHUGY.js → chunk-PTLDR7X7.js} +1 -1
  31. package/public/{chunk-UYHRT3PR.js → chunk-X44BRNVL.js} +1 -1
  32. package/public/{chunk-ZXO4VMEH.js → chunk-Y6JCNR3H.js} +1 -1
  33. package/public/{chunk-MGLD6QDJ.js → chunk-YY4ZUJFI.js} +5 -5
  34. package/public/{chunk-5SAXWR6Z.js → chunk-Z4K5KE3I.js} +8 -8
  35. package/public/index.html +1 -1
  36. package/public/{main-I33LH3HC.js → main-775NFBN3.js} +1 -1
  37. package/public/chunk-6A4NRSCL.js +0 -5
  38. 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 all binary and external dependencies.
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 version supports the required Node.js version. See the [Signal K installation documentation](https://demo.signalk.org/documentation/Installation.html).
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.6.0",
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.4",
79
+ "@angular-devkit/build-angular": "^21.2.1",
80
80
  "@angular-devkit/schematics-cli": "^20.1.6",
81
- "@angular/build": "^21.1.4",
82
- "@angular/cli": "^21.1.4",
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.2.0",
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 = Date.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 (JSON.stringify(existing) !== JSON.stringify(desired)) {
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
- const seriesEntry = Array.from(this.seriesById.entries())
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(seriesEntry[0], value, timestamp);
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.seriesById.forEach((series, seriesKey) => {
217
- if (series.path !== leaf.path || series.enabled === false) {
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
- // Determine if this is a chart type widget
314
- const isDataWidget = ownerWidgetUuid?.startsWith('widget-windtrends-chart') || ownerWidgetUuid?.startsWith('widget-data-chart');
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
- return {
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 === 'vessels.self') {
412
- return sampleContext === 'vessels.self' || sampleContext.startsWith('vessels.');
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 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 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 sqliteModule = await getSqliteModule();
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 host = server;
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
- const registry = host.history && typeof host.history.registerHistoryApiProvider === 'function'
489
- ? host.history
490
- : (typeof host.registerHistoryApiProvider === 'function'
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(series => series.enabled !== false).forEach(series => {
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
- if (historyApiRegistry) {
746
- try {
747
- historyApiRegistry.unregisterHistoryApiProvider();
748
- server.debug('[KIP][HISTORY_PROVIDER] unregister success provider=kip');
749
- }
750
- catch (error) {
751
- server.error(`[HISTORY PROVIDER] unregister failed: ${String(error.message || error)}`);
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
- historySeries.listSeries().forEach(series => {
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 result = simulated.reconcileSeries(scopedPayload);
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
+ }