@mxtommy/kip 4.8.1 → 4.8.4

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 (69) hide show
  1. package/CHANGELOG.md +17 -3
  2. package/package.json +27 -4
  3. package/public/{chunk-Bp0MQwPM.js → chunk-07zHjmg-.js} +1 -1
  4. package/public/{chunk-AANk-NXH.js → chunk-0z3nIRXw.js} +3 -3
  5. package/public/{chunk-sNakMDHi.js → chunk-B09jfNia.js} +1 -1
  6. package/public/{chunk-DDqeRiB0.js → chunk-B0D54iJs.js} +1 -1
  7. package/public/{chunk-BwOIef42.js → chunk-B0eE4udE.js} +1 -1
  8. package/public/{chunk-BX6skw5k.js → chunk-B9nVBlGj.js} +1 -1
  9. package/public/{chunk-C2VQxCXa.js → chunk-BFpzxIIT.js} +1 -1
  10. package/public/{chunk-Dxlia2NE.js → chunk-BIb-xYiS.js} +1 -1
  11. package/public/{chunk-dgaOX1wC.js → chunk-BJ-wyixK.js} +1 -1
  12. package/public/chunk-BLEZOsfR.js +1 -0
  13. package/public/{chunk-CNMFlw_H.js → chunk-BN7A4g6c.js} +1 -1
  14. package/public/{chunk-By8ZVvDx.js → chunk-BTOTKeBo.js} +1 -1
  15. package/public/{chunk-BoNDCNGE.js → chunk-BUguZ7cK.js} +1 -1
  16. package/public/{chunk-DE4ylhpq.js → chunk-BZx964-p.js} +1 -1
  17. package/public/{chunk-CNNTuNlM.js → chunk-BeM64etA.js} +1 -1
  18. package/public/chunk-BfiyH3nV.js +1 -0
  19. package/public/{chunk-Ii9n-oiL.js → chunk-BjINKQ03.js} +1 -1
  20. package/public/{chunk-MjWuPUhu.js → chunk-BpcNrtxJ.js} +1 -1
  21. package/public/{chunk-CkXTl3su.js → chunk-BuvyF0y5.js} +1 -1
  22. package/public/{chunk-BKj60jdq.js → chunk-C6b8CVno.js} +1 -1
  23. package/public/{chunk-hd2_AKwt.js → chunk-CCfhzpE8.js} +1 -1
  24. package/public/{chunk-nu4vZg-z.js → chunk-CHAFm_nM.js} +1 -1
  25. package/public/{chunk-5U8d2Ygo.js → chunk-CIGP2YJm.js} +1 -1
  26. package/public/{chunk-JbEkcxJm.js → chunk-CLTHuhlN.js} +1 -1
  27. package/public/{chunk-CwSa_BNV.js → chunk-CLs_NL1y.js} +1 -1
  28. package/public/{chunk-DNS-qsFC.js → chunk-COI6IBXH.js} +1 -1
  29. package/public/{chunk-CpuB_ZZz.js → chunk-CQMVIDcp.js} +1 -1
  30. package/public/{chunk-00hwknVI.js → chunk-CXvWM_vF.js} +1 -1
  31. package/public/{chunk-BdokyhBM.js → chunk-CYYBxjrc.js} +1 -1
  32. package/public/{chunk-DzTTqoF_.js → chunk-C_OZJltF.js} +1 -1
  33. package/public/{chunk-DQnoPLyC.js → chunk-Co2wVrD6.js} +1 -1
  34. package/public/{chunk-CXvG8mMq.js → chunk-CoCQ3DW2.js} +1 -1
  35. package/public/{chunk-DchvLV_Y.js → chunk-CxPx4NZv.js} +1 -1
  36. package/public/{chunk-CA6RRaxd.js → chunk-CyVRpGoJ.js} +1 -1
  37. package/public/{chunk-CL2xiH57.js → chunk-D-_-GUVr.js} +1 -1
  38. package/public/{chunk-C6UXF7BI.js → chunk-D1HYfaL_.js} +1 -1
  39. package/public/{chunk-DjZgzSiH.js → chunk-D2953Rfp.js} +1 -1
  40. package/public/{chunk-BJz7Ij9E.js → chunk-DIl1k00y.js} +1 -1
  41. package/public/{chunk-B6bEhzNR.js → chunk-DPnbM-aB.js} +1 -1
  42. package/public/{chunk-CIlfzV9R.js → chunk-DTN-sa1K.js} +1 -1
  43. package/public/{chunk-BOAD0N2a.js → chunk-DUEKoZhd.js} +1 -1
  44. package/public/{chunk-DumLX5k_.js → chunk-DmkAKXR1.js} +1 -1
  45. package/public/chunk-Do8wqING.js +1 -0
  46. package/public/{chunk-DXkKj21H.js → chunk-JhFkJS7i.js} +1 -1
  47. package/public/{chunk-B82ADJSs.js → chunk-OeQDZKFO.js} +1 -1
  48. package/public/{chunk-PheXEqie.js → chunk-_zRMMvCu.js} +1 -1
  49. package/public/{chunk-CrBxxZCe.js → chunk-eQ1pa34Q.js} +1 -1
  50. package/public/{chunk-D4xZ2tGB.js → chunk-ibauGmSP.js} +1 -1
  51. package/public/{chunk-VgxMqRPk.js → chunk-otzyItXL.js} +1 -1
  52. package/public/{chunk-BQr-tSkv.js → chunk-q9NoPvcx.js} +1 -1
  53. package/public/{chunk-DWLISAZi.js → chunk-szQs8fiq.js} +1 -1
  54. package/public/{chunk-tAiV3ARh.js → chunk-x7L6DXVK.js} +1 -1
  55. package/public/index.html +1 -1
  56. package/public/{main-BAV5VDOD.js → main-YKDNK5VZ.js} +20 -20
  57. package/plugin/history-series.service.js +0 -637
  58. package/plugin/index.js +0 -1322
  59. package/plugin/kip-series-contract.js +0 -31
  60. package/plugin/openApi.json +0 -855
  61. package/plugin/sqlite-history-storage.service.js +0 -1124
  62. package/plugin/src/history-series.service.js +0 -634
  63. package/plugin/src/index.js +0 -1322
  64. package/plugin/src/kip-series-contract.js +0 -31
  65. package/plugin/src/openApi.json +0 -855
  66. package/plugin/src/sqlite-history-storage.service.js +0 -1122
  67. package/public/chunk-Blokg6Ds.js +0 -1
  68. package/public/chunk-CpMCBDvH.js +0 -1
  69. package/public/chunk-y1imvaA-.js +0 -1
@@ -1,637 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HistorySeriesService = exports.isKipTemplateSeriesDefinition = exports.isKipSolarTemplateSeriesDefinition = exports.isKipSeriesEnabled = exports.isKipElectricalTemplateSeriesDefinition = exports.isKipConcreteSeriesDefinition = exports.isKipBmsTemplateSeriesDefinition = void 0;
4
- const kip_series_contract_1 = require("./kip-series-contract");
5
- Object.defineProperty(exports, "isKipElectricalTemplateSeriesDefinition", { enumerable: true, get: function () { return kip_series_contract_1.isKipElectricalTemplateSeriesDefinition; } });
6
- Object.defineProperty(exports, "isKipBmsTemplateSeriesDefinition", { enumerable: true, get: function () { return kip_series_contract_1.isKipBmsTemplateSeriesDefinition; } });
7
- Object.defineProperty(exports, "isKipConcreteSeriesDefinition", { enumerable: true, get: function () { return kip_series_contract_1.isKipConcreteSeriesDefinition; } });
8
- Object.defineProperty(exports, "isKipSeriesEnabled", { enumerable: true, get: function () { return kip_series_contract_1.isKipSeriesEnabled; } });
9
- Object.defineProperty(exports, "isKipSolarTemplateSeriesDefinition", { enumerable: true, get: function () { return kip_series_contract_1.isKipSolarTemplateSeriesDefinition; } });
10
- Object.defineProperty(exports, "isKipTemplateSeriesDefinition", { enumerable: true, get: function () { return kip_series_contract_1.isKipTemplateSeriesDefinition; } });
11
- /**
12
- * Manages history capture series definitions and serves History API-compatible query results.
13
- */
14
- class HistorySeriesService {
15
- nowProvider;
16
- selfContext;
17
- seriesById = new Map();
18
- enabledSeriesKeysByPath = new Map();
19
- lastAcceptedTimestampBySeriesKey = new Map();
20
- sampleSink = null;
21
- constructor(nowProvider = () => Date.now(), selfContext = null) {
22
- this.nowProvider = nowProvider;
23
- this.selfContext = selfContext;
24
- }
25
- /**
26
- * Returns all configured series sorted by `seriesId`.
27
- *
28
- * @returns Ordered list of series definitions.
29
- *
30
- * @example
31
- * const list = service.listSeries();
32
- * console.log(list.length);
33
- */
34
- listSeries() {
35
- return Array.from(this.seriesById.values()).sort((left, right) => {
36
- return left.seriesId.localeCompare(right.seriesId);
37
- });
38
- }
39
- /**
40
- * Finds a series by identifier.
41
- *
42
- * @param {string} seriesId Series identifier.
43
- * @returns {ISeriesDefinition | null} Matching series or null.
44
- *
45
- * @example
46
- * const row = service.findSeriesById('series-1');
47
- */
48
- findSeriesById(seriesId) {
49
- return this.seriesById.get(seriesId) ?? null;
50
- }
51
- /**
52
- * Creates or updates a series definition.
53
- *
54
- * @param {ISeriesDefinition} input The incoming series definition payload.
55
- * @returns {ISeriesDefinition} The normalized and stored series definition.
56
- *
57
- * @example
58
- * service.upsertSeries({
59
- * seriesId: 'abc',
60
- * datasetUuid: 'abc',
61
- * ownerWidgetUuid: 'widget-1',
62
- * path: 'navigation.speedOverGround'
63
- * });
64
- */
65
- upsertSeries(input) {
66
- const normalized = this.normalizeSeries(input);
67
- const key = normalized.seriesId;
68
- this.seriesById.set(key, normalized);
69
- this.rebuildEnabledPathIndex();
70
- return normalized;
71
- }
72
- /**
73
- * Registers a callback invoked for every accepted sample.
74
- *
75
- * @param {(sample: IRecordedSeriesSample) => void | null} sink Callback used to forward samples to persistent storage.
76
- * @returns {void}
77
- *
78
- * @example
79
- * service.setSampleSink(sample => console.log(sample.seriesId));
80
- */
81
- setSampleSink(sink) {
82
- this.sampleSink = sink;
83
- }
84
- /**
85
- * Deletes an existing series definition and all captured samples for the series.
86
- *
87
- * @param {string} seriesId Unique series identifier.
88
- * @returns {boolean} True when a series existed and was deleted.
89
- *
90
- * @example
91
- * const deleted = service.deleteSeries('abc');
92
- */
93
- deleteSeries(seriesId) {
94
- const keysToDelete = Array.from(this.seriesById.entries())
95
- .filter(([, series]) => series.seriesId === seriesId)
96
- .map(([key]) => key);
97
- if (keysToDelete.length === 0) {
98
- return false;
99
- }
100
- keysToDelete.forEach(key => {
101
- this.seriesById.delete(key);
102
- this.lastAcceptedTimestampBySeriesKey.delete(key);
103
- });
104
- this.rebuildEnabledPathIndex();
105
- return true;
106
- }
107
- /**
108
- * Reconciles the entire desired series set against the current state.
109
- *
110
- * @param {ISeriesDefinition[]} desiredSeries Full desired series payload list.
111
- * @returns {{ created: number; updated: number; deleted: number; total: number }} Reconciliation summary.
112
- *
113
- * @example
114
- * const result = service.reconcileSeries([{ seriesId: 's1', datasetUuid: 's1', ownerWidgetUuid: 'w1', path: 'p' }]);
115
- * console.log(result.created, result.deleted);
116
- */
117
- reconcileSeries(desiredSeries) {
118
- const now = this.nowProvider();
119
- const desiredById = new Map();
120
- desiredSeries.forEach(entry => {
121
- const normalized = this.normalizeSeries(entry);
122
- const key = normalized.seriesId;
123
- desiredById.set(key, normalized);
124
- });
125
- let created = 0;
126
- let updated = 0;
127
- let deleted = 0;
128
- desiredById.forEach((desired, seriesKey) => {
129
- const existing = this.seriesById.get(seriesKey);
130
- // Always update reconcile_ts on reconcile
131
- const desiredWithReconcile = { ...desired, reconcileTs: now };
132
- if (!existing) {
133
- this.seriesById.set(seriesKey, desiredWithReconcile);
134
- created += 1;
135
- return;
136
- }
137
- if (!this.areSeriesEquivalent(existing, desired)) {
138
- this.seriesById.set(seriesKey, desiredWithReconcile);
139
- updated += 1;
140
- }
141
- else {
142
- // Even if not updated, update reconcileTs
143
- this.seriesById.set(seriesKey, { ...existing, reconcileTs: now });
144
- }
145
- });
146
- Array.from(this.seriesById.keys()).forEach(seriesKey => {
147
- if (!desiredById.has(seriesKey)) {
148
- this.seriesById.delete(seriesKey);
149
- this.lastAcceptedTimestampBySeriesKey.delete(seriesKey);
150
- deleted += 1;
151
- }
152
- });
153
- this.rebuildEnabledPathIndex();
154
- return {
155
- created,
156
- updated,
157
- deleted,
158
- total: this.seriesById.size
159
- };
160
- }
161
- /**
162
- * Records a single numeric sample for a configured series.
163
- *
164
- * @param {string} seriesId Unique series identifier.
165
- * @param {number} value Numeric sample value.
166
- * @param {number} timestamp Sample timestamp in milliseconds.
167
- * @returns {boolean} True when sample was accepted.
168
- *
169
- * @example
170
- * service.recordSample('abc', 12.4, Date.now());
171
- */
172
- recordSample(seriesId, value, timestamp) {
173
- if (!this.seriesById.has(seriesId)) {
174
- return false;
175
- }
176
- return this.recordSampleByKey(seriesId, value, timestamp);
177
- }
178
- recordSampleByKey(seriesKey, value, timestamp) {
179
- const series = this.seriesById.get(seriesKey);
180
- if (!series || series.enabled === false || !Number.isFinite(value) || !Number.isFinite(timestamp)) {
181
- return false;
182
- }
183
- const previousTimestamp = this.lastAcceptedTimestampBySeriesKey.get(seriesKey);
184
- // Enforces a minimum of 1 second to prevent excessive sampling on short retention durations
185
- const minSampleTime = Math.max(Number(series.sampleTime) || 0, 1000);
186
- if (previousTimestamp !== undefined && (timestamp - previousTimestamp) < minSampleTime) {
187
- return false;
188
- }
189
- const context = series.context ?? 'vessels.self';
190
- const source = series.source ?? 'default';
191
- this.sampleSink?.({
192
- seriesId: series.seriesId,
193
- datasetUuid: series.datasetUuid,
194
- ownerWidgetUuid: series.ownerWidgetUuid,
195
- path: series.path,
196
- context,
197
- source,
198
- timestamp,
199
- value
200
- });
201
- this.lastAcceptedTimestampBySeriesKey.set(seriesKey, timestamp);
202
- return true;
203
- }
204
- /**
205
- * Records a sample based on a Signal K stream value by matching path/context/source against configured series.
206
- *
207
- * @param {{ path?: unknown; value?: unknown; timestamp?: unknown; context?: unknown; source?: unknown; $source?: unknown }} sample Signal K normalized value entry.
208
- * @returns {number} Number of configured series that accepted the sample.
209
- *
210
- * @example
211
- * const count = service.recordFromSignalKSample({ path: 'navigation.speedOverGround', value: 6.2, timestamp: new Date().toISOString() });
212
- */
213
- recordFromSignalKSample(sample) {
214
- const path = this.normalizePathIdentifier(typeof sample.path === 'string' ? sample.path : '');
215
- if (!path) {
216
- return 0;
217
- }
218
- const ts = this.resolveTimestamp(sample.timestamp);
219
- const context = typeof sample.context === 'string' && sample.context ? sample.context : 'vessels.self';
220
- const source = this.resolveSource(sample);
221
- const leafSamples = this.extractNumericLeafSamples(path, sample.value);
222
- if (leafSamples.length === 0) {
223
- return 0;
224
- }
225
- let recorded = 0;
226
- leafSamples.forEach(leaf => {
227
- const seriesKeys = this.enabledSeriesKeysByPath.get(leaf.path);
228
- if (!seriesKeys || seriesKeys.length === 0) {
229
- return;
230
- }
231
- const hasSpecificSourceMatch = seriesKeys.some(seriesKey => {
232
- const series = this.seriesById.get(seriesKey);
233
- if (!series) {
234
- return false;
235
- }
236
- const seriesContext = series.context ?? 'vessels.self';
237
- if (!this.isContextMatch(seriesContext, context)) {
238
- return false;
239
- }
240
- const seriesSource = series.source ?? 'default';
241
- return seriesSource !== 'default' && seriesSource === source;
242
- });
243
- seriesKeys.forEach(seriesKey => {
244
- const series = this.seriesById.get(seriesKey);
245
- if (!series) {
246
- return;
247
- }
248
- const seriesContext = series.context ?? 'vessels.self';
249
- if (!this.isContextMatch(seriesContext, context)) {
250
- return;
251
- }
252
- const seriesSource = series.source ?? 'default';
253
- if (!this.isSourceMatch(seriesSource, source)) {
254
- return;
255
- }
256
- if (seriesSource === 'default' && source !== 'default' && hasSpecificSourceMatch) {
257
- return;
258
- }
259
- if (this.recordSampleByKey(seriesKey, leaf.value, ts)) {
260
- recorded += 1;
261
- }
262
- });
263
- });
264
- return recorded;
265
- }
266
- extractNumericLeafSamples(basePath, value) {
267
- const samples = [];
268
- const addNumeric = (samplePath, sampleValue) => {
269
- const normalizedPath = this.normalizePathIdentifier(samplePath);
270
- if (!normalizedPath) {
271
- return;
272
- }
273
- const numericValue = Number(sampleValue);
274
- if (!Number.isFinite(numericValue)) {
275
- return;
276
- }
277
- samples.push({ path: normalizedPath, value: numericValue });
278
- };
279
- const walk = (currentPath, currentValue) => {
280
- if (currentValue && typeof currentValue === 'object') {
281
- if (Array.isArray(currentValue)) {
282
- currentValue.forEach((entry, index) => {
283
- walk(`${currentPath}.${index}`, entry);
284
- });
285
- return;
286
- }
287
- Object.entries(currentValue).forEach(([key, child]) => {
288
- walk(`${currentPath}.${key}`, child);
289
- });
290
- return;
291
- }
292
- addNumeric(currentPath, currentValue);
293
- };
294
- walk(basePath, value);
295
- return samples;
296
- }
297
- /**
298
- * Returns all known history paths.
299
- *
300
- * @returns {string[]} Ordered unique path list.
301
- *
302
- * @example
303
- * const paths = service.getPaths();
304
- */
305
- getPaths() {
306
- const paths = new Set();
307
- this.seriesById.forEach(series => {
308
- paths.add(series.path);
309
- });
310
- return Array.from(paths).sort();
311
- }
312
- /**
313
- * Returns all known history contexts.
314
- *
315
- * @returns {string[]} Ordered unique context list.
316
- *
317
- * @example
318
- * const contexts = service.getContexts();
319
- */
320
- getContexts() {
321
- const contexts = new Set();
322
- this.seriesById.forEach(series => {
323
- contexts.add(series.context ?? 'vessels.self');
324
- });
325
- return Array.from(contexts).sort();
326
- }
327
- rebuildEnabledPathIndex() {
328
- this.enabledSeriesKeysByPath.clear();
329
- this.seriesById.forEach((series, seriesKey) => {
330
- if (series.enabled === false) {
331
- return;
332
- }
333
- const keys = this.enabledSeriesKeysByPath.get(series.path) ?? [];
334
- keys.push(seriesKey);
335
- this.enabledSeriesKeysByPath.set(series.path, keys);
336
- });
337
- }
338
- areSeriesEquivalent(left, right) {
339
- const leftComparable = this.toComparableSeries(left);
340
- const rightComparable = this.toComparableSeries(right);
341
- return leftComparable.seriesId === rightComparable.seriesId
342
- && leftComparable.datasetUuid === rightComparable.datasetUuid
343
- && leftComparable.ownerWidgetUuid === rightComparable.ownerWidgetUuid
344
- && leftComparable.ownerWidgetSelector === rightComparable.ownerWidgetSelector
345
- && leftComparable.path === rightComparable.path
346
- && leftComparable.expansionMode === rightComparable.expansionMode
347
- && leftComparable.familyKey === rightComparable.familyKey
348
- && this.areStringArraysEquivalent(leftComparable.allowedIds, rightComparable.allowedIds)
349
- && this.areTrackedDevicesEquivalent(leftComparable.trackedDevices, rightComparable.trackedDevices)
350
- && leftComparable.source === rightComparable.source
351
- && leftComparable.context === rightComparable.context
352
- && leftComparable.timeScale === rightComparable.timeScale
353
- && leftComparable.period === rightComparable.period
354
- && leftComparable.retentionDurationMs === rightComparable.retentionDurationMs
355
- && leftComparable.sampleTime === rightComparable.sampleTime
356
- && leftComparable.enabled === rightComparable.enabled
357
- && this.areStringArraysEquivalent(leftComparable.methods, rightComparable.methods);
358
- }
359
- toComparableSeries(series) {
360
- const { reconcileTs, ...comparable } = series;
361
- void reconcileTs;
362
- return {
363
- ...comparable,
364
- allowedIds: this.normalizeComparableStringArray(comparable.allowedIds),
365
- trackedDevices: this.normalizeComparableTrackedDevices(comparable.trackedDevices),
366
- methods: this.normalizeComparableStringArray(comparable.methods)
367
- };
368
- }
369
- areTrackedDevicesEquivalent(left, right) {
370
- const normalizedLeft = this.normalizeComparableTrackedDevices(left) ?? [];
371
- const normalizedRight = this.normalizeComparableTrackedDevices(right) ?? [];
372
- if (normalizedLeft.length !== normalizedRight.length) {
373
- return false;
374
- }
375
- return normalizedLeft.every((value, index) => {
376
- const candidate = normalizedRight[index];
377
- return value.id === candidate?.id && value.source === candidate?.source;
378
- });
379
- }
380
- areStringArraysEquivalent(left, right) {
381
- const normalizedLeft = this.normalizeComparableStringArray(left) ?? [];
382
- const normalizedRight = this.normalizeComparableStringArray(right) ?? [];
383
- if (normalizedLeft.length !== normalizedRight.length) {
384
- return false;
385
- }
386
- return normalizedLeft.every((value, index) => value === normalizedRight[index]);
387
- }
388
- normalizeComparableStringArray(values) {
389
- if (!Array.isArray(values) || values.length === 0) {
390
- return undefined;
391
- }
392
- return [...values]
393
- .filter((value) => typeof value === 'string')
394
- .sort((left, right) => left.localeCompare(right));
395
- }
396
- normalizeComparableTrackedDevices(values) {
397
- if (!Array.isArray(values) || values.length === 0) {
398
- return undefined;
399
- }
400
- const normalizedByKey = new Map();
401
- values.forEach(value => {
402
- if (!value || typeof value !== 'object') {
403
- return;
404
- }
405
- const id = typeof value.id === 'string' ? value.id.trim() : '';
406
- const sourceText = typeof value.source === 'string' ? value.source.trim() : '';
407
- const source = sourceText.length > 0 ? sourceText : 'default';
408
- if (!id) {
409
- return;
410
- }
411
- normalizedByKey.set(`${id}||${source}`, { id, source });
412
- });
413
- if (normalizedByKey.size === 0) {
414
- return undefined;
415
- }
416
- return Array.from(normalizedByKey.values()).sort((left, right) => {
417
- const idCompare = left.id.localeCompare(right.id);
418
- return idCompare !== 0 ? idCompare : left.source.localeCompare(right.source);
419
- });
420
- }
421
- isChartWidget(ownerWidgetSelector, ownerWidgetUuid) {
422
- if (ownerWidgetSelector === 'widget-data-chart' || ownerWidgetSelector === 'widget-windtrends-chart') {
423
- return true;
424
- }
425
- return ownerWidgetUuid?.startsWith('widget-windtrends-chart') === true
426
- || ownerWidgetUuid?.startsWith('widget-data-chart') === true;
427
- }
428
- normalizeSeries(input) {
429
- const seriesId = (input.seriesId || input.datasetUuid || '').trim();
430
- if (!seriesId) {
431
- throw new Error('seriesId is required');
432
- }
433
- const datasetUuid = (input.datasetUuid || seriesId).trim();
434
- if (!datasetUuid) {
435
- throw new Error('datasetUuid is required');
436
- }
437
- const ownerWidgetUuid = (input.ownerWidgetUuid || '').trim();
438
- if (!ownerWidgetUuid) {
439
- throw new Error('ownerWidgetUuid is required');
440
- }
441
- const path = this.normalizePathIdentifier(input.path || '');
442
- if (!path) {
443
- throw new Error('path is required');
444
- }
445
- const ownerWidgetSelector = typeof input.ownerWidgetSelector === 'string' ? input.ownerWidgetSelector.trim() : null;
446
- const expansionMode = input.expansionMode ?? null;
447
- const familyKey = expansionMode ? this.expansionModeToFamilyKey(expansionMode) : null;
448
- let normalizedTemplateSelector = null;
449
- if (expansionMode) {
450
- const selectorByMode = {
451
- 'bms-battery-tree': 'widget-bms',
452
- 'solar-tree': 'widget-solar-charger',
453
- 'charger-tree': 'widget-charger',
454
- 'inverter-tree': 'widget-inverter',
455
- 'alternator-tree': 'widget-alternator',
456
- 'ac-tree': 'widget-ac'
457
- };
458
- const requiredSelector = selectorByMode[expansionMode];
459
- if (ownerWidgetSelector !== requiredSelector) {
460
- throw new Error(`Template series mode "${expansionMode}" must use ownerWidgetSelector "${requiredSelector}"`);
461
- }
462
- normalizedTemplateSelector = requiredSelector;
463
- }
464
- const normalizedMethods = this.normalizeComparableStringArray(input.methods);
465
- const normalizedAllowedIds = expansionMode
466
- ? this.normalizeComparableStringArray(input.allowedIds)
467
- : undefined;
468
- const normalizedTrackedDevices = expansionMode
469
- ? this.normalizeComparableTrackedDevices(input.trackedDevices)
470
- : undefined;
471
- const isDataWidget = this.isChartWidget(ownerWidgetSelector, ownerWidgetUuid);
472
- const retentionMs = this.resolveRetentionMs(input);
473
- let sampleTime;
474
- if (isDataWidget) {
475
- // For chart type widgets we use retention duration to dynamically calculate sampleTime to
476
- // aims for around 120 samples.
477
- sampleTime = retentionMs ? Math.max(1000, Math.round(retentionMs / 120)) : 1000;
478
- }
479
- else {
480
- // Non chart type widgets, ie historical Time-Series, have a fixed sampleTime of 15 sec that is
481
- // a good median amount of samples for the dynamically queryable chart display range (15 min up to 24h).
482
- sampleTime = 15000; // ms
483
- }
484
- const normalizedBase = {
485
- seriesId,
486
- datasetUuid,
487
- ownerWidgetUuid,
488
- ownerWidgetSelector,
489
- path,
490
- familyKey,
491
- source: input.source ?? 'default',
492
- context: input.context ?? 'vessels.self',
493
- timeScale: input.timeScale ?? null,
494
- period: Number.isFinite(input.period) ? input.period ?? null : null,
495
- enabled: input.enabled !== false,
496
- retentionDurationMs: retentionMs,
497
- sampleTime,
498
- methods: normalizedMethods,
499
- reconcileTs: input.reconcileTs
500
- };
501
- if (expansionMode) {
502
- if (!normalizedTemplateSelector) {
503
- throw new Error(`Template series mode \"${expansionMode}\" must resolve a non-null ownerWidgetSelector`);
504
- }
505
- const templateSeries = {
506
- ...normalizedBase,
507
- ownerWidgetSelector: normalizedTemplateSelector,
508
- expansionMode,
509
- familyKey,
510
- allowedIds: normalizedAllowedIds ?? null,
511
- trackedDevices: normalizedTrackedDevices ?? null
512
- };
513
- return templateSeries;
514
- }
515
- const concreteSeries = {
516
- ...normalizedBase,
517
- expansionMode: null,
518
- familyKey: null,
519
- allowedIds: null,
520
- trackedDevices: null
521
- };
522
- return concreteSeries;
523
- }
524
- expansionModeToFamilyKey(mode) {
525
- switch (mode) {
526
- case 'bms-battery-tree':
527
- return 'batteries';
528
- case 'solar-tree':
529
- return 'solar';
530
- case 'charger-tree':
531
- return 'chargers';
532
- case 'inverter-tree':
533
- return 'inverters';
534
- case 'alternator-tree':
535
- return 'alternators';
536
- case 'ac-tree':
537
- return 'ac';
538
- default:
539
- throw new Error(`Unsupported expansion mode: ${String(mode)}`);
540
- }
541
- }
542
- resolveRetentionMs(series) {
543
- if (Number.isFinite(series.retentionDurationMs) && series.retentionDurationMs > 0) {
544
- return series.retentionDurationMs;
545
- }
546
- const period = Number(series.period ?? 0);
547
- const scale = String(series.timeScale ?? '').toLowerCase();
548
- if (scale === 'last minute')
549
- return 60_000;
550
- if (scale === 'last 5 minutes')
551
- return 5 * 60_000;
552
- if (scale === 'last 30 minutes')
553
- return 30 * 60_000;
554
- if (scale === 'second')
555
- return Math.max(0, period) * 1_000;
556
- if (scale === 'minute')
557
- return Math.max(0, period) * 60_000;
558
- if (scale === 'hour')
559
- return Math.max(0, period) * 60 * 60_000;
560
- if (scale === 'day')
561
- return Math.max(0, period) * 24 * 60 * 60_000;
562
- return 24 * 60 * 60_000;
563
- }
564
- normalizePathIdentifier(path) {
565
- const trimmed = String(path).trim();
566
- if (!trimmed) {
567
- return '';
568
- }
569
- if (trimmed.startsWith('vessels.self.')) {
570
- return trimmed.slice('vessels.self.'.length);
571
- }
572
- if (trimmed.startsWith('self.')) {
573
- return trimmed.slice('self.'.length);
574
- }
575
- return trimmed;
576
- }
577
- resolveTimestamp(timestamp) {
578
- if (typeof timestamp === 'number' && Number.isFinite(timestamp)) {
579
- return timestamp;
580
- }
581
- if (typeof timestamp === 'string') {
582
- const parsed = Date.parse(timestamp);
583
- if (Number.isFinite(parsed)) {
584
- return parsed;
585
- }
586
- }
587
- return this.nowProvider();
588
- }
589
- resolveSource(sample) {
590
- if (typeof sample.$source === 'string' && sample.$source.trim().length > 0) {
591
- return sample.$source;
592
- }
593
- if (typeof sample.source === 'string' && sample.source.trim().length > 0) {
594
- return sample.source;
595
- }
596
- if (sample.source && typeof sample.source === 'object') {
597
- const source = sample.source;
598
- const label = typeof source.label === 'string' ? source.label : '';
599
- const src = typeof source.src === 'string' || typeof source.src === 'number' ? String(source.src) : '';
600
- if (label && src) {
601
- return `${label}.${src}`;
602
- }
603
- if (label) {
604
- return label;
605
- }
606
- }
607
- return 'default';
608
- }
609
- isContextMatch(seriesContext, sampleContext) {
610
- if (seriesContext === sampleContext) {
611
- return true;
612
- }
613
- if (this.isSelfContext(seriesContext) && this.isSelfContext(sampleContext)) {
614
- return true;
615
- }
616
- return false;
617
- }
618
- isSelfContext(context) {
619
- if (context === 'vessels.self') {
620
- return true;
621
- }
622
- return !!this.selfContext && context === this.selfContext;
623
- }
624
- isSourceMatch(seriesSource, sampleSource) {
625
- if (seriesSource === '*' || seriesSource === 'any') {
626
- return true;
627
- }
628
- if (seriesSource === 'default') {
629
- return true;
630
- }
631
- if (!sampleSource) {
632
- return false;
633
- }
634
- return seriesSource === sampleSource;
635
- }
636
- }
637
- exports.HistorySeriesService = HistorySeriesService;