@mxtommy/kip 4.8.0-beta.1 → 4.8.1

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 (113) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/LICENSE +3 -0
  3. package/README.md +56 -49
  4. package/package.json +24 -21
  5. package/plugin/history-series.service.js +108 -30
  6. package/plugin/index.js +157 -115
  7. package/plugin/kip-series-contract.js +12 -3
  8. package/plugin/openApi.json +64 -13
  9. package/plugin/sqlite-history-storage.service.js +4 -2
  10. package/plugin/src/history-series.service.js +634 -0
  11. package/plugin/src/index.js +1322 -0
  12. package/plugin/src/kip-series-contract.js +31 -0
  13. package/plugin/src/openApi.json +855 -0
  14. package/plugin/src/sqlite-history-storage.service.js +1122 -0
  15. package/public/3rdpartylicenses.txt +569 -727
  16. package/public/assets/help-docs/chartplotter.md +1 -1
  17. package/public/assets/help-docs/contributing-widgets.md +40 -0
  18. package/public/assets/help-docs/dashboards.md +6 -0
  19. package/public/assets/help-docs/history-api.md +69 -71
  20. package/public/assets/help-docs/menu.json +27 -7
  21. package/public/assets/help-docs/time-series.md +81 -0
  22. package/public/assets/help-docs/welcome.md +1 -1
  23. package/public/assets/svg/icons.svg +36 -1
  24. package/public/chunk-00hwknVI.js +1 -0
  25. package/public/chunk-5U8d2Ygo.js +14 -0
  26. package/public/chunk-AANk-NXH.js +1015 -0
  27. package/public/chunk-B6bEhzNR.js +1 -0
  28. package/public/chunk-B82ADJSs.js +1 -0
  29. package/public/chunk-BJz7Ij9E.js +1 -0
  30. package/public/chunk-BKj60jdq.js +1 -0
  31. package/public/chunk-BOAD0N2a.js +2 -0
  32. package/public/chunk-BQr-tSkv.js +1 -0
  33. package/public/chunk-BX6skw5k.js +1 -0
  34. package/public/chunk-BdokyhBM.js +1 -0
  35. package/public/chunk-Blokg6Ds.js +1 -0
  36. package/public/chunk-BoNDCNGE.js +1 -0
  37. package/public/chunk-Bp0MQwPM.js +1 -0
  38. package/public/chunk-BwOIef42.js +1 -0
  39. package/public/chunk-By8ZVvDx.js +2 -0
  40. package/public/chunk-C2VQxCXa.js +1 -0
  41. package/public/chunk-C6UXF7BI.js +1 -0
  42. package/public/chunk-CA6RRaxd.js +1 -0
  43. package/public/chunk-CIlfzV9R.js +1 -0
  44. package/public/chunk-CL2xiH57.js +1 -0
  45. package/public/chunk-CNMFlw_H.js +1 -0
  46. package/public/chunk-CNNTuNlM.js +1 -0
  47. package/public/chunk-CQNVyz8q.js +1 -0
  48. package/public/chunk-CXvG8mMq.js +1 -0
  49. package/public/chunk-CaD3zVfK.js +1 -0
  50. package/public/chunk-CkXTl3su.js +1 -0
  51. package/public/chunk-CpMCBDvH.js +1 -0
  52. package/public/chunk-CpuB_ZZz.js +2 -0
  53. package/public/chunk-CrBxxZCe.js +1 -0
  54. package/public/chunk-CwSa_BNV.js +1 -0
  55. package/public/chunk-D4xZ2tGB.js +39 -0
  56. package/public/chunk-DDqeRiB0.js +1 -0
  57. package/public/chunk-DE4ylhpq.js +1 -0
  58. package/public/chunk-DNS-qsFC.js +1 -0
  59. package/public/chunk-DQnoPLyC.js +1 -0
  60. package/public/chunk-DWLISAZi.js +1 -0
  61. package/public/chunk-DXkKj21H.js +1 -0
  62. package/public/chunk-DchvLV_Y.js +1 -0
  63. package/public/chunk-DjZgzSiH.js +1 -0
  64. package/public/chunk-DumLX5k_.js +1 -0
  65. package/public/chunk-Dxlia2NE.js +1 -0
  66. package/public/chunk-DzTTqoF_.js +1 -0
  67. package/public/chunk-H9_eqTYx.js +1 -0
  68. package/public/chunk-Ii9n-oiL.js +1 -0
  69. package/public/chunk-JbEkcxJm.js +2 -0
  70. package/public/chunk-MjWuPUhu.js +1 -0
  71. package/public/chunk-PheXEqie.js +1 -0
  72. package/public/chunk-TakPRI3t.js +2 -0
  73. package/public/chunk-VgxMqRPk.js +3 -0
  74. package/public/chunk-dgaOX1wC.js +382 -0
  75. package/public/chunk-hd2_AKwt.js +1 -0
  76. package/public/chunk-nu4vZg-z.js +1 -0
  77. package/public/chunk-sNakMDHi.js +1 -0
  78. package/public/chunk-tAiV3ARh.js +1 -0
  79. package/public/chunk-y1imvaA-.js +1 -0
  80. package/public/index.html +4 -4
  81. package/public/main-BAV5VDOD.js +7109 -0
  82. package/public/polyfills-5MJXXCNC.js +1 -0
  83. package/public/styles-JZSMCSRO.css +1 -0
  84. package/public/assets/help-docs/widget-historical-series.md +0 -110
  85. package/public/chunk-2TP7C66X.js +0 -5
  86. package/public/chunk-4KHUCTYN.js +0 -3
  87. package/public/chunk-6ISGIPNP.js +0 -1
  88. package/public/chunk-7JGTAL26.js +0 -10
  89. package/public/chunk-C23GLDFH.js +0 -62
  90. package/public/chunk-CB4E7PPA.js +0 -4
  91. package/public/chunk-DNM5XUIF.js +0 -1
  92. package/public/chunk-HF7V6XFA.js +0 -2
  93. package/public/chunk-HXR2BKDL.js +0 -4
  94. package/public/chunk-ILQQCGMJ.js +0 -5
  95. package/public/chunk-IXUKD73N.js +0 -5
  96. package/public/chunk-JMZYPL54.js +0 -2
  97. package/public/chunk-KJCBQE6W.js +0 -6
  98. package/public/chunk-LEY6MANN.js +0 -16
  99. package/public/chunk-O5BTKN5D.js +0 -1
  100. package/public/chunk-OH5KNIQ7.js +0 -50
  101. package/public/chunk-Q32FSCUX.js +0 -1
  102. package/public/chunk-RIX4VQJ2.js +0 -1
  103. package/public/chunk-SHJMXSDM.js +0 -1
  104. package/public/chunk-T4VTC6GW.js +0 -1
  105. package/public/chunk-VFJD3XKT.js +0 -1
  106. package/public/chunk-VFZSH4TC.js +0 -1
  107. package/public/chunk-W6MCE3GH.js +0 -6
  108. package/public/chunk-WAOG456B.js +0 -2
  109. package/public/chunk-XBOM6DMF.js +0 -9
  110. package/public/main-BECMST5R.js +0 -1
  111. package/public/polyfills-BWA36QKG.js +0 -1
  112. package/public/scripts-M6PHZBML.js +0 -14
  113. package/public/styles-DYEO23GZ.css +0 -1
package/plugin/index.js CHANGED
@@ -65,7 +65,7 @@ const start = (server) => {
65
65
  const CONFIG_SCHEMA = {
66
66
  type: 'object',
67
67
  title: 'Remote Control and Data Series',
68
- description: 'NOTE: All plugin settings are also managed from within KIP\'s Display Options panel. Changes made here will be overridden when KIP applies settings from the Display Options.',
68
+ description: 'NOTE: All plugin settings are also managed from within KIP\'s Display Options panel. Changes made here will be overridden when KIP applies settings from the Display Options. See the KIP Time-Series help documentation for details.',
69
69
  properties: {
70
70
  nodeSqliteAvailable: {
71
71
  type: 'boolean',
@@ -155,37 +155,105 @@ const start = (server) => {
155
155
  .replace(/[^a-z0-9]+/g, '-')
156
156
  .replace(/^-+|-+$/g, '');
157
157
  }
158
- function resolveBmsBatteryIdsFromSelfPath() {
159
- const batteriesPath = server.getSelfPath('electrical.batteries');
160
- const readCandidate = (node) => {
161
- if (!node || typeof node !== 'object' || Array.isArray(node)) {
162
- return null;
163
- }
164
- const root = node;
165
- if (Object.prototype.hasOwnProperty.call(root, 'value')) {
166
- const value = root.value;
167
- if (value && typeof value === 'object' && !Array.isArray(value)) {
168
- return value;
169
- }
158
+ const ELECTRICAL_SERIES_PREFIX_BY_FAMILY = {
159
+ batteries: 'bms',
160
+ solar: 'solar',
161
+ chargers: 'charger',
162
+ inverters: 'inverter',
163
+ alternators: 'alternator',
164
+ ac: 'ac'
165
+ };
166
+ const ELECTRICAL_SELF_ROOT_BY_FAMILY = {
167
+ batteries: 'electrical.batteries',
168
+ solar: 'electrical.solar',
169
+ chargers: 'electrical.chargers',
170
+ inverters: 'electrical.inverters',
171
+ alternators: 'electrical.alternators',
172
+ ac: 'electrical.ac'
173
+ };
174
+ const ELECTRICAL_METRICS_BY_FAMILY = {
175
+ batteries: ['capacity.stateOfCharge', 'current'],
176
+ solar: ['current', 'panelPower'],
177
+ chargers: ['voltage', 'current'],
178
+ inverters: ['voltage', 'current'],
179
+ alternators: ['voltage', 'current'],
180
+ ac: [
181
+ 'line1.voltage',
182
+ 'line1.current',
183
+ 'line1.frequency',
184
+ 'line2.voltage',
185
+ 'line2.current',
186
+ 'line2.frequency',
187
+ 'line3.voltage',
188
+ 'line3.current',
189
+ 'line3.frequency'
190
+ ]
191
+ };
192
+ function resolveFamilyKeyFromTemplateSeries(series) {
193
+ if (!(0, history_series_service_1.isKipElectricalTemplateSeriesDefinition)(series)) {
194
+ return null;
195
+ }
196
+ if (series.familyKey) {
197
+ return series.familyKey;
198
+ }
199
+ switch (series.expansionMode) {
200
+ case 'bms-battery-tree':
201
+ return 'batteries';
202
+ case 'solar-tree':
203
+ return 'solar';
204
+ case 'charger-tree':
205
+ return 'chargers';
206
+ case 'inverter-tree':
207
+ return 'inverters';
208
+ case 'alternator-tree':
209
+ return 'alternators';
210
+ case 'ac-tree':
211
+ return 'ac';
212
+ default:
170
213
  return null;
171
- }
172
- return root;
173
- };
174
- const candidates = readCandidate(batteriesPath);
175
- if (!candidates) {
214
+ }
215
+ }
216
+ function normalizeAllowedIds(series) {
217
+ const rawAllowedIds = Array.isArray(series.allowedIds)
218
+ ? series.allowedIds
219
+ : null;
220
+ if (!Array.isArray(rawAllowedIds)) {
176
221
  return [];
177
222
  }
178
- return Object.keys(candidates)
179
- .filter(id => /^[a-z0-9_-]+$/i.test(id))
180
- .sort((left, right) => left.localeCompare(right));
223
+ return rawAllowedIds
224
+ .filter((id) => typeof id === 'string')
225
+ .map(id => id.trim())
226
+ .filter(id => id.length > 0);
227
+ }
228
+ function normalizeTrackedDevices(series) {
229
+ if (!Array.isArray(series.trackedDevices)) {
230
+ return [];
231
+ }
232
+ const trackedByKey = new Map();
233
+ series.trackedDevices.forEach(item => {
234
+ if (!item || typeof item !== 'object') {
235
+ return;
236
+ }
237
+ const id = typeof item.id === 'string' ? item.id.trim() : '';
238
+ const normalizedSource = typeof item.source === 'string' ? item.source.trim() : '';
239
+ const source = normalizedSource.length > 0 ? normalizedSource : 'default';
240
+ if (!id) {
241
+ return;
242
+ }
243
+ trackedByKey.set(`${id}||${source}`, { id, source });
244
+ });
245
+ return Array.from(trackedByKey.values()).sort((left, right) => {
246
+ const idCompare = left.id.localeCompare(right.id);
247
+ return idCompare !== 0 ? idCompare : left.source.localeCompare(right.source);
248
+ });
181
249
  }
182
- function resolveSolarIdsFromSelfPath() {
183
- const solarPath = server.getSelfPath('electrical.solar');
184
- const readCandidate = (node) => {
185
- if (!node || typeof node !== 'object' || Array.isArray(node)) {
250
+ function resolveElectricalIdsFromSelfPath(rootPath) {
251
+ const node = server.getSelfPath(rootPath);
252
+ const readCandidate = (entry) => {
253
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
186
254
  return null;
187
255
  }
188
- const root = node;
256
+ const root = entry;
189
257
  if (Object.prototype.hasOwnProperty.call(root, 'value')) {
190
258
  const value = root.value;
191
259
  if (value && typeof value === 'object' && !Array.isArray(value)) {
@@ -195,7 +263,7 @@ const start = (server) => {
195
263
  }
196
264
  return root;
197
265
  };
198
- const candidates = readCandidate(solarPath);
266
+ const candidates = readCandidate(node);
199
267
  if (!candidates) {
200
268
  return [];
201
269
  }
@@ -203,19 +271,13 @@ const start = (server) => {
203
271
  .filter(id => /^[a-z0-9_-]+$/i.test(id))
204
272
  .sort((left, right) => left.localeCompare(right));
205
273
  }
206
- function getExistingConcreteBmsSeries(templateSeries, existingSeries) {
274
+ function getExistingConcreteElectricalSeries(templateSeries, familyKey, existingSeries) {
275
+ const rootPrefix = `${ELECTRICAL_SELF_ROOT_BY_FAMILY[familyKey]}.`;
207
276
  return existingSeries
208
277
  .filter(series => series.ownerWidgetUuid === templateSeries.ownerWidgetUuid)
209
278
  .filter(history_series_service_1.isKipConcreteSeriesDefinition)
210
279
  .filter(series => series.seriesId !== templateSeries.seriesId)
211
- .map(series => ({ ...series }));
212
- }
213
- function getExistingConcreteSolarSeries(templateSeries, existingSeries) {
214
- return existingSeries
215
- .filter(series => series.ownerWidgetUuid === templateSeries.ownerWidgetUuid)
216
- .filter(history_series_service_1.isKipConcreteSeriesDefinition)
217
- .filter(series => series.seriesId !== templateSeries.seriesId)
218
- .filter(series => series.path.startsWith('electrical.solar.'))
280
+ .filter(series => series.path.startsWith(rootPrefix))
219
281
  .map(series => ({ ...series }));
220
282
  }
221
283
  function mergeSeriesDefinitions(series) {
@@ -226,91 +288,69 @@ const start = (server) => {
226
288
  return Array.from(mergedById.values());
227
289
  }
228
290
  function expandTemplateSeriesDefinitions(payload, existingSeries = []) {
229
- const bmsMetrics = ['capacity.stateOfCharge', 'current'];
230
- const solarMetrics = ['current', 'panelPower'];
231
291
  const expandedById = new Map();
232
- const discoveredBatteryIds = resolveBmsBatteryIdsFromSelfPath();
233
- const discoveredSolarIds = resolveSolarIdsFromSelfPath();
292
+ const discoveredIdsByFamily = new Map();
293
+ const ensureDiscoveredIds = (familyKey) => {
294
+ if (!discoveredIdsByFamily.has(familyKey)) {
295
+ const discovered = resolveElectricalIdsFromSelfPath(ELECTRICAL_SELF_ROOT_BY_FAMILY[familyKey]);
296
+ discoveredIdsByFamily.set(familyKey, discovered);
297
+ }
298
+ return discoveredIdsByFamily.get(familyKey) ?? [];
299
+ };
234
300
  payload.forEach(series => {
235
301
  if (!(0, history_series_service_1.isKipTemplateSeriesDefinition)(series)) {
236
302
  expandedById.set(series.seriesId, series);
237
303
  return;
238
304
  }
239
- if ((0, history_series_service_1.isKipBmsTemplateSeriesDefinition)(series)) {
240
- if (discoveredBatteryIds.length === 0) {
241
- getExistingConcreteBmsSeries(series, existingSeries).forEach(existing => {
242
- expandedById.set(existing.seriesId, existing);
243
- });
244
- return;
245
- }
246
- const allowedBatteryIds = Array.isArray(series.allowedBatteryIds)
247
- ? series.allowedBatteryIds
248
- .filter((id) => typeof id === 'string')
249
- .map(id => id.trim())
250
- .filter(id => id.length > 0)
251
- : [];
252
- const allowedSet = allowedBatteryIds.length > 0 ? new Set(allowedBatteryIds) : null;
253
- const batteryIds = discoveredBatteryIds.filter(id => !allowedSet || allowedSet.has(id));
254
- if (batteryIds.length === 0) {
255
- return;
256
- }
257
- const source = series.source ?? 'default';
258
- const sourceKey = slugify(source || 'default') || 'default';
259
- batteryIds.forEach(batteryId => {
260
- bmsMetrics.forEach(metric => {
261
- const path = `self.electrical.batteries.${batteryId}.${metric}`;
262
- const seriesId = `${series.ownerWidgetUuid}:bms:${batteryId}:${metric}:${sourceKey}`;
263
- expandedById.set(seriesId, {
264
- ...series,
265
- seriesId,
266
- datasetUuid: `${series.ownerWidgetUuid}:bms:${batteryId}:${metric}:${sourceKey}`,
267
- path,
268
- retentionDurationMs: Number.isFinite(series.retentionDurationMs) ? series.retentionDurationMs : 24 * 60 * 60 * 1000,
269
- expansionMode: null,
270
- allowedBatteryIds: null,
271
- allowedSolarIds: null
272
- });
273
- });
274
- });
305
+ const familyKey = resolveFamilyKeyFromTemplateSeries(series);
306
+ if (!familyKey) {
307
+ expandedById.set(series.seriesId, series);
275
308
  return;
276
309
  }
277
- if ((0, history_series_service_1.isKipSolarTemplateSeriesDefinition)(series)) {
278
- if (discoveredSolarIds.length === 0) {
279
- getExistingConcreteSolarSeries(series, existingSeries).forEach(existing => {
310
+ const seriesPrefix = ELECTRICAL_SERIES_PREFIX_BY_FAMILY[familyKey];
311
+ const metricList = ELECTRICAL_METRICS_BY_FAMILY[familyKey];
312
+ const selfRoot = `self.${ELECTRICAL_SELF_ROOT_BY_FAMILY[familyKey]}`;
313
+ const trackedDevices = normalizeTrackedDevices(series);
314
+ let concreteDevices = trackedDevices;
315
+ if (concreteDevices.length === 0) {
316
+ const discoveredIds = ensureDiscoveredIds(familyKey);
317
+ if (discoveredIds.length === 0) {
318
+ getExistingConcreteElectricalSeries(series, familyKey, existingSeries).forEach(existing => {
280
319
  expandedById.set(existing.seriesId, existing);
281
320
  });
282
321
  return;
283
322
  }
284
- const allowedSolarIds = Array.isArray(series.allowedSolarIds)
285
- ? series.allowedSolarIds
286
- .filter((id) => typeof id === 'string')
287
- .map(id => id.trim())
288
- .filter(id => id.length > 0)
289
- : [];
290
- const allowedSet = allowedSolarIds.length > 0 ? new Set(allowedSolarIds) : null;
291
- const chargerIds = discoveredSolarIds.filter(id => !allowedSet || allowedSet.has(id));
292
- if (chargerIds.length === 0) {
323
+ const allowedIds = normalizeAllowedIds(series);
324
+ const allowedSet = allowedIds.length > 0 ? new Set(allowedIds) : null;
325
+ const filteredIds = discoveredIds.filter(id => !allowedSet || allowedSet.has(id));
326
+ if (filteredIds.length === 0) {
293
327
  return;
294
328
  }
295
- const source = series.source ?? 'default';
296
- const sourceKey = slugify(source || 'default') || 'default';
297
- chargerIds.forEach(chargerId => {
298
- solarMetrics.forEach(metric => {
299
- const path = `self.electrical.solar.${chargerId}.${metric}`;
300
- const seriesId = `${series.ownerWidgetUuid}:solar:${chargerId}:${metric}:${sourceKey}`;
301
- expandedById.set(seriesId, {
302
- ...series,
303
- seriesId,
304
- datasetUuid: `${series.ownerWidgetUuid}:solar:${chargerId}:${metric}:${sourceKey}`,
305
- path,
306
- retentionDurationMs: Number.isFinite(series.retentionDurationMs) ? series.retentionDurationMs : 24 * 60 * 60 * 1000,
307
- expansionMode: null,
308
- allowedBatteryIds: null,
309
- allowedSolarIds: null
310
- });
329
+ const fallbackSource = series.source ?? 'default';
330
+ concreteDevices = filteredIds.map(deviceId => ({
331
+ id: deviceId,
332
+ source: fallbackSource
333
+ }));
334
+ }
335
+ concreteDevices.forEach(device => {
336
+ const sourceKey = slugify(device.source || 'default') || 'default';
337
+ metricList.forEach(metric => {
338
+ const path = `${selfRoot}.${device.id}.${metric}`;
339
+ const seriesId = `${series.ownerWidgetUuid}:${seriesPrefix}:${device.id}:${metric}:${sourceKey}`;
340
+ expandedById.set(seriesId, {
341
+ ...series,
342
+ seriesId,
343
+ datasetUuid: `${series.ownerWidgetUuid}:${seriesPrefix}:${device.id}:${metric}:${sourceKey}`,
344
+ path,
345
+ source: device.source,
346
+ retentionDurationMs: Number.isFinite(series.retentionDurationMs) ? series.retentionDurationMs : 24 * 60 * 60 * 1000,
347
+ expansionMode: null,
348
+ familyKey: null,
349
+ allowedIds: null,
350
+ trackedDevices: null
311
351
  });
312
352
  });
313
- }
353
+ });
314
354
  });
315
355
  return Array.from(expandedById.values());
316
356
  }
@@ -358,16 +398,15 @@ const start = (server) => {
358
398
  }
359
399
  }
360
400
  function getRouteError(error, fallbackMessage) {
361
- const message = String(error?.message || fallbackMessage);
401
+ const message = error?.message?.trim() || fallbackMessage;
362
402
  const normalized = message.toLowerCase();
363
403
  if (normalized.includes('invalid ')
364
404
  || normalized.includes('missing ')
365
- || normalized.includes('body must')
366
- || normalized.includes('required')
367
- || normalized.includes('expected an iso')) {
405
+ || normalized.includes('required')) {
368
406
  return { statusCode: 400, message };
369
407
  }
370
408
  if (normalized.includes('sqlite')
409
+ || normalized.includes('database')
371
410
  || normalized.includes('storage unavailable')
372
411
  || normalized.includes('not initialized')
373
412
  || isSqliteUnavailable()) {
@@ -1228,16 +1267,19 @@ const start = (server) => {
1228
1267
  const scopedPayload = payload.map(series => ({
1229
1268
  ...series
1230
1269
  }));
1231
- const isBatteryDiscoveryUnavailable = resolveBmsBatteryIdsFromSelfPath().length === 0;
1232
- const isSolarDiscoveryUnavailable = resolveSolarIdsFromSelfPath().length === 0;
1233
1270
  const preservedTemplateConcreteSeries = scopedPayload
1234
1271
  .filter(history_series_service_1.isKipTemplateSeriesDefinition)
1235
1272
  .flatMap(series => {
1236
- if ((0, history_series_service_1.isKipBmsTemplateSeriesDefinition)(series) && isBatteryDiscoveryUnavailable) {
1237
- return getExistingConcreteBmsSeries(series, currentSeries);
1273
+ const familyKey = resolveFamilyKeyFromTemplateSeries(series);
1274
+ if (!familyKey) {
1275
+ return [];
1276
+ }
1277
+ if (normalizeTrackedDevices(series).length > 0) {
1278
+ return [];
1238
1279
  }
1239
- if ((0, history_series_service_1.isKipSolarTemplateSeriesDefinition)(series) && isSolarDiscoveryUnavailable) {
1240
- return getExistingConcreteSolarSeries(series, currentSeries);
1280
+ const isDiscoveryUnavailable = resolveElectricalIdsFromSelfPath(ELECTRICAL_SELF_ROOT_BY_FAMILY[familyKey]).length === 0;
1281
+ if (isDiscoveryUnavailable) {
1282
+ return getExistingConcreteElectricalSeries(series, familyKey, currentSeries);
1241
1283
  }
1242
1284
  return [];
1243
1285
  });
@@ -1,18 +1,27 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.isKipTemplateSeriesDefinition = isKipTemplateSeriesDefinition;
4
+ exports.isKipElectricalTemplateSeriesDefinition = isKipElectricalTemplateSeriesDefinition;
4
5
  exports.isKipBmsTemplateSeriesDefinition = isKipBmsTemplateSeriesDefinition;
5
6
  exports.isKipSolarTemplateSeriesDefinition = isKipSolarTemplateSeriesDefinition;
6
7
  exports.isKipConcreteSeriesDefinition = isKipConcreteSeriesDefinition;
7
8
  exports.isKipSeriesEnabled = isKipSeriesEnabled;
8
9
  function isKipTemplateSeriesDefinition(series) {
9
- return series.expansionMode === 'bms-battery-tree' || series.expansionMode === 'solar-tree';
10
+ return series.expansionMode === 'bms-battery-tree'
11
+ || series.expansionMode === 'solar-tree'
12
+ || series.expansionMode === 'charger-tree'
13
+ || series.expansionMode === 'inverter-tree'
14
+ || series.expansionMode === 'alternator-tree'
15
+ || series.expansionMode === 'ac-tree';
16
+ }
17
+ function isKipElectricalTemplateSeriesDefinition(series) {
18
+ return isKipTemplateSeriesDefinition(series);
10
19
  }
11
20
  function isKipBmsTemplateSeriesDefinition(series) {
12
- return series.expansionMode === 'bms-battery-tree';
21
+ return series.expansionMode === 'bms-battery-tree' && (series.familyKey == null || series.familyKey === 'batteries');
13
22
  }
14
23
  function isKipSolarTemplateSeriesDefinition(series) {
15
- return series.expansionMode === 'solar-tree';
24
+ return series.expansionMode === 'solar-tree' && (series.familyKey == null || series.familyKey === 'solar');
16
25
  }
17
26
  function isKipConcreteSeriesDefinition(series) {
18
27
  return series.expansionMode == null;
@@ -217,21 +217,38 @@
217
217
  "nullable": true,
218
218
  "description": "Null for concrete series definitions."
219
219
  },
220
- "allowedBatteryIds": {
220
+ "familyKey": {
221
+ "type": "string",
222
+ "nullable": true,
223
+ "description": "Concrete series do not use family key and should leave this null."
224
+ },
225
+ "allowedIds": {
221
226
  "type": "array",
222
227
  "nullable": true,
223
228
  "items": {
224
229
  "type": "string"
225
230
  },
226
- "description": "Concrete series do not use battery filters and should leave this null."
231
+ "description": "Concrete series do not use template filters and should leave this null."
227
232
  },
228
- "allowedSolarIds": {
233
+ "trackedDevices": {
229
234
  "type": "array",
230
235
  "nullable": true,
231
236
  "items": {
232
- "type": "string"
237
+ "type": "object",
238
+ "properties": {
239
+ "id": {
240
+ "type": "string"
241
+ },
242
+ "source": {
243
+ "type": "string"
244
+ }
245
+ },
246
+ "required": [
247
+ "id",
248
+ "source"
249
+ ]
233
250
  },
234
- "description": "Concrete series do not use solar charger filters and should leave this null."
251
+ "description": "Concrete series do not use tracked device template filters and should leave this null."
235
252
  }
236
253
  }
237
254
  }
@@ -249,34 +266,68 @@
249
266
  "type": "string",
250
267
  "enum": [
251
268
  "widget-bms",
252
- "widget-solar-charger"
269
+ "widget-solar-charger",
270
+ "widget-charger",
271
+ "widget-inverter",
272
+ "widget-alternator",
273
+ "widget-ac"
253
274
  ]
254
275
  },
255
276
  "expansionMode": {
256
277
  "type": "string",
257
278
  "enum": [
258
279
  "bms-battery-tree",
259
- "solar-tree"
280
+ "solar-tree",
281
+ "charger-tree",
282
+ "inverter-tree",
283
+ "alternator-tree",
284
+ "ac-tree"
260
285
  ]
261
286
  },
262
- "allowedBatteryIds": {
287
+ "familyKey": {
288
+ "type": "string",
289
+ "enum": [
290
+ "batteries",
291
+ "solar",
292
+ "chargers",
293
+ "inverters",
294
+ "alternators",
295
+ "ac"
296
+ ]
297
+ },
298
+ "allowedIds": {
263
299
  "type": "array",
264
300
  "nullable": true,
265
301
  "items": {
266
302
  "type": "string"
267
- }
303
+ },
304
+ "description": "Template device IDs to include. Null or empty includes all discovered devices."
268
305
  },
269
- "allowedSolarIds": {
306
+ "trackedDevices": {
270
307
  "type": "array",
271
308
  "nullable": true,
272
309
  "items": {
273
- "type": "string"
274
- }
310
+ "type": "object",
311
+ "properties": {
312
+ "id": {
313
+ "type": "string"
314
+ },
315
+ "source": {
316
+ "type": "string"
317
+ }
318
+ },
319
+ "required": [
320
+ "id",
321
+ "source"
322
+ ]
323
+ },
324
+ "description": "Explicit tracked device id/source pairs to expand. When provided, template expansion skips self-tree discovery and emits one concrete series per pair and metric."
275
325
  }
276
326
  },
277
327
  "required": [
278
328
  "ownerWidgetSelector",
279
- "expansionMode"
329
+ "expansionMode",
330
+ "familyKey"
280
331
  ]
281
332
  }
282
333
  ]
@@ -709,7 +709,8 @@ class SqliteHistoryStorageService {
709
709
  });
710
710
  }
711
711
  catch (err) {
712
- this.logger.error(`[SERIES STORAGE] Prune failed: ${err?.message ?? err}`);
712
+ const message = err instanceof Error ? err.message : String(err);
713
+ this.logger.error(`[SERIES STORAGE] Prune failed: ${message}`);
713
714
  }
714
715
  }, SqliteHistoryStorageService.FOUR_HOURS_INTERVAL);
715
716
  this.pruneJob.unref?.();
@@ -739,7 +740,8 @@ class SqliteHistoryStorageService {
739
740
  });
740
741
  }
741
742
  catch (err) {
742
- this.logger.error(`[SERIES STORAGE] Stale series cleanup failed: ${err?.message ?? err}`);
743
+ const message = err instanceof Error ? err.message : String(err);
744
+ this.logger.error(`[SERIES STORAGE] Stale series cleanup failed: ${message}`);
743
745
  }
744
746
  }, SqliteHistoryStorageService.EIGHT_HOURS_INTERVAL);
745
747
  this.staleSeriesCleanupJob.unref?.();