@mxtommy/kip 4.7.0-beta.2 → 4.7.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/plugin/history-series.service.js +91 -13
- package/plugin/index.js +110 -8
- package/plugin/kip-plugin/src/history-series.service.js +537 -0
- package/plugin/kip-plugin/src/index.js +1194 -0
- package/plugin/kip-plugin/src/openApi.json +787 -0
- package/plugin/kip-plugin/src/sqlite-history-storage.service.js +1122 -0
- package/plugin/openApi.json +14 -0
- package/plugin/sqlite-history-storage.service.js +13 -19
- package/plugin/src/app/core/contracts/kip-series-contract.js +14 -0
- package/plugin/src/app/core/contracts/kip-series-contract.spec.js +68 -0
- package/public/assets/svg/icons.svg +1 -1
- package/public/{chunk-3MSOVKX6.js → chunk-4YDVZHMH.js} +1 -1
- package/public/{chunk-7H5VXIPS.js → chunk-7ZZK5KBC.js} +1 -1
- package/public/{chunk-EZZ4IJBX.js → chunk-AB34PRGT.js} +1 -1
- package/public/{chunk-NK7SNP45.js → chunk-AQROQY2F.js} +1 -1
- package/public/{chunk-CMHH7BXX.js → chunk-BQPPRM7O.js} +1 -1
- package/public/{chunk-64MHGMIL.js → chunk-CRJ2S7XQ.js} +2 -2
- package/public/{chunk-7EAIOLCB.js → chunk-IENESD5Q.js} +1 -1
- package/public/{chunk-CHMMSVYD.js → chunk-M37BLWHF.js} +5 -5
- package/public/{chunk-Y4DXERRE.js → chunk-OXKOGBYN.js} +12 -12
- package/public/{chunk-FVGLVFWP.js → chunk-QV7OHVD5.js} +1 -1
- package/public/{chunk-NFUW7ILE.js → chunk-SDPSUOCJ.js} +1 -1
- package/public/{chunk-B3VMWHNV.js → chunk-YY4ZUJFI.js} +1 -1
- package/public/index.html +1 -1
- package/public/{main-VB3XIM4H.js → main-5HQ6OYM7.js} +1 -1
package/package.json
CHANGED
|
@@ -6,11 +6,14 @@ exports.HistorySeriesService = void 0;
|
|
|
6
6
|
*/
|
|
7
7
|
class HistorySeriesService {
|
|
8
8
|
nowProvider;
|
|
9
|
+
selfContext;
|
|
9
10
|
seriesById = new Map();
|
|
11
|
+
enabledSeriesKeysByPath = new Map();
|
|
10
12
|
lastAcceptedTimestampBySeriesKey = new Map();
|
|
11
13
|
sampleSink = null;
|
|
12
|
-
constructor(nowProvider = () => Date.now()) {
|
|
14
|
+
constructor(nowProvider = () => Date.now(), selfContext = null) {
|
|
13
15
|
this.nowProvider = nowProvider;
|
|
16
|
+
this.selfContext = selfContext;
|
|
14
17
|
}
|
|
15
18
|
/**
|
|
16
19
|
* Returns all configured series sorted by `seriesId`.
|
|
@@ -56,6 +59,7 @@ class HistorySeriesService {
|
|
|
56
59
|
const normalized = this.normalizeSeries(input);
|
|
57
60
|
const key = normalized.seriesId;
|
|
58
61
|
this.seriesById.set(key, normalized);
|
|
62
|
+
this.rebuildEnabledPathIndex();
|
|
59
63
|
return normalized;
|
|
60
64
|
}
|
|
61
65
|
/**
|
|
@@ -90,6 +94,7 @@ class HistorySeriesService {
|
|
|
90
94
|
this.seriesById.delete(key);
|
|
91
95
|
this.lastAcceptedTimestampBySeriesKey.delete(key);
|
|
92
96
|
});
|
|
97
|
+
this.rebuildEnabledPathIndex();
|
|
93
98
|
return true;
|
|
94
99
|
}
|
|
95
100
|
/**
|
|
@@ -103,7 +108,7 @@ class HistorySeriesService {
|
|
|
103
108
|
* console.log(result.created, result.deleted);
|
|
104
109
|
*/
|
|
105
110
|
reconcileSeries(desiredSeries) {
|
|
106
|
-
const now =
|
|
111
|
+
const now = this.nowProvider();
|
|
107
112
|
const desiredById = new Map();
|
|
108
113
|
desiredSeries.forEach(entry => {
|
|
109
114
|
const normalized = this.normalizeSeries(entry);
|
|
@@ -122,7 +127,7 @@ class HistorySeriesService {
|
|
|
122
127
|
created += 1;
|
|
123
128
|
return;
|
|
124
129
|
}
|
|
125
|
-
if (
|
|
130
|
+
if (!this.areSeriesEquivalent(existing, desired)) {
|
|
126
131
|
this.seriesById.set(seriesKey, desiredWithReconcile);
|
|
127
132
|
updated += 1;
|
|
128
133
|
}
|
|
@@ -138,6 +143,7 @@ class HistorySeriesService {
|
|
|
138
143
|
deleted += 1;
|
|
139
144
|
}
|
|
140
145
|
});
|
|
146
|
+
this.rebuildEnabledPathIndex();
|
|
141
147
|
return {
|
|
142
148
|
created,
|
|
143
149
|
updated,
|
|
@@ -157,12 +163,10 @@ class HistorySeriesService {
|
|
|
157
163
|
* service.recordSample('abc', 12.4, Date.now());
|
|
158
164
|
*/
|
|
159
165
|
recordSample(seriesId, value, timestamp) {
|
|
160
|
-
|
|
161
|
-
.find(([, series]) => series.seriesId === seriesId);
|
|
162
|
-
if (!seriesEntry) {
|
|
166
|
+
if (!this.seriesById.has(seriesId)) {
|
|
163
167
|
return false;
|
|
164
168
|
}
|
|
165
|
-
return this.recordSampleByKey(
|
|
169
|
+
return this.recordSampleByKey(seriesId, value, timestamp);
|
|
166
170
|
}
|
|
167
171
|
recordSampleByKey(seriesKey, value, timestamp) {
|
|
168
172
|
const series = this.seriesById.get(seriesKey);
|
|
@@ -213,8 +217,13 @@ class HistorySeriesService {
|
|
|
213
217
|
}
|
|
214
218
|
let recorded = 0;
|
|
215
219
|
leafSamples.forEach(leaf => {
|
|
216
|
-
this.
|
|
217
|
-
|
|
220
|
+
const seriesKeys = this.enabledSeriesKeysByPath.get(leaf.path);
|
|
221
|
+
if (!seriesKeys || seriesKeys.length === 0) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
seriesKeys.forEach(seriesKey => {
|
|
225
|
+
const series = this.seriesById.get(seriesKey);
|
|
226
|
+
if (!series) {
|
|
218
227
|
return;
|
|
219
228
|
}
|
|
220
229
|
const seriesContext = series.context ?? 'vessels.self';
|
|
@@ -293,6 +302,68 @@ class HistorySeriesService {
|
|
|
293
302
|
});
|
|
294
303
|
return Array.from(contexts).sort();
|
|
295
304
|
}
|
|
305
|
+
rebuildEnabledPathIndex() {
|
|
306
|
+
this.enabledSeriesKeysByPath.clear();
|
|
307
|
+
this.seriesById.forEach((series, seriesKey) => {
|
|
308
|
+
if (series.enabled === false) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const keys = this.enabledSeriesKeysByPath.get(series.path) ?? [];
|
|
312
|
+
keys.push(seriesKey);
|
|
313
|
+
this.enabledSeriesKeysByPath.set(series.path, keys);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
areSeriesEquivalent(left, right) {
|
|
317
|
+
const leftComparable = this.toComparableSeries(left);
|
|
318
|
+
const rightComparable = this.toComparableSeries(right);
|
|
319
|
+
return leftComparable.seriesId === rightComparable.seriesId
|
|
320
|
+
&& leftComparable.datasetUuid === rightComparable.datasetUuid
|
|
321
|
+
&& leftComparable.ownerWidgetUuid === rightComparable.ownerWidgetUuid
|
|
322
|
+
&& leftComparable.ownerWidgetSelector === rightComparable.ownerWidgetSelector
|
|
323
|
+
&& leftComparable.path === rightComparable.path
|
|
324
|
+
&& leftComparable.expansionMode === rightComparable.expansionMode
|
|
325
|
+
&& this.areStringArraysEquivalent(leftComparable.allowedBatteryIds, rightComparable.allowedBatteryIds)
|
|
326
|
+
&& leftComparable.source === rightComparable.source
|
|
327
|
+
&& leftComparable.context === rightComparable.context
|
|
328
|
+
&& leftComparable.timeScale === rightComparable.timeScale
|
|
329
|
+
&& leftComparable.period === rightComparable.period
|
|
330
|
+
&& leftComparable.retentionDurationMs === rightComparable.retentionDurationMs
|
|
331
|
+
&& leftComparable.sampleTime === rightComparable.sampleTime
|
|
332
|
+
&& leftComparable.enabled === rightComparable.enabled
|
|
333
|
+
&& this.areStringArraysEquivalent(leftComparable.methods, rightComparable.methods);
|
|
334
|
+
}
|
|
335
|
+
toComparableSeries(series) {
|
|
336
|
+
const { reconcileTs, ...comparable } = series;
|
|
337
|
+
void reconcileTs;
|
|
338
|
+
return {
|
|
339
|
+
...comparable,
|
|
340
|
+
allowedBatteryIds: this.normalizeComparableStringArray(comparable.allowedBatteryIds),
|
|
341
|
+
methods: this.normalizeComparableStringArray(comparable.methods)
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
areStringArraysEquivalent(left, right) {
|
|
345
|
+
const normalizedLeft = this.normalizeComparableStringArray(left) ?? [];
|
|
346
|
+
const normalizedRight = this.normalizeComparableStringArray(right) ?? [];
|
|
347
|
+
if (normalizedLeft.length !== normalizedRight.length) {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
return normalizedLeft.every((value, index) => value === normalizedRight[index]);
|
|
351
|
+
}
|
|
352
|
+
normalizeComparableStringArray(values) {
|
|
353
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
354
|
+
return undefined;
|
|
355
|
+
}
|
|
356
|
+
return [...values]
|
|
357
|
+
.filter((value) => typeof value === 'string')
|
|
358
|
+
.sort((left, right) => left.localeCompare(right));
|
|
359
|
+
}
|
|
360
|
+
isChartWidget(ownerWidgetSelector, ownerWidgetUuid) {
|
|
361
|
+
if (ownerWidgetSelector === 'widget-data-chart' || ownerWidgetSelector === 'widget-windtrends-chart') {
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
return ownerWidgetUuid?.startsWith('widget-windtrends-chart') === true
|
|
365
|
+
|| ownerWidgetUuid?.startsWith('widget-data-chart') === true;
|
|
366
|
+
}
|
|
296
367
|
normalizeSeries(input) {
|
|
297
368
|
const seriesId = (input.seriesId || input.datasetUuid || '').trim();
|
|
298
369
|
if (!seriesId) {
|
|
@@ -310,8 +381,8 @@ class HistorySeriesService {
|
|
|
310
381
|
if (!path) {
|
|
311
382
|
throw new Error('path is required');
|
|
312
383
|
}
|
|
313
|
-
|
|
314
|
-
const isDataWidget =
|
|
384
|
+
const ownerWidgetSelector = typeof input.ownerWidgetSelector === 'string' ? input.ownerWidgetSelector.trim() : undefined;
|
|
385
|
+
const isDataWidget = this.isChartWidget(ownerWidgetSelector, ownerWidgetUuid);
|
|
315
386
|
const retentionMs = this.resolveRetentionMs(input);
|
|
316
387
|
let sampleTime;
|
|
317
388
|
if (isDataWidget) {
|
|
@@ -329,6 +400,7 @@ class HistorySeriesService {
|
|
|
329
400
|
seriesId,
|
|
330
401
|
datasetUuid,
|
|
331
402
|
ownerWidgetUuid,
|
|
403
|
+
ownerWidgetSelector,
|
|
332
404
|
path,
|
|
333
405
|
source: input.source ?? 'default',
|
|
334
406
|
context: input.context ?? 'vessels.self',
|
|
@@ -408,11 +480,17 @@ class HistorySeriesService {
|
|
|
408
480
|
if (seriesContext === sampleContext) {
|
|
409
481
|
return true;
|
|
410
482
|
}
|
|
411
|
-
if (seriesContext
|
|
412
|
-
return
|
|
483
|
+
if (this.isSelfContext(seriesContext) && this.isSelfContext(sampleContext)) {
|
|
484
|
+
return true;
|
|
413
485
|
}
|
|
414
486
|
return false;
|
|
415
487
|
}
|
|
488
|
+
isSelfContext(context) {
|
|
489
|
+
if (context === 'vessels.self') {
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
return !!this.selfContext && context === this.selfContext;
|
|
493
|
+
}
|
|
416
494
|
isSourceMatch(seriesSource, sampleSource) {
|
|
417
495
|
if (seriesSource === '*' || seriesSource === 'any') {
|
|
418
496
|
return true;
|
package/plugin/index.js
CHANGED
|
@@ -87,8 +87,8 @@ const start = (server) => {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
};
|
|
90
|
-
const historySeries = new history_series_service_1.HistorySeriesService(() => Date.now());
|
|
91
|
-
const storageService = new sqlite_history_storage_service_1.SqliteHistoryStorageService();
|
|
90
|
+
const historySeries = new history_series_service_1.HistorySeriesService(() => Date.now(), typeof server.selfId === 'string' && server.selfId.trim().length > 0 ? `vessels.${server.selfId.trim()}` : null);
|
|
91
|
+
const storageService = new sqlite_history_storage_service_1.SqliteHistoryStorageService(server.getDataDirPath());
|
|
92
92
|
let retentionSweepTimer = null;
|
|
93
93
|
let storageFlushTimer = null;
|
|
94
94
|
let sqliteInitializationPromise = null;
|
|
@@ -148,6 +148,97 @@ const start = (server) => {
|
|
|
148
148
|
nodeSqliteAvailable: nodeSqliteAvailable !== false
|
|
149
149
|
};
|
|
150
150
|
}
|
|
151
|
+
function slugify(value) {
|
|
152
|
+
return value
|
|
153
|
+
.toLowerCase()
|
|
154
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
155
|
+
.replace(/^-+|-+$/g, '');
|
|
156
|
+
}
|
|
157
|
+
function resolveBmsBatteryIdsFromSelfPath() {
|
|
158
|
+
const batteriesPath = server.getSelfPath('electrical.batteries');
|
|
159
|
+
const readCandidate = (node) => {
|
|
160
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
const root = node;
|
|
164
|
+
if (Object.prototype.hasOwnProperty.call(root, 'value')) {
|
|
165
|
+
const value = root.value;
|
|
166
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
167
|
+
return value;
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
return root;
|
|
172
|
+
};
|
|
173
|
+
const candidates = readCandidate(batteriesPath);
|
|
174
|
+
if (!candidates) {
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
return Object.keys(candidates)
|
|
178
|
+
.filter(id => /^[a-z0-9_-]+$/i.test(id))
|
|
179
|
+
.sort((left, right) => left.localeCompare(right));
|
|
180
|
+
}
|
|
181
|
+
function getExistingConcreteBmsSeries(templateSeries, existingSeries) {
|
|
182
|
+
return existingSeries
|
|
183
|
+
.filter(series => series.ownerWidgetUuid === templateSeries.ownerWidgetUuid)
|
|
184
|
+
.filter(series => series.expansionMode == null)
|
|
185
|
+
.filter(series => series.seriesId !== templateSeries.seriesId)
|
|
186
|
+
.map(series => ({ ...series }));
|
|
187
|
+
}
|
|
188
|
+
function mergeSeriesDefinitions(series) {
|
|
189
|
+
const mergedById = new Map();
|
|
190
|
+
series.forEach(item => {
|
|
191
|
+
mergedById.set(item.seriesId, item);
|
|
192
|
+
});
|
|
193
|
+
return Array.from(mergedById.values());
|
|
194
|
+
}
|
|
195
|
+
function expandTemplateSeriesDefinitions(payload, existingSeries = []) {
|
|
196
|
+
const bmsMetrics = ['capacity.stateOfCharge', 'current'];
|
|
197
|
+
const expandedById = new Map();
|
|
198
|
+
const discoveredBatteryIds = resolveBmsBatteryIdsFromSelfPath();
|
|
199
|
+
payload.forEach(series => {
|
|
200
|
+
const isBmsTemplate = series.expansionMode === 'bms-battery-tree' && series.ownerWidgetSelector === 'widget-bms';
|
|
201
|
+
if (!isBmsTemplate) {
|
|
202
|
+
expandedById.set(series.seriesId, series);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (discoveredBatteryIds.length === 0) {
|
|
206
|
+
getExistingConcreteBmsSeries(series, existingSeries).forEach(existing => {
|
|
207
|
+
expandedById.set(existing.seriesId, existing);
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const allowedBatteryIds = Array.isArray(series.allowedBatteryIds)
|
|
212
|
+
? series.allowedBatteryIds
|
|
213
|
+
.filter((id) => typeof id === 'string')
|
|
214
|
+
.map(id => id.trim())
|
|
215
|
+
.filter(id => id.length > 0)
|
|
216
|
+
: [];
|
|
217
|
+
const allowedSet = allowedBatteryIds.length > 0 ? new Set(allowedBatteryIds) : null;
|
|
218
|
+
const batteryIds = discoveredBatteryIds.filter(id => !allowedSet || allowedSet.has(id));
|
|
219
|
+
if (batteryIds.length === 0) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const source = series.source ?? 'default';
|
|
223
|
+
const sourceKey = slugify(source || 'default') || 'default';
|
|
224
|
+
batteryIds.forEach(batteryId => {
|
|
225
|
+
bmsMetrics.forEach(metric => {
|
|
226
|
+
const path = `self.electrical.batteries.${batteryId}.${metric}`;
|
|
227
|
+
const seriesId = `${series.ownerWidgetUuid}:bms:${batteryId}:${metric}:${sourceKey}`;
|
|
228
|
+
expandedById.set(seriesId, {
|
|
229
|
+
...series,
|
|
230
|
+
seriesId,
|
|
231
|
+
datasetUuid: `${series.ownerWidgetUuid}:bms:${batteryId}:${metric}:${sourceKey}`,
|
|
232
|
+
path,
|
|
233
|
+
retentionDurationMs: Number.isFinite(series.retentionDurationMs) ? series.retentionDurationMs : 24 * 60 * 60 * 1000,
|
|
234
|
+
expansionMode: null,
|
|
235
|
+
allowedBatteryIds: null
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
return Array.from(expandedById.values());
|
|
241
|
+
}
|
|
151
242
|
function getDisplaySelfPath(displayId, suffix) {
|
|
152
243
|
const tail = suffix ? `.${suffix}` : '';
|
|
153
244
|
const want = `displays.${displayId}${tail}`;
|
|
@@ -599,9 +690,6 @@ const start = (server) => {
|
|
|
599
690
|
if (!modeConfig.nodeSqliteAvailable) {
|
|
600
691
|
server.error(`[KIP][RUNTIME] node:sqlite unavailable. ${runtimeSqliteUnavailableMessage}`);
|
|
601
692
|
}
|
|
602
|
-
const serverWithApp = server;
|
|
603
|
-
const dataDirPath = serverWithApp.app?.getDataDirPath?.();
|
|
604
|
-
storageService.setDataDirPath(typeof dataDirPath === 'string' ? dataDirPath : null);
|
|
605
693
|
storageService.setRuntimeAvailability(modeConfig.nodeSqliteAvailable, runtimeSqliteUnavailableMessage ?? undefined);
|
|
606
694
|
logRuntimeDependencyVersions();
|
|
607
695
|
logOperationalMode('start-configured');
|
|
@@ -1053,14 +1141,28 @@ const start = (server) => {
|
|
|
1053
1141
|
if (!Array.isArray(payload)) {
|
|
1054
1142
|
return sendFail(res, 400, 'Body must be an array of series definitions');
|
|
1055
1143
|
}
|
|
1056
|
-
const simulated = new history_series_service_1.HistorySeriesService(() => Date.now());
|
|
1057
|
-
|
|
1144
|
+
const simulated = new history_series_service_1.HistorySeriesService(() => Date.now(), typeof server.selfId === 'string' && server.selfId.trim().length > 0 ? `vessels.${server.selfId.trim()}` : null);
|
|
1145
|
+
const currentSeries = mergeSeriesDefinitions([
|
|
1146
|
+
...(await storageService.getSeriesDefinitions()),
|
|
1147
|
+
...historySeries.listSeries()
|
|
1148
|
+
]);
|
|
1149
|
+
currentSeries.forEach(series => {
|
|
1058
1150
|
simulated.upsertSeries(series);
|
|
1059
1151
|
});
|
|
1060
1152
|
const scopedPayload = payload.map(series => ({
|
|
1061
1153
|
...series
|
|
1062
1154
|
}));
|
|
1063
|
-
const
|
|
1155
|
+
const isBatteryDiscoveryUnavailable = resolveBmsBatteryIdsFromSelfPath().length === 0;
|
|
1156
|
+
const preservedBmsSeries = isBatteryDiscoveryUnavailable
|
|
1157
|
+
? scopedPayload
|
|
1158
|
+
.filter(series => series.expansionMode === 'bms-battery-tree' && series.ownerWidgetSelector === 'widget-bms')
|
|
1159
|
+
.flatMap(series => currentSeries.filter(current => current.ownerWidgetUuid === series.ownerWidgetUuid && current.expansionMode == null && current.seriesId !== series.seriesId))
|
|
1160
|
+
: [];
|
|
1161
|
+
const expandedPayload = mergeSeriesDefinitions([
|
|
1162
|
+
...expandTemplateSeriesDefinitions(scopedPayload, currentSeries),
|
|
1163
|
+
...preservedBmsSeries
|
|
1164
|
+
]);
|
|
1165
|
+
const result = simulated.reconcileSeries(expandedPayload);
|
|
1064
1166
|
const nextSeries = simulated.listSeries();
|
|
1065
1167
|
await storageService.replaceSeriesDefinitions(nextSeries);
|
|
1066
1168
|
const seriesOutsideScope = historySeries.listSeries();
|