@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.
- package/CHANGELOG.md +12 -3
- package/package.json +17 -17
- package/plugin/history-series.service.js +125 -17
- package/plugin/index.js +168 -54
- 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 +56 -12
- package/public/assets/help-docs/zones.md +1 -1
- package/public/assets/svg/icons.svg +17 -0
- package/public/{chunk-P7JKENHI.js → chunk-2GOHQZH5.js} +4 -4
- package/public/{chunk-NFJ4RQSE.js → chunk-4YDVZHMH.js} +4 -4
- package/public/{chunk-FZSLNGBK.js → chunk-6U74K6G4.js} +7 -7
- package/public/{chunk-TVNXBPFF.js → chunk-AQROQY2F.js} +1 -1
- package/public/{chunk-XBSU7OGT.js → chunk-AZC2WKQI.js} +1 -1
- package/public/{chunk-WH5CIUSB.js → chunk-BGGO4PGD.js} +1 -1
- package/public/{chunk-R36UY4Q4.js → chunk-BQPPRM7O.js} +1 -1
- package/public/{chunk-BEQKBGLG.js → chunk-BTVGQ4ZG.js} +2 -2
- package/public/{chunk-RCYOZLZB.js → chunk-CSIELI2Z.js} +2 -2
- package/public/{chunk-VXCYPAWR.js → chunk-FYDLTNP4.js} +1 -1
- package/public/{chunk-YI3MZWRZ.js → chunk-HSKVTFFQ.js} +1 -1
- package/public/{chunk-TBNKOU7M.js → chunk-IENESD5Q.js} +1 -1
- package/public/chunk-LS6AJ3JI.js +50 -0
- package/public/{chunk-SJFJEOSG.js → chunk-M37BLWHF.js} +5 -5
- package/public/chunk-MXUEYEZU.js +5 -0
- package/public/{chunk-WQSJFJLW.js → chunk-POMIQBAL.js} +2 -2
- package/public/{chunk-P4CRTB7N.js → chunk-PTLDR7X7.js} +1 -1
- package/public/{chunk-OPTBDYBL.js → chunk-PUPM3HUQ.js} +1 -1
- package/public/chunk-PZ6I6W3H.js +16 -0
- package/public/{chunk-Q2ANAJAD.js → chunk-SUWMN3AE.js} +1 -1
- package/public/{chunk-FZFDGAQO.js → chunk-WJFXI5PQ.js} +1 -1
- package/public/{chunk-BJEHRCYP.js → chunk-X44BRNVL.js} +1 -1
- package/public/{chunk-KWTS7JF7.js → chunk-Y6JCNR3H.js} +1 -1
- package/public/{chunk-VPF5756E.js → chunk-YY4ZUJFI.js} +5 -5
- package/public/{chunk-J6EEFXKZ.js → chunk-Z4K5KE3I.js} +8 -8
- package/public/index.html +1 -1
- package/public/{main-TZOV3JCT.js → main-775NFBN3.js} +1 -1
- package/public/chunk-67V4XHCY.js +0 -5
- package/public/chunk-BTFZS2TW.js +0 -16
- 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
|
|
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');
|
|
@@ -633,13 +731,13 @@ const start = (server) => {
|
|
|
633
731
|
}
|
|
634
732
|
startStorageFlushTimer(storageConfig.flushIntervalMs);
|
|
635
733
|
logOperationalMode('sqlite-ready');
|
|
636
|
-
server.setPluginStatus(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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.
|
|
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
|
-
|
|
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
|
+
}
|
package/plugin/openApi.json
CHANGED
|
@@ -131,7 +131,7 @@
|
|
|
131
131
|
"nullable": true,
|
|
132
132
|
"additionalProperties": true
|
|
133
133
|
},
|
|
134
|
-
"
|
|
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
|
-
"
|
|
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: '
|
|
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
|
|
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 ??
|
|
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.
|