@mxtommy/kip 4.6.0-beta.2 → 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 (47) hide show
  1. package/CHANGELOG.md +12 -3
  2. package/package.json +17 -17
  3. package/plugin/history-series.service.js +125 -17
  4. package/plugin/index.js +168 -54
  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 +56 -12
  16. package/public/assets/help-docs/zones.md +1 -1
  17. package/public/assets/svg/icons.svg +17 -0
  18. package/public/{chunk-P7JKENHI.js → chunk-2GOHQZH5.js} +4 -4
  19. package/public/{chunk-NFJ4RQSE.js → chunk-4YDVZHMH.js} +4 -4
  20. package/public/{chunk-FZSLNGBK.js → chunk-6U74K6G4.js} +7 -7
  21. package/public/{chunk-TVNXBPFF.js → chunk-AQROQY2F.js} +1 -1
  22. package/public/{chunk-XBSU7OGT.js → chunk-AZC2WKQI.js} +1 -1
  23. package/public/{chunk-WH5CIUSB.js → chunk-BGGO4PGD.js} +1 -1
  24. package/public/{chunk-R36UY4Q4.js → chunk-BQPPRM7O.js} +1 -1
  25. package/public/{chunk-BEQKBGLG.js → chunk-BTVGQ4ZG.js} +2 -2
  26. package/public/{chunk-RCYOZLZB.js → chunk-CSIELI2Z.js} +2 -2
  27. package/public/{chunk-VXCYPAWR.js → chunk-FYDLTNP4.js} +1 -1
  28. package/public/{chunk-YI3MZWRZ.js → chunk-HSKVTFFQ.js} +1 -1
  29. package/public/{chunk-TBNKOU7M.js → chunk-IENESD5Q.js} +1 -1
  30. package/public/chunk-LS6AJ3JI.js +50 -0
  31. package/public/{chunk-SJFJEOSG.js → chunk-M37BLWHF.js} +5 -5
  32. package/public/chunk-MXUEYEZU.js +5 -0
  33. package/public/{chunk-WQSJFJLW.js → chunk-POMIQBAL.js} +2 -2
  34. package/public/{chunk-P4CRTB7N.js → chunk-PTLDR7X7.js} +1 -1
  35. package/public/{chunk-OPTBDYBL.js → chunk-PUPM3HUQ.js} +1 -1
  36. package/public/chunk-PZ6I6W3H.js +16 -0
  37. package/public/{chunk-Q2ANAJAD.js → chunk-SUWMN3AE.js} +1 -1
  38. package/public/{chunk-FZFDGAQO.js → chunk-WJFXI5PQ.js} +1 -1
  39. package/public/{chunk-BJEHRCYP.js → chunk-X44BRNVL.js} +1 -1
  40. package/public/{chunk-KWTS7JF7.js → chunk-Y6JCNR3H.js} +1 -1
  41. package/public/{chunk-VPF5756E.js → chunk-YY4ZUJFI.js} +5 -5
  42. package/public/{chunk-J6EEFXKZ.js → chunk-Z4K5KE3I.js} +8 -8
  43. package/public/index.html +1 -1
  44. package/public/{main-TZOV3JCT.js → main-775NFBN3.js} +1 -1
  45. package/public/chunk-67V4XHCY.js +0 -5
  46. package/public/chunk-BTFZS2TW.js +0 -16
  47. package/public/chunk-RFNZ4AQG.js +0 -50
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');
@@ -633,13 +731,13 @@ const start = (server) => {
633
731
  }
634
732
  startStorageFlushTimer(storageConfig.flushIntervalMs);
635
733
  logOperationalMode('sqlite-ready');
636
- server.setPluginStatus(`KIP plugin started with node:sqlite history storage. Loaded ${storedSeries.length} persisted series. historySeriesServiceEnabled=${modeConfig.historySeriesServiceEnabled} historyApiProviderEnabled=${modeConfig.registerAsHistoryApiProvider} historyApiProviderRegistered=${historyApiProviderRegistered}`);
734
+ server.setPluginStatus(`Providing: Remote Control${historyApiProviderRegistered ? ', History service' : ', No History service'}${storedSeries.length > 0 ? `, ${storedSeries.length} Time-Series` : ', No Time-Series'}.`);
637
735
  })
638
736
  .catch((loadError) => {
639
737
  server.error(`[SERIES STORAGE] failed to load persisted series: ${String(loadError.message || loadError)}`);
640
738
  startStorageFlushTimer(storageConfig.flushIntervalMs);
641
739
  logOperationalMode('sqlite-ready-series-load-failed');
642
- server.setPluginStatus(`KIP plugin started with node:sqlite history storage. historySeriesServiceEnabled=${modeConfig.historySeriesServiceEnabled} historyApiProviderEnabled=${modeConfig.registerAsHistoryApiProvider} historyApiProviderRegistered=${historyApiProviderRegistered}`);
740
+ server.setPluginStatus(`Providing: Remote Control${historyApiProviderRegistered ? ', History service' : ', No History service'}, No Time-Series.`);
643
741
  });
644
742
  }
645
743
  else {
@@ -647,7 +745,7 @@ const start = (server) => {
647
745
  stopSeriesCapture();
648
746
  startStorageFlushTimer(storageConfig.flushIntervalMs);
649
747
  logOperationalMode('sqlite-ready-series-disabled');
650
- server.setPluginStatus(`KIP plugin started with history-series service disabled. historyApiProviderEnabled=${modeConfig.registerAsHistoryApiProvider} historyApiProviderRegistered=${historyApiProviderRegistered}`);
748
+ server.setPluginStatus(`Providing: Remote Control${historyApiProviderRegistered ? ', History service' : ', No History service'}, No Time-Series.`);
651
749
  }
652
750
  return;
653
751
  }
@@ -659,7 +757,7 @@ const start = (server) => {
659
757
  if (initError) {
660
758
  server.setPluginError(`node:sqlite unavailable. ${initError}`);
661
759
  logOperationalMode('sqlite-unavailable');
662
- server.setPluginStatus(`KIP plugin started with node:sqlite unavailable. historySeriesServiceEnabled=${modeConfig?.historySeriesServiceEnabled} historyApiProviderEnabled=${modeConfig?.registerAsHistoryApiProvider} historyApiProviderRegistered=${historyApiProviderRegistered}`);
760
+ server.setPluginStatus(`Providing: Remote Control${historyApiProviderRegistered ? ', History service' : ', No History service'}, No Time-Series.`);
663
761
  }
664
762
  });
665
763
  if (retentionSweepTimer) {
@@ -695,7 +793,7 @@ const start = (server) => {
695
793
  }
696
794
  else {
697
795
  if (modeConfig && !modeConfig.nodeSqliteAvailable && (modeConfig.historySeriesServiceEnabled || modeConfig.registerAsHistoryApiProvider)) {
698
- server.setPluginError(getSqliteUnavailableMessage());
796
+ server.setPluginStatus(getSqliteUnavailableMessage());
699
797
  }
700
798
  server.debug('[KIP][STORAGE] sqlite init skipped reason=config-disabled-or-runtime');
701
799
  sqliteInitializationPromise = null;
@@ -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
+ }
@@ -131,7 +131,7 @@
131
131
  "nullable": true,
132
132
  "additionalProperties": true
133
133
  },
134
- "SeriesDefinition": {
134
+ "SeriesDefinitionBase": {
135
135
  "type": "object",
136
136
  "properties": {
137
137
  "seriesId": {
@@ -177,13 +177,101 @@
177
177
  "enabled": {
178
178
  "type": "boolean",
179
179
  "default": true
180
+ },
181
+ "methods": {
182
+ "type": "array",
183
+ "items": {
184
+ "type": "string",
185
+ "enum": [
186
+ "min",
187
+ "max",
188
+ "avg",
189
+ "sma",
190
+ "ema"
191
+ ]
192
+ }
193
+ },
194
+ "reconcileTs": {
195
+ "type": "integer",
196
+ "nullable": true
180
197
  }
181
198
  },
182
199
  "required": [
183
200
  "seriesId",
184
201
  "datasetUuid",
185
202
  "ownerWidgetUuid",
186
- "path"
203
+ "ownerWidgetSelector",
204
+ "path",
205
+ "enabled"
206
+ ]
207
+ },
208
+ "ConcreteSeriesDefinition": {
209
+ "allOf": [
210
+ {
211
+ "$ref": "#/components/schemas/SeriesDefinitionBase"
212
+ },
213
+ {
214
+ "type": "object",
215
+ "properties": {
216
+ "expansionMode": {
217
+ "nullable": true,
218
+ "description": "Null for concrete series definitions."
219
+ },
220
+ "allowedBatteryIds": {
221
+ "type": "array",
222
+ "nullable": true,
223
+ "items": {
224
+ "type": "string"
225
+ },
226
+ "description": "Concrete series do not use battery filters and should leave this null."
227
+ }
228
+ }
229
+ }
230
+ ]
231
+ },
232
+ "TemplateSeriesDefinition": {
233
+ "allOf": [
234
+ {
235
+ "$ref": "#/components/schemas/SeriesDefinitionBase"
236
+ },
237
+ {
238
+ "type": "object",
239
+ "properties": {
240
+ "ownerWidgetSelector": {
241
+ "type": "string",
242
+ "enum": [
243
+ "widget-bms"
244
+ ]
245
+ },
246
+ "expansionMode": {
247
+ "type": "string",
248
+ "enum": [
249
+ "bms-battery-tree"
250
+ ]
251
+ },
252
+ "allowedBatteryIds": {
253
+ "type": "array",
254
+ "nullable": true,
255
+ "items": {
256
+ "type": "string"
257
+ }
258
+ }
259
+ },
260
+ "required": [
261
+ "ownerWidgetSelector",
262
+ "expansionMode"
263
+ ]
264
+ }
265
+ ]
266
+ },
267
+ "SeriesDefinition": {
268
+ "oneOf": [
269
+ {
270
+ "$ref": "#/components/schemas/ConcreteSeriesDefinition"
271
+ },
272
+ {
273
+ "$ref": "#/components/schemas/TemplateSeriesDefinition"
274
+ }
187
275
  ]
188
276
  },
189
277
  "SeriesReconcileResult": {
@@ -38,7 +38,7 @@ const fs_1 = require("fs");
38
38
  const path_1 = require("path");
39
39
  const DEFAULT_STORAGE_CONFIG = {
40
40
  engine: 'node:sqlite',
41
- databaseFile: 'plugin-config-data/kip/historicalData/kip-history.sqlite',
41
+ databaseFile: '',
42
42
  flushIntervalMs: 30_000
43
43
  };
44
44
  /**
@@ -49,8 +49,7 @@ class SqliteHistoryStorageService {
49
49
  static FOUR_HOURS_INTERVAL = 4 * 60 * 60 * 1000;
50
50
  static STALE_SERIES_AGE_MS = 180 * 24 * 60 * 60 * 1000;
51
51
  static PRUNE_BATCH_SIZE = 10_000;
52
- config = { ...DEFAULT_STORAGE_CONFIG };
53
- dataDirPath = null;
52
+ config;
54
53
  logger = {
55
54
  debug: () => undefined,
56
55
  error: () => undefined
@@ -66,6 +65,16 @@ class SqliteHistoryStorageService {
66
65
  vacuumJob = null;
67
66
  pruneJob = null;
68
67
  staleSeriesCleanupJob = null;
68
+ constructor(dataDirPath) {
69
+ const normalizedDataDirPath = String(dataDirPath);
70
+ if (!normalizedDataDirPath) {
71
+ throw new Error('SqliteHistoryStorageService requires a valid dataDirPath from server.getDataDirPath()');
72
+ }
73
+ this.config = {
74
+ ...DEFAULT_STORAGE_CONFIG,
75
+ databaseFile: (0, path_1.join)(normalizedDataDirPath, 'historicalData', 'kip-history.sqlite')
76
+ };
77
+ }
69
78
  /**
70
79
  * Sets logger callbacks used by the storage service.
71
80
  *
@@ -89,27 +98,12 @@ class SqliteHistoryStorageService {
89
98
  */
90
99
  configure() {
91
100
  this.initialized = false;
92
- const databaseFile = this.dataDirPath
93
- ? (0, path_1.join)(this.dataDirPath, 'historicalData', 'kip-history.sqlite')
94
- : DEFAULT_STORAGE_CONFIG.databaseFile;
95
101
  this.config = {
96
102
  ...DEFAULT_STORAGE_CONFIG,
97
- databaseFile
103
+ databaseFile: this.config.databaseFile
98
104
  };
99
105
  return this.config;
100
106
  }
101
- /**
102
- * Sets the base directory for persisted history data.
103
- *
104
- * @param {string | null} baseDir Absolute directory path for plugin data.
105
- * @returns {void}
106
- *
107
- * @example
108
- * storage.setDataDirPath('/var/lib/signalk');
109
- */
110
- setDataDirPath(baseDir) {
111
- this.dataDirPath = typeof baseDir === 'string' && baseDir.trim() ? baseDir.trim() : null;
112
- }
113
107
  /**
114
108
  * Updates runtime availability of node:sqlite, clearing stored errors when enabled.
115
109
  *
@@ -316,7 +310,7 @@ class SqliteHistoryStorageService {
316
310
  seriesId: row.series_id,
317
311
  datasetUuid: row.dataset_uuid,
318
312
  ownerWidgetUuid: row.owner_widget_uuid,
319
- ownerWidgetSelector: row.owner_widget_selector ?? undefined,
313
+ ownerWidgetSelector: row.owner_widget_selector ?? null,
320
314
  path: row.path,
321
315
  source: row.source ?? undefined,
322
316
  context: row.context ?? undefined,
@@ -276,6 +276,42 @@ OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
276
276
  TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
277
277
  THIS SOFTWARE.
278
278
 
279
+ --------------------------------------------------------------------------------
280
+ Package: d3-path
281
+ License: "ISC"
282
+
283
+ Copyright 2015-2022 Mike Bostock
284
+
285
+ Permission to use, copy, modify, and/or distribute this software for any purpose
286
+ with or without fee is hereby granted, provided that the above copyright notice
287
+ and this permission notice appear in all copies.
288
+
289
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
290
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
291
+ FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
292
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
293
+ OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
294
+ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
295
+ THIS SOFTWARE.
296
+
297
+ --------------------------------------------------------------------------------
298
+ Package: d3-shape
299
+ License: "ISC"
300
+
301
+ Copyright 2010-2022 Mike Bostock
302
+
303
+ Permission to use, copy, modify, and/or distribute this software for any purpose
304
+ with or without fee is hereby granted, provided that the above copyright notice
305
+ and this permission notice appear in all copies.
306
+
307
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
308
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
309
+ FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
310
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
311
+ OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
312
+ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
313
+ THIS SOFTWARE.
314
+
279
315
  --------------------------------------------------------------------------------
280
316
  Package: d3-zoom
281
317
  License: "ISC"
@@ -69,9 +69,9 @@ KIP widgets turn Signal K data into readable visuals and controls. Available wid
69
69
  - **Position** – Displays latitude and longitude for location tracking and navigation.
70
70
  - **Static Label** – Add customizable labels to organize and clarify your dashboard layout.
71
71
  - **Zones State Panel**: Monitor the health/state of path data. Each panel control displays path data severity and status messages (driven by Signal K metadata zones).
72
- - **Switch Panel** – Group of toggle switches, indicator lights, and press buttons for digital switching and operations. See [Digital Switching and PUT Path Setup](putcontrols.md).
73
- - **Slider** – Range slider for adjusting values (e.g. lighting intensity). See [Digital Switching and PUT Path Setup](putcontrols.md).
74
- - **Multi State Switch** - Lists all available device/path operating modes/states (e.g., On, Off, Charge Only, Invert Only), highlights the current state, and lets you select a new state to send to the device and see the result. See [Digital Switching and PUT Path Setup](putcontrols.md).
72
+ - **Switch Panel** – Group of toggle switches, indicator lights, and press buttons for digital switching and operations. See [Digital Switching and PUT Path Setup](#/help/putcontrols.md).
73
+ - **Slider** – Range slider for adjusting values (e.g. lighting intensity). See [Digital Switching and PUT Path Setup](#/help/putcontrols.md).
74
+ - **Multi State Switch** - Lists all available device/path operating modes/states (e.g., On, Off, Charge Only, Invert Only), highlights the current state, and lets you select a new state to send to the device and see the result. See [Digital Switching and PUT Path Setup](#/help/putcontrols.md).
75
75
  - **Compact Linear** – Simple horizontal linear gauge with a large value label and modern look.
76
76
  - **Linear** – Horizontal or vertical linear gauge with zone highlighting.
77
77
  - **Radial** – Radial gauge with configurable dials and zone highlighting.
@@ -24,7 +24,7 @@ The Data Inspector is a good way to validate raw data and available paths withou
24
24
 
25
25
  2. **PUT Support**:
26
26
  - You can see if a path supports PUT operations, indicated by a green checkmark in the **PUT Support** column.
27
- - For more details on PUT support and how to use it, refer to the [Updating Signal K Data](putcontrols.md) help documentation.
27
+ - For more details on PUT support and how to use it, refer to the [Updating Signal K Data](#/help/putcontrols.md) help documentation.
28
28
 
29
29
  3. **Multiple Data Sources**:
30
30
  - The Data Inspector displays how many sources are providing data for each path.
@@ -42,8 +42,8 @@ The Data Inspector is a good way to validate raw data and available paths withou
42
42
 
43
43
  - **Verify PUT Support**:
44
44
  - Check if a path supports PUT operations. It is required to configure widgets like Switch Panel, Slider, or Multi State Switch.
45
- - For more details on PUT support and how to use it, refer to the [Updating Signal K Data](putcontrols.md) help documentation.
46
- - If you are learning Node-RED flows and want your flow to work with KIP digital switching widgets, continue with [Node-RED Control Flows for KIP Widgets (Beginner Guide)](nodered-control-flows.md).
45
+ - For more details on PUT support and how to use it, refer to the [Updating Signal K Data](#/help/putcontrols.md) help documentation.
46
+ - If you are learning Node-RED flows and want your flow to work with KIP digital switching widgets, continue with [Node-RED Control Flows for KIP Widgets (Beginner Guide)](#/help/nodered-control-flows.md).
47
47
 
48
48
  - **Troubleshoot Data Issues**:
49
49
  - The Data Inspector is a good troubleshooting tool, but it should be used with the Signal K Data Browser when trying to understand raw data and behavior. The combination of these tools provides a more complete picture of the data, its processing, and its behavior.