iobroker.sun2000 2.4.2 → 2.4.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.
- package/README.md +9 -265
- package/admin/i18n/de/translations.json +27 -27
- package/admin/i18n/es/translations.json +27 -27
- package/admin/i18n/fr/translations.json +27 -27
- package/admin/i18n/it/translations.json +27 -27
- package/admin/i18n/nl/translations.json +27 -27
- package/admin/i18n/pl/translations.json +27 -27
- package/admin/i18n/pt/translations.json +27 -27
- package/admin/i18n/ru/translations.json +27 -27
- package/admin/i18n/uk/translations.json +27 -27
- package/admin/i18n/zh-cn/translations.json +27 -27
- package/io-package.json +27 -27
- package/lib/modbus/modbus_server.js +35 -8
- package/lib/register.js +3 -0
- package/lib/statistics.js +582 -218
- package/lib/tools.js +13 -9
- package/lib/types.js +1 -0
- package/main.js +80 -0
- package/package.json +2 -2
package/lib/statistics.js
CHANGED
|
@@ -25,6 +25,7 @@ class statistics {
|
|
|
25
25
|
this.stateCache = stateCache;
|
|
26
26
|
this.taskTimer = null;
|
|
27
27
|
this._path = 'statistics';
|
|
28
|
+
this._initialized = false;
|
|
28
29
|
this.testing = false; // set to true for testing purposes
|
|
29
30
|
|
|
30
31
|
this.stats = [
|
|
@@ -32,7 +33,7 @@ class statistics {
|
|
|
32
33
|
sourceId: 'collected.consumptionToday',
|
|
33
34
|
targetPath: 'consumption',
|
|
34
35
|
unit: 'kWh',
|
|
35
|
-
type: statisticsType.deltaReset,
|
|
36
|
+
type: statisticsType.deltaReset,
|
|
36
37
|
},
|
|
37
38
|
{
|
|
38
39
|
sourceId: 'collected.dailySolarYield',
|
|
@@ -41,23 +42,50 @@ class statistics {
|
|
|
41
42
|
type: statisticsType.deltaReset,
|
|
42
43
|
},
|
|
43
44
|
{ sourceId: 'collected.dailyInputYield', targetPath: 'inputYield', unit: 'kWh', type: statisticsType.deltaReset },
|
|
44
|
-
{
|
|
45
|
-
sourceId: 'collected.dailyExternalYield',
|
|
46
|
-
targetPath: 'externalYield',
|
|
47
|
-
unit: 'kWh',
|
|
48
|
-
type: statisticsType.deltaReset,
|
|
49
|
-
},
|
|
45
|
+
{ sourceId: 'collected.dailyExternalYield', targetPath: 'externalYield', unit: 'kWh', type: statisticsType.deltaReset },
|
|
50
46
|
{ sourceId: 'collected.dailyEnergyYield', targetPath: 'energyYield', unit: 'kWh', type: statisticsType.deltaReset },
|
|
51
47
|
{
|
|
52
48
|
sourceId: 'collected.SOC',
|
|
53
49
|
targetPath: 'SOC',
|
|
54
50
|
unit: '%',
|
|
55
|
-
type: statisticsType.level,
|
|
51
|
+
type: statisticsType.level,
|
|
56
52
|
},
|
|
57
53
|
{ sourceId: 'collected.currentDayChargeCapacity', targetPath: 'chargeCapacity', unit: 'kWh', type: statisticsType.deltaReset },
|
|
58
54
|
{ sourceId: 'collected.currentDayDischargeCapacity', targetPath: 'dischargeCapacity', unit: 'kWh', type: statisticsType.deltaReset },
|
|
59
55
|
{ sourceId: 'collected.gridExportToday', targetPath: 'gridExport', unit: 'kWh', type: statisticsType.deltaReset },
|
|
60
56
|
{ sourceId: 'collected.gridImportToday', targetPath: 'gridImport', unit: 'kWh', type: statisticsType.deltaReset },
|
|
57
|
+
// --- Computed stats ---
|
|
58
|
+
{
|
|
59
|
+
targetPath: 'selfSufficiency',
|
|
60
|
+
unit: '%',
|
|
61
|
+
type: statisticsType.computed,
|
|
62
|
+
// unten im FusionSolar Fenster
|
|
63
|
+
// selfSufficiency = (consumption - gridImport)/consumption * 100
|
|
64
|
+
// selfSufficiency = (1 - gridImport / consumption) * 100
|
|
65
|
+
// If consumption = 0 → 100% (no consumption, fully self-sufficient)
|
|
66
|
+
compute: entry => {
|
|
67
|
+
const consumption = (entry.consumption?.total != null ? entry.consumption?.total : entry.consumption?.value) ?? 0;
|
|
68
|
+
const gridImport = (entry.gridImport?.total != null ? entry.gridImport?.total : entry.gridImport?.value) ?? 0;
|
|
69
|
+
if (consumption <= 0) return 100;
|
|
70
|
+
return Math.round(Math.max(0, Math.min(100, (1 - gridImport / consumption) * 100)) * 10) / 10;
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
targetPath: 'selfConsumption',
|
|
75
|
+
unit: '%',
|
|
76
|
+
type: statisticsType.computed,
|
|
77
|
+
// oben im FusionSolar Fenster
|
|
78
|
+
// selfConsumption = (solarYield - gridExport) / solarYield * 100
|
|
79
|
+
// selfConsumption = (1 - gridExport / solarYield) * 100
|
|
80
|
+
// If solarYield = 0 → 0% (no generation, no self-consumption possible)
|
|
81
|
+
compute: entry => {
|
|
82
|
+
let solarYield = (entry.solarYield?.total != null ? entry.solarYield?.total : entry.solarYield?.value) ?? 0;
|
|
83
|
+
solarYield += (entry.externalYield?.total != null ? entry.externalYield?.total : entry.externalYield?.value) ?? 0;
|
|
84
|
+
const gridExport = (entry.gridExport?.total != null ? entry.gridExport?.total : entry.gridExport?.value) ?? 0;
|
|
85
|
+
if (solarYield <= 0) return 0;
|
|
86
|
+
return Math.round(Math.max(0, Math.min(100, (1 - gridExport / solarYield) * 100)) * 10) / 10;
|
|
87
|
+
},
|
|
88
|
+
},
|
|
61
89
|
];
|
|
62
90
|
|
|
63
91
|
this.postProcessHooks = [
|
|
@@ -104,8 +132,16 @@ class statistics {
|
|
|
104
132
|
desc: 'Annual consumption per year',
|
|
105
133
|
initVal: '[]',
|
|
106
134
|
},
|
|
107
|
-
//
|
|
108
|
-
|
|
135
|
+
// Today summary state
|
|
136
|
+
{
|
|
137
|
+
id: 'statistics.jsonToday',
|
|
138
|
+
name: 'Today summary',
|
|
139
|
+
type: 'string',
|
|
140
|
+
role: 'json',
|
|
141
|
+
desc: "Live summary of today's energy values",
|
|
142
|
+
initVal: '{}',
|
|
143
|
+
},
|
|
144
|
+
// Templates: one per chart type
|
|
109
145
|
{
|
|
110
146
|
id: 'statistics.flexCharts.template.hourly',
|
|
111
147
|
name: 'Flexcharts template hourly',
|
|
@@ -151,7 +187,7 @@ class statistics {
|
|
|
151
187
|
write: true,
|
|
152
188
|
initVal: '{}',
|
|
153
189
|
},
|
|
154
|
-
//
|
|
190
|
+
// Output: one per chart type
|
|
155
191
|
{
|
|
156
192
|
id: 'statistics.flexCharts.jsonOutput.hourly',
|
|
157
193
|
name: 'Flexcharts output hourly',
|
|
@@ -228,12 +264,10 @@ class statistics {
|
|
|
228
264
|
* Generic function to calculate consumption statistics for different time periods.
|
|
229
265
|
*
|
|
230
266
|
* @param {string} stateId - The state ID for storing the JSON
|
|
231
|
-
* @param {string} consumptionKey - The state key for consumption value
|
|
232
267
|
* @param {Date} periodStart - The start of the current period
|
|
233
|
-
* @param {
|
|
234
|
-
* @returns {
|
|
268
|
+
* @param {Date} periodEnde - The end of the current period
|
|
269
|
+
* @returns {boolean} true if a new entry was appended, false otherwise.
|
|
235
270
|
*/
|
|
236
|
-
|
|
237
271
|
_calculateGeneric(stateId, periodStart, periodEnde) {
|
|
238
272
|
const toStr = this._localIsoWithOffset(periodEnde);
|
|
239
273
|
let jsonStr = this.stateCache.get(stateId)?.value ?? '[]';
|
|
@@ -248,7 +282,6 @@ class statistics {
|
|
|
248
282
|
let last = {};
|
|
249
283
|
if (arr.length > 0) {
|
|
250
284
|
last = arr[arr.length - 1];
|
|
251
|
-
// avoid duplicates
|
|
252
285
|
if (last.to === toStr) return false;
|
|
253
286
|
const lastToDate = new Date(last.to);
|
|
254
287
|
const toDate = new Date(toStr);
|
|
@@ -262,7 +295,10 @@ class statistics {
|
|
|
262
295
|
to: toStr,
|
|
263
296
|
};
|
|
264
297
|
|
|
298
|
+
// First pass: deltaReset, delta, level stats
|
|
265
299
|
for (const stat of this.stats) {
|
|
300
|
+
if (stat.type === statisticsType.computed) continue;
|
|
301
|
+
|
|
266
302
|
const source = this.stateCache.get(stat.sourceId)?.value;
|
|
267
303
|
if (source === null || source === undefined) {
|
|
268
304
|
this.adapter.logger.warn(`Source state ${stat.sourceId} not found statistic hook`);
|
|
@@ -272,42 +308,54 @@ class statistics {
|
|
|
272
308
|
if (stat.type === statisticsType.delta || stat.type === statisticsType.deltaReset) {
|
|
273
309
|
const lastTotal = Number(last[stat.targetPath]?.['total'] ?? 0);
|
|
274
310
|
if (stat.type === statisticsType.deltaReset) {
|
|
275
|
-
//if (value >= lastTotal * 0.5) {
|
|
276
311
|
if (fromDate.getTime() !== periodStart.getTime()) {
|
|
277
|
-
// Delta-Berechnung
|
|
278
312
|
value -= lastTotal;
|
|
279
313
|
}
|
|
280
314
|
} else {
|
|
281
|
-
// Ein lastTotal-Wert vorhanden –> normale Delta-Berechnung
|
|
282
315
|
if (last[stat.targetPath]?.['total'] === undefined) {
|
|
283
|
-
// Kein lastTotal-Wert vorhanden –> wahrscheinlich erster Eintrag, Delta-Berechnung nicht möglich
|
|
284
316
|
this.adapter.logger.debug(`No total value found for ${stat.targetPath} in last entry, setting delta to 0`);
|
|
285
317
|
value = 0;
|
|
286
318
|
} else {
|
|
287
|
-
// Delta-Berechnung
|
|
288
319
|
value -= lastTotal;
|
|
289
320
|
}
|
|
290
321
|
}
|
|
291
322
|
}
|
|
292
323
|
value = Math.round((Number(value) + Number.EPSILON) * 1000) / 1000;
|
|
293
|
-
entry[stat.targetPath] = {
|
|
294
|
-
value: Number(value.toFixed(3)),
|
|
295
|
-
};
|
|
296
|
-
|
|
324
|
+
entry[stat.targetPath] = { value: Number(value.toFixed(3)) };
|
|
297
325
|
if (stat.type === statisticsType.delta || stat.type === statisticsType.deltaReset) {
|
|
298
326
|
entry[stat.targetPath].total = Number(source.toFixed(3));
|
|
299
327
|
}
|
|
300
|
-
entry[stat.targetPath].unit = stat.unit || 'kWh';
|
|
328
|
+
entry[stat.targetPath].unit = stat.unit || 'kWh';
|
|
301
329
|
}
|
|
302
|
-
arr.push(entry);
|
|
303
330
|
|
|
304
|
-
|
|
331
|
+
// Second pass: computed stats (all other values are now in entry)
|
|
332
|
+
for (const stat of this.stats) {
|
|
333
|
+
if (stat.type !== statisticsType.computed) continue;
|
|
334
|
+
try {
|
|
335
|
+
const value = stat.compute(entry);
|
|
336
|
+
entry[stat.targetPath] = {
|
|
337
|
+
value: Number(Number(value).toFixed(3)),
|
|
338
|
+
unit: stat.unit || '%',
|
|
339
|
+
};
|
|
340
|
+
} catch (e) {
|
|
341
|
+
this.adapter.logger.warn(`statistics: error computing ${stat.targetPath}: ${e.message}`);
|
|
342
|
+
entry[stat.targetPath] = { value: 0, unit: stat.unit || '%' };
|
|
343
|
+
}
|
|
344
|
+
}
|
|
305
345
|
|
|
346
|
+
arr.push(entry);
|
|
347
|
+
arr.sort((a, b) => Date.parse(a.to) - Date.parse(b.to));
|
|
306
348
|
this.stateCache.set(stateId, JSON.stringify(arr), { type: 'string' });
|
|
307
349
|
this.adapter.logger.debug(`Appended ${stateId} statistic ${toStr}`);
|
|
308
350
|
return arr.length > 0;
|
|
309
351
|
}
|
|
310
352
|
|
|
353
|
+
/**
|
|
354
|
+
* Removes entries older than periodStart from the given state.
|
|
355
|
+
*
|
|
356
|
+
* @param {string} stateId
|
|
357
|
+
* @param {Date} periodStart
|
|
358
|
+
*/
|
|
311
359
|
_clearGeneric(stateId, periodStart) {
|
|
312
360
|
let jsonStr = this.stateCache.get(stateId)?.value ?? '[]';
|
|
313
361
|
let arr = [];
|
|
@@ -317,29 +365,163 @@ class statistics {
|
|
|
317
365
|
} catch {
|
|
318
366
|
arr = [];
|
|
319
367
|
}
|
|
320
|
-
|
|
321
|
-
// Keep only entries within the window
|
|
322
368
|
arr = arr.filter(item => {
|
|
323
369
|
const ts = Date.parse(item.from);
|
|
324
370
|
return !Number.isNaN(ts) && ts >= periodStart.getTime();
|
|
325
371
|
});
|
|
326
|
-
|
|
327
372
|
this.stateCache.set(stateId, JSON.stringify(arr), { type: 'string' });
|
|
328
373
|
}
|
|
329
374
|
|
|
375
|
+
// eslint-disable-next-line jsdoc/require-returns-check
|
|
330
376
|
/**
|
|
331
|
-
* Calculates and aggregates
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
377
|
+
* Calculates and aggregates statistics for a given time window.
|
|
378
|
+
* If the window end is in the future (current period), the last entry
|
|
379
|
+
* is updated in place on every call — effectively a live running total.
|
|
380
|
+
* If the window end is in the past (completed period), a new entry is
|
|
381
|
+
* appended only once and never touched again.
|
|
335
382
|
*
|
|
336
|
-
* @param {string} sourceStateId
|
|
337
|
-
* @param {string} targetStateId
|
|
338
|
-
* @param {Function} getWindow -
|
|
339
|
-
*
|
|
340
|
-
* @
|
|
383
|
+
* @param {string} sourceStateId
|
|
384
|
+
* @param {string} targetStateId
|
|
385
|
+
* @param {Function} getWindow - returns { from: Date, to: Date }
|
|
386
|
+
* where to is the natural end of the period (e.g. tomorrow 0:00)
|
|
387
|
+
* @param {string} periodType
|
|
388
|
+
* @returns {boolean} true if a new entry was appended, false otherwise
|
|
341
389
|
*/
|
|
342
390
|
_calculateAggregation(sourceStateId, targetStateId, getWindow, periodType) {
|
|
391
|
+
try {
|
|
392
|
+
const now = new Date();
|
|
393
|
+
const window = getWindow(now);
|
|
394
|
+
const fromDate = window.from;
|
|
395
|
+
const toDate = window.to;
|
|
396
|
+
|
|
397
|
+
// Effective end: either the natural period end (past) or now (running)
|
|
398
|
+
const isRunning = now < toDate;
|
|
399
|
+
const effectiveTo = isRunning ? now : toDate;
|
|
400
|
+
const toStr = this._localIsoWithOffset(effectiveTo);
|
|
401
|
+
|
|
402
|
+
let jsonTarget = this.stateCache.get(targetStateId)?.value ?? '[]';
|
|
403
|
+
let targetArray = [];
|
|
404
|
+
try {
|
|
405
|
+
targetArray = JSON.parse(jsonTarget);
|
|
406
|
+
if (!Array.isArray(targetArray)) targetArray = [];
|
|
407
|
+
} catch {
|
|
408
|
+
targetArray = [];
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Find existing entry for this window (matched by from date)
|
|
412
|
+
const fromStr = this._localIsoWithOffset(fromDate);
|
|
413
|
+
const existingIdx = targetArray.findLastIndex(e => (isRunning ? e._live === true : e.from === fromStr));
|
|
414
|
+
//const existing = existingIdx >= 0 ? targetArray[existingIdx] : null;
|
|
415
|
+
|
|
416
|
+
// For completed periods: skip if already finalized (to matches natural end)
|
|
417
|
+
if (!isRunning && existingIdx >= 0) {
|
|
418
|
+
//const naturalToStr = this._localIsoWithOffset(toDate);
|
|
419
|
+
if (targetArray[existingIdx] === this._localIsoWithOffset(toDate)) {
|
|
420
|
+
this.adapter.logger.debug(`statistics.js: ${periodType} entry already finalized, skipping`);
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const target = {
|
|
426
|
+
from: fromStr,
|
|
427
|
+
to: toStr,
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
if (isRunning) {
|
|
431
|
+
target._live = true;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Load source entries within the window
|
|
435
|
+
let jsonStr = this.stateCache.get(sourceStateId)?.value ?? '[]';
|
|
436
|
+
let sourceEntries = [];
|
|
437
|
+
try {
|
|
438
|
+
sourceEntries = JSON.parse(jsonStr);
|
|
439
|
+
if (!Array.isArray(sourceEntries)) sourceEntries = [];
|
|
440
|
+
} catch {
|
|
441
|
+
sourceEntries = [];
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
sourceEntries = sourceEntries.filter(item => {
|
|
445
|
+
const ts = Date.parse(item.from);
|
|
446
|
+
//return !Number.isNaN(ts) && ts >= fromDate.getTime() && ts < effectiveTo.getTime(); //toDate ?
|
|
447
|
+
return !Number.isNaN(ts) && ts >= fromDate.getTime() && ts < toDate.getTime();
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
if (sourceEntries.length === 0) {
|
|
451
|
+
/*
|
|
452
|
+
this.adapter.logger.debug(
|
|
453
|
+
`statistics.js: No source entries for ${periodType} between ${fromDate.toISOString()} and ${effectiveTo.toISOString()}, skipping`,
|
|
454
|
+
);
|
|
455
|
+
*/
|
|
456
|
+
this.adapter.logger.debug(
|
|
457
|
+
`statistics.js: No source entries for ${periodType} between ${fromDate.toISOString()} and ${toDate.toISOString()}, skipping`,
|
|
458
|
+
);
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
this.adapter.logger.debug(
|
|
463
|
+
`statistics.js: ${sourceEntries.length} source entries for ${periodType} between ${fromDate.toISOString()} and ${effectiveTo.toISOString()} (running=${isRunning})`,
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
// First pass: sum delta/deltaReset stats
|
|
467
|
+
for (const stat of this.stats) {
|
|
468
|
+
if (stat.type === statisticsType.level) continue;
|
|
469
|
+
if (stat.type === statisticsType.computed) continue;
|
|
470
|
+
|
|
471
|
+
let sum = 0;
|
|
472
|
+
try {
|
|
473
|
+
sourceEntries.forEach(entry => {
|
|
474
|
+
sum += Number(entry[stat.targetPath]?.['value'] ?? 0);
|
|
475
|
+
});
|
|
476
|
+
} catch (e) {
|
|
477
|
+
this.adapter.logger.warn(`statistics.js: Error during ${periodType} aggregation: ${e.message}`);
|
|
478
|
+
}
|
|
479
|
+
sum = Math.round((Number(sum) + Number.EPSILON) * 1000) / 1000;
|
|
480
|
+
target[stat.targetPath] = { value: Number(sum.toFixed(3)), unit: stat.unit || 'kWh' };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Second pass: computed stats
|
|
484
|
+
for (const stat of this.stats) {
|
|
485
|
+
if (stat.type !== statisticsType.computed) continue;
|
|
486
|
+
try {
|
|
487
|
+
const value = stat.compute(target);
|
|
488
|
+
target[stat.targetPath] = {
|
|
489
|
+
value: Number(Number(value).toFixed(3)),
|
|
490
|
+
unit: stat.unit || '%',
|
|
491
|
+
};
|
|
492
|
+
} catch (e) {
|
|
493
|
+
this.adapter.logger.warn(`statistics: error computing ${stat.targetPath}: ${e.message}`);
|
|
494
|
+
target[stat.targetPath] = { value: 0, unit: stat.unit || '%' };
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Update in place or append
|
|
499
|
+
if (existingIdx >= 0) {
|
|
500
|
+
targetArray[existingIdx] = target;
|
|
501
|
+
this.adapter.logger.debug(`statistics.js: Updated ${periodType} entry (running=${isRunning})`);
|
|
502
|
+
} else {
|
|
503
|
+
targetArray.push(target);
|
|
504
|
+
this.adapter.logger.debug(`statistics.js: Appended ${periodType} entry (running=${isRunning})`);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
targetArray.sort((a, b) => Date.parse(a.to) - Date.parse(b.to));
|
|
508
|
+
this.stateCache.set(targetStateId, JSON.stringify(targetArray), { type: 'string' });
|
|
509
|
+
return targetArray.length > 0;
|
|
510
|
+
} catch (err) {
|
|
511
|
+
this.adapter.logger.warn(`Error during ${periodType} aggregation: ${err.message}`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// eslint-disable-next-line jsdoc/require-returns-check
|
|
515
|
+
/**
|
|
516
|
+
* Calculates and aggregates consumption statistics based on the given parameters.
|
|
517
|
+
*
|
|
518
|
+
* @param {string} sourceStateId
|
|
519
|
+
* @param {string} targetStateId
|
|
520
|
+
* @param {Function} getWindow
|
|
521
|
+
* @param {string} periodType
|
|
522
|
+
* @returns {boolean} true if a new entry was appended, false otherwise.
|
|
523
|
+
*/
|
|
524
|
+
_calculateAggregation_old(sourceStateId, targetStateId, getWindow, periodType) {
|
|
343
525
|
try {
|
|
344
526
|
const now = new Date();
|
|
345
527
|
const window = getWindow(now);
|
|
@@ -350,8 +532,8 @@ class statistics {
|
|
|
350
532
|
return false;
|
|
351
533
|
}
|
|
352
534
|
const toStr = this._localIsoWithOffset(toDate);
|
|
535
|
+
//const fromStr = this._localIsoWithOffset(fromDate);
|
|
353
536
|
|
|
354
|
-
// Load target array
|
|
355
537
|
let jsonTarget = this.stateCache.get(targetStateId)?.value ?? '[]';
|
|
356
538
|
let targetArray = [];
|
|
357
539
|
try {
|
|
@@ -361,8 +543,8 @@ class statistics {
|
|
|
361
543
|
targetArray = [];
|
|
362
544
|
}
|
|
363
545
|
|
|
364
|
-
// Avoid duplicates
|
|
365
546
|
const last = targetArray.length > 0 ? targetArray[targetArray.length - 1] : {};
|
|
547
|
+
|
|
366
548
|
if (last.to === toStr) return false;
|
|
367
549
|
|
|
368
550
|
const target = {
|
|
@@ -370,7 +552,6 @@ class statistics {
|
|
|
370
552
|
to: toStr,
|
|
371
553
|
};
|
|
372
554
|
|
|
373
|
-
// Load source entries
|
|
374
555
|
let jsonStr = this.stateCache.get(sourceStateId)?.value ?? '[]';
|
|
375
556
|
let sourceEntries = [];
|
|
376
557
|
try {
|
|
@@ -380,27 +561,22 @@ class statistics {
|
|
|
380
561
|
sourceEntries = [];
|
|
381
562
|
}
|
|
382
563
|
|
|
383
|
-
// Keep only entries within the window
|
|
384
564
|
sourceEntries = sourceEntries.filter(item => {
|
|
385
565
|
const ts = Date.parse(item.from);
|
|
386
566
|
return !Number.isNaN(ts) && ts >= fromDate.getTime() && ts < toDate.getTime();
|
|
387
567
|
});
|
|
388
|
-
|
|
568
|
+
|
|
389
569
|
if (sourceEntries.length > 0) {
|
|
390
570
|
this.adapter.logger.debug(
|
|
391
571
|
`statistics.js: Found ${sourceEntries.length} source entries for ${periodType} aggregation between ${fromDate.toISOString()} and ${toDate.toISOString()}`,
|
|
392
572
|
);
|
|
393
573
|
|
|
574
|
+
// First pass: sum delta/deltaReset stats
|
|
394
575
|
for (const stat of this.stats) {
|
|
395
|
-
|
|
396
|
-
if (stat.type === statisticsType.
|
|
576
|
+
if (stat.type === statisticsType.level) continue;
|
|
577
|
+
if (stat.type === statisticsType.computed) continue;
|
|
397
578
|
|
|
398
579
|
let sum = 0;
|
|
399
|
-
/*
|
|
400
|
-
if (stat.type === statisticsType.average) {
|
|
401
|
-
stat.sum = sourceEntries.length > 0 ? sourceEntries[sourceEntries.length - 1]?.[stat.targetPath]?.['total'] : 0;
|
|
402
|
-
} else {
|
|
403
|
-
*/
|
|
404
580
|
try {
|
|
405
581
|
sourceEntries.forEach(entry => {
|
|
406
582
|
sum += Number(entry[stat.targetPath]?.['value'] ?? 0);
|
|
@@ -408,13 +584,23 @@ class statistics {
|
|
|
408
584
|
} catch (e) {
|
|
409
585
|
this.adapter.logger.warn(`statistics.js: Error during ${periodType} statistic aggregation: ${e.message}`);
|
|
410
586
|
}
|
|
411
|
-
|
|
412
587
|
sum = Math.round((Number(sum) + Number.EPSILON) * 1000) / 1000;
|
|
588
|
+
target[stat.targetPath] = { value: Number(sum.toFixed(3)), unit: stat.unit || 'kWh' };
|
|
589
|
+
}
|
|
413
590
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
591
|
+
// Second pass: compute derived stats from aggregated values
|
|
592
|
+
for (const stat of this.stats) {
|
|
593
|
+
if (stat.type !== statisticsType.computed) continue;
|
|
594
|
+
try {
|
|
595
|
+
const value = stat.compute(target);
|
|
596
|
+
target[stat.targetPath] = {
|
|
597
|
+
value: Number(Number(value).toFixed(3)),
|
|
598
|
+
unit: stat.unit || '%',
|
|
599
|
+
};
|
|
600
|
+
} catch (e) {
|
|
601
|
+
this.adapter.logger.warn(`statistics: error computing aggregated ${stat.targetPath}: ${e.message}`);
|
|
602
|
+
target[stat.targetPath] = { value: 0, unit: stat.unit || '%' };
|
|
603
|
+
}
|
|
418
604
|
}
|
|
419
605
|
|
|
420
606
|
targetArray.push(target);
|
|
@@ -422,89 +608,165 @@ class statistics {
|
|
|
422
608
|
|
|
423
609
|
targetArray.sort((a, b) => Date.parse(a.to) - Date.parse(b.to));
|
|
424
610
|
this.stateCache.set(targetStateId, JSON.stringify(targetArray), { type: 'string' });
|
|
425
|
-
this.adapter.logger.debug(`Appended ${periodType} statistic ${toStr}
|
|
611
|
+
this.adapter.logger.debug(`Appended ${periodType} statistic ${toStr}`);
|
|
426
612
|
return targetArray.length > 0;
|
|
427
613
|
} catch (err) {
|
|
428
614
|
this.adapter.logger.warn(`Error during ${periodType} aggregation: ${err.message}`);
|
|
429
615
|
}
|
|
430
616
|
}
|
|
431
617
|
|
|
618
|
+
/**
|
|
619
|
+
* Updates the statistics.jsonToday state with the current live day values.
|
|
620
|
+
* Reads directly from the stateCache (collected.*) and computes derived values.
|
|
621
|
+
*/
|
|
622
|
+
updateJsonToday() {
|
|
623
|
+
if (!this._initialized) {
|
|
624
|
+
this.adapter.logger.debug('statistics: updateJsonToday called before initialization');
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
try {
|
|
629
|
+
const now = new Date();
|
|
630
|
+
const today = {};
|
|
631
|
+
|
|
632
|
+
// Read all non-computed stats directly from stateCache
|
|
633
|
+
for (const stat of this.stats) {
|
|
634
|
+
if (stat.type === statisticsType.computed) continue;
|
|
635
|
+
if (stat.type === statisticsType.level) {
|
|
636
|
+
const val = this.stateCache.get(stat.sourceId)?.value;
|
|
637
|
+
today[stat.targetPath] = {
|
|
638
|
+
value: val != null ? Number(Number(val).toFixed(3)) : null,
|
|
639
|
+
unit: stat.unit,
|
|
640
|
+
};
|
|
641
|
+
} else {
|
|
642
|
+
// deltaReset: value is the current day total from the source state
|
|
643
|
+
const val = this.stateCache.get(stat.sourceId)?.value;
|
|
644
|
+
today[stat.targetPath] = {
|
|
645
|
+
value: val != null ? Number(Number(val).toFixed(3)) : null,
|
|
646
|
+
unit: stat.unit,
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Compute derived stats
|
|
652
|
+
for (const stat of this.stats) {
|
|
653
|
+
if (stat.type !== statisticsType.computed) continue;
|
|
654
|
+
try {
|
|
655
|
+
// Build a proxy entry using the raw today values
|
|
656
|
+
const proxyEntry = {};
|
|
657
|
+
for (const key of Object.keys(today)) {
|
|
658
|
+
proxyEntry[key] = today[key];
|
|
659
|
+
}
|
|
660
|
+
const value = stat.compute(proxyEntry);
|
|
661
|
+
today[stat.targetPath] = {
|
|
662
|
+
value: Number(Number(value).toFixed(3)),
|
|
663
|
+
unit: stat.unit || '%',
|
|
664
|
+
};
|
|
665
|
+
} catch (e) {
|
|
666
|
+
this.adapter.logger.warn(`statistics: error computing today.${stat.targetPath}: ${e.message}`);
|
|
667
|
+
today[stat.targetPath] = { value: 0, unit: stat.unit || '%' };
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
today.updatedAt = this._localIsoWithOffset(now);
|
|
672
|
+
|
|
673
|
+
const todayStr = JSON.stringify(today);
|
|
674
|
+
this.stateCache.set('statistics.jsonToday', todayStr, { type: 'string' });
|
|
675
|
+
this.adapter.logger.debug('statistics: jsonToday state updated');
|
|
676
|
+
} catch (err) {
|
|
677
|
+
this.adapter.logger.warn(`statistics: error updating today state: ${err.message}`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
432
681
|
/**
|
|
433
682
|
* Calculates and updates hourly consumption statistics.
|
|
434
|
-
*
|
|
435
|
-
* This function calculates the hourly consumption statistics based on the current day's data.
|
|
436
|
-
* It retrieves the consumption data and updates the hourly consumption JSON accordingly.
|
|
437
|
-
*
|
|
438
|
-
* @returns {void}
|
|
439
683
|
*/
|
|
440
684
|
_calculateHourly() {
|
|
441
685
|
const now = new Date();
|
|
442
686
|
if (this.testing) {
|
|
443
687
|
const state = this.adapter.getState('statistics.jsonHourly');
|
|
444
688
|
this.stateCache.set('statistics.jsonHourly', state?.val ?? '[]', { type: 'string', stored: true });
|
|
445
|
-
now.setDate(now.getDate() + 1);
|
|
446
|
-
now.setHours(1, 0, 0, 1);
|
|
689
|
+
now.setDate(now.getDate() + 1);
|
|
690
|
+
now.setHours(1, 0, 0, 1);
|
|
447
691
|
}
|
|
448
692
|
const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0);
|
|
449
693
|
const lastHour = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), 0, 0, 0);
|
|
450
|
-
this.adapter.log.debug(
|
|
694
|
+
this.adapter.log.debug(`### Hourly execution triggered with lastHour: ${lastHour.toLocaleTimeString()} ###`);
|
|
451
695
|
if (this._calculateGeneric('statistics.jsonHourly', startOfDay, lastHour)) {
|
|
452
696
|
this._buildFlexchart('hourly');
|
|
453
697
|
}
|
|
454
698
|
}
|
|
455
699
|
|
|
456
|
-
/**
|
|
457
|
-
* Calculates and updates daily consumption statistics from hourly data.
|
|
458
|
-
*
|
|
459
|
-
* @returns {void}
|
|
460
|
-
*/
|
|
461
700
|
_calculateDaily() {
|
|
462
701
|
this.adapter.log.debug('### Daily execution triggered ###');
|
|
702
|
+
|
|
703
|
+
// Abgeschlossener Vortag — normaler Eintrag
|
|
463
704
|
this._calculateAggregation(
|
|
464
705
|
'statistics.jsonHourly',
|
|
465
706
|
'statistics.jsonDaily',
|
|
466
707
|
now => {
|
|
467
|
-
// aggregation window: previous day (day that just ended)
|
|
468
708
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0);
|
|
469
709
|
const yesterday = new Date(today);
|
|
470
710
|
yesterday.setDate(today.getDate() - 1);
|
|
471
711
|
return { from: yesterday, to: today };
|
|
472
712
|
},
|
|
473
713
|
'daily',
|
|
474
|
-
)
|
|
714
|
+
);
|
|
715
|
+
// Laufender heutiger Tag — live entry aus jsonHourly aggregieren
|
|
716
|
+
this._calculateAggregation(
|
|
717
|
+
'statistics.jsonHourly',
|
|
718
|
+
'statistics.jsonDaily',
|
|
719
|
+
now => {
|
|
720
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0);
|
|
721
|
+
const tomorrow = new Date(today);
|
|
722
|
+
tomorrow.setDate(today.getDate() + 1);
|
|
723
|
+
return { from: today, to: tomorrow };
|
|
724
|
+
},
|
|
725
|
+
'daily-live',
|
|
726
|
+
);
|
|
727
|
+
this._buildFlexchart('daily');
|
|
475
728
|
}
|
|
476
729
|
|
|
477
730
|
/**
|
|
478
731
|
* Calculates and updates weekly consumption statistics from daily data.
|
|
479
|
-
*
|
|
480
|
-
* @returns {void}
|
|
481
732
|
*/
|
|
482
733
|
_calculateWeekly() {
|
|
483
734
|
this.adapter.log.debug('### Weekly execution triggered ###');
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
)
|
|
500
|
-
|
|
501
|
-
|
|
735
|
+
this._calculateAggregation(
|
|
736
|
+
'statistics.jsonDaily',
|
|
737
|
+
'statistics.jsonWeekly',
|
|
738
|
+
now => {
|
|
739
|
+
// Previous week (completed)
|
|
740
|
+
const monday = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0);
|
|
741
|
+
monday.setDate(now.getDate() - (now.getDay() || 7) + 1);
|
|
742
|
+
const prevMonday = new Date(monday);
|
|
743
|
+
prevMonday.setDate(monday.getDate() - 7);
|
|
744
|
+
const nextMonday = new Date(monday);
|
|
745
|
+
nextMonday.setDate(monday.getDate() + 7);
|
|
746
|
+
// Window: previous Monday → next Monday (covers both last and current week)
|
|
747
|
+
return { from: prevMonday, to: nextMonday };
|
|
748
|
+
},
|
|
749
|
+
'weekly',
|
|
750
|
+
);
|
|
751
|
+
|
|
752
|
+
// Laufende Woche — live entry
|
|
753
|
+
this._calculateAggregation(
|
|
754
|
+
'statistics.jsonDaily',
|
|
755
|
+
'statistics.jsonWeekly',
|
|
756
|
+
now => {
|
|
757
|
+
const monday = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0);
|
|
758
|
+
monday.setDate(now.getDate() - (now.getDay() || 7) + 1);
|
|
759
|
+
const nextMonday = new Date(monday);
|
|
760
|
+
nextMonday.setDate(monday.getDate() + 7);
|
|
761
|
+
return { from: monday, to: nextMonday };
|
|
762
|
+
},
|
|
763
|
+
'weekly-live',
|
|
764
|
+
);
|
|
765
|
+
this._buildFlexchart('weekly');
|
|
502
766
|
}
|
|
503
767
|
|
|
504
768
|
/**
|
|
505
769
|
* Calculates and updates monthly consumption statistics from daily data.
|
|
506
|
-
*
|
|
507
|
-
* @returns {void}
|
|
508
770
|
*/
|
|
509
771
|
_calculateMonthly() {
|
|
510
772
|
this.adapter.log.debug('### Monthly execution triggered ###');
|
|
@@ -512,19 +774,28 @@ class statistics {
|
|
|
512
774
|
'statistics.jsonDaily',
|
|
513
775
|
'statistics.jsonMonthly',
|
|
514
776
|
now => {
|
|
515
|
-
// aggregation windowStart: previous month (month that just ended)
|
|
516
777
|
const thisMonth = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0, 0);
|
|
517
778
|
const prevMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1, 0, 0, 0, 0);
|
|
518
779
|
return { from: prevMonth, to: thisMonth };
|
|
519
780
|
},
|
|
520
781
|
'monthly',
|
|
521
|
-
)
|
|
782
|
+
);
|
|
783
|
+
// Laufender Monat — live entry
|
|
784
|
+
this._calculateAggregation(
|
|
785
|
+
'statistics.jsonDaily',
|
|
786
|
+
'statistics.jsonMonthly',
|
|
787
|
+
now => {
|
|
788
|
+
const thisMonth = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0, 0);
|
|
789
|
+
const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1, 0, 0, 0, 0);
|
|
790
|
+
return { from: thisMonth, to: nextMonth };
|
|
791
|
+
},
|
|
792
|
+
'monthly-live',
|
|
793
|
+
);
|
|
794
|
+
this._buildFlexchart('monthly');
|
|
522
795
|
}
|
|
523
796
|
|
|
524
797
|
/**
|
|
525
798
|
* Calculates and updates annual consumption statistics from daily data.
|
|
526
|
-
*
|
|
527
|
-
* @returns {void}
|
|
528
799
|
*/
|
|
529
800
|
_calculateAnnual() {
|
|
530
801
|
this.adapter.log.debug('### Annual execution triggered ###');
|
|
@@ -532,47 +803,57 @@ class statistics {
|
|
|
532
803
|
'statistics.jsonDaily',
|
|
533
804
|
'statistics.jsonAnnual',
|
|
534
805
|
now => {
|
|
535
|
-
// aggregation window: previous year (year that just ended)
|
|
536
806
|
const thisYear = new Date(now.getFullYear(), 0, 1, 0, 0, 0, 0);
|
|
537
807
|
const prevYear = new Date(now.getFullYear() - 1, 0, 1, 0, 0, 0, 0);
|
|
538
808
|
return { from: prevYear, to: thisYear };
|
|
539
809
|
},
|
|
540
810
|
'annual',
|
|
541
|
-
)
|
|
811
|
+
);
|
|
812
|
+
// Laufendes Jahr — live entry
|
|
813
|
+
this._calculateAggregation(
|
|
814
|
+
'statistics.jsonDaily',
|
|
815
|
+
'statistics.jsonAnnual',
|
|
816
|
+
now => {
|
|
817
|
+
const thisYear = new Date(now.getFullYear(), 0, 1, 0, 0, 0, 0);
|
|
818
|
+
const nextYear = new Date(now.getFullYear() + 1, 0, 1, 0, 0, 0, 0);
|
|
819
|
+
return { from: thisYear, to: nextYear };
|
|
820
|
+
},
|
|
821
|
+
'annual-live',
|
|
822
|
+
);
|
|
823
|
+
this._buildFlexchart('annual');
|
|
542
824
|
}
|
|
543
825
|
|
|
544
826
|
/**
|
|
545
827
|
* Initialize and schedule the unified task manager.
|
|
546
|
-
*
|
|
828
|
+
* Runs every full hour.
|
|
547
829
|
*/
|
|
548
830
|
_initializeTask() {
|
|
549
831
|
const scheduleNextRun = () => {
|
|
550
832
|
const now = new Date();
|
|
833
|
+
|
|
551
834
|
const next = new Date(now);
|
|
552
835
|
if (this.testing) {
|
|
553
|
-
next.setMinutes(now.getMinutes() + 1, 0, 0);
|
|
836
|
+
next.setMinutes(now.getMinutes() + 1, 0, 0);
|
|
554
837
|
} else {
|
|
555
|
-
next.setHours(next.getHours() + 1, 0, 0,
|
|
838
|
+
next.setHours(next.getHours() + 1, 0, 0, 100);
|
|
556
839
|
}
|
|
557
|
-
|
|
558
|
-
// Skip executing tasks exactly at midnight to avoid running aggregated jobs
|
|
559
840
|
if (next.getHours() === 0 && next.getMinutes() === 0) {
|
|
560
|
-
next.setHours(next.getHours() + 1, 0, 0, 0);
|
|
841
|
+
next.setHours(next.getHours() + 1, 0, 0, 0);
|
|
561
842
|
}
|
|
562
843
|
const msToNextHour = next.getTime() - now.getTime();
|
|
844
|
+
this.adapter.logger.debug(`### Statistics - Scheduler start ${now.toLocaleTimeString()} next ${next.toLocaleTimeString()}`);
|
|
563
845
|
|
|
564
846
|
if (this.taskTimer) {
|
|
565
847
|
this.adapter.clearTimeout(this.taskTimer);
|
|
566
848
|
}
|
|
567
|
-
|
|
568
849
|
this.taskTimer = this.adapter.setTimeout(() => {
|
|
569
850
|
this._executeScheduledTasks();
|
|
570
|
-
scheduleNextRun();
|
|
851
|
+
scheduleNextRun();
|
|
571
852
|
}, msToNextHour);
|
|
572
853
|
};
|
|
573
|
-
// Schedule the next run
|
|
574
854
|
scheduleNextRun();
|
|
575
855
|
}
|
|
856
|
+
|
|
576
857
|
_executeScheduledTasks() {
|
|
577
858
|
this._calculateHourly();
|
|
578
859
|
this._calculateDaily();
|
|
@@ -582,14 +863,13 @@ class statistics {
|
|
|
582
863
|
}
|
|
583
864
|
|
|
584
865
|
/**
|
|
585
|
-
* Executes every midnight
|
|
586
|
-
* -
|
|
587
|
-
* -
|
|
866
|
+
* Executes every midnight:
|
|
867
|
+
* - Runs all scheduled tasks
|
|
868
|
+
* - Clears old data based on retention policies
|
|
588
869
|
*/
|
|
589
870
|
mitNightProcess() {
|
|
590
871
|
const now = new Date();
|
|
591
872
|
this._executeScheduledTasks();
|
|
592
|
-
// Clear old data based on retention policies
|
|
593
873
|
const startOfYear = new Date(now.getFullYear(), 0, 1, 0, 0, 0, 0);
|
|
594
874
|
this._clearGeneric('statistics.jsonDaily', startOfYear);
|
|
595
875
|
this._clearGeneric('statistics.jsonWeekly', startOfYear);
|
|
@@ -598,7 +878,6 @@ class statistics {
|
|
|
598
878
|
}
|
|
599
879
|
|
|
600
880
|
async initialize() {
|
|
601
|
-
// load consumption JSON states (keep as string)
|
|
602
881
|
let state = await this.adapter.getState('statistics.jsonHourly');
|
|
603
882
|
this.stateCache.set('statistics.jsonHourly', state?.val ?? '[]', { type: 'string', stored: true });
|
|
604
883
|
|
|
@@ -606,7 +885,7 @@ class statistics {
|
|
|
606
885
|
this.stateCache.set('statistics.jsonDaily', state?.val ?? '[]', { type: 'string', stored: true });
|
|
607
886
|
|
|
608
887
|
state = await this.adapter.getState('statistics.jsonWeekly');
|
|
609
|
-
this.stateCache.set('statistics.jsonWeekly', state?.val ?? '[]', { type: 'string', stored: true });
|
|
888
|
+
this.stateCache.set('statistics.jsonWeekly', state?.val ?? '[]', { type: 'string', stored: true });
|
|
610
889
|
|
|
611
890
|
state = await this.adapter.getState('statistics.jsonMonthly');
|
|
612
891
|
this.stateCache.set('statistics.jsonMonthly', state?.val ?? '[]', { type: 'string', stored: true });
|
|
@@ -614,10 +893,17 @@ class statistics {
|
|
|
614
893
|
state = await this.adapter.getState('statistics.jsonAnnual');
|
|
615
894
|
this.stateCache.set('statistics.jsonAnnual', state?.val ?? '[]', { type: 'string', stored: true });
|
|
616
895
|
|
|
617
|
-
|
|
618
|
-
|
|
896
|
+
state = await this.adapter.getState('statistics.jsonToday');
|
|
897
|
+
this.stateCache.set('statistics.jsonToday', state?.val ?? '{}', { type: 'string', stored: true });
|
|
898
|
+
try {
|
|
899
|
+
await tools.waitForValue(() => this.stateCache.get('collected.accumulatedEnergyYield')?.value, 5 * 60000);
|
|
900
|
+
} catch {
|
|
901
|
+
this.adapter.logger.warn(
|
|
902
|
+
"statistics: waited 5 minutes for state 'collected.accumulatedEnergyYield' to be available but it didn't, computed statistics will not work until this state is present",
|
|
903
|
+
);
|
|
904
|
+
}
|
|
619
905
|
|
|
620
|
-
//
|
|
906
|
+
// Load templates — one per chart type
|
|
621
907
|
for (const chartType of ['hourly', 'daily', 'weekly', 'monthly', 'annual']) {
|
|
622
908
|
const templateStateId = `statistics.flexCharts.template.${chartType}`;
|
|
623
909
|
state = await this.adapter.getState(templateStateId);
|
|
@@ -629,13 +915,22 @@ class statistics {
|
|
|
629
915
|
}
|
|
630
916
|
}
|
|
631
917
|
|
|
632
|
-
this.mitNightProcess();
|
|
918
|
+
this.mitNightProcess();
|
|
633
919
|
this._initializeTask();
|
|
634
920
|
this.adapter.subscribeStates(`${this._path}.*`);
|
|
921
|
+
this._initialized = true;
|
|
635
922
|
}
|
|
636
923
|
|
|
924
|
+
/**
|
|
925
|
+
* Builds and updates the Flexchart configuration for the specified chart type.
|
|
926
|
+
*
|
|
927
|
+
* @param {string} myChart - 'hourly' | 'daily' | 'weekly' | 'monthly' | 'annual'
|
|
928
|
+
* @param {string} [chartStyle] - 'line' | 'bar' — defaults to 'line' for hourly, 'bar' for others
|
|
929
|
+
* @returns {string} The generated chart configuration as a javascript-stringify string
|
|
930
|
+
*/
|
|
637
931
|
_buildFlexchart(myChart, chartStyle) {
|
|
638
|
-
chartStyle = chartStyle || (myChart === 'hourly' ? 'line' : 'bar');
|
|
932
|
+
chartStyle = chartStyle || (myChart === 'hourly' ? 'line' : 'bar');
|
|
933
|
+
|
|
639
934
|
const IDS = {
|
|
640
935
|
hourly: 'statistics.jsonHourly',
|
|
641
936
|
daily: 'statistics.jsonDaily',
|
|
@@ -663,9 +958,12 @@ class statistics {
|
|
|
663
958
|
})}`;
|
|
664
959
|
}
|
|
665
960
|
if (myChart === 'weekly') {
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
961
|
+
const toDay = new Date(to);
|
|
962
|
+
//mitnight -> the day before
|
|
963
|
+
if (toDay.getHours() === 0 && toDay.getMinutes() === 0) {
|
|
964
|
+
toDay.setDate(toDay.getDate() - 1);
|
|
965
|
+
}
|
|
966
|
+
return `${from.toLocaleDateString('de-DE', { month: '2-digit', day: '2-digit' })}-${toDay.toLocaleDateString('de-DE', { month: '2-digit', day: '2-digit' })}`;
|
|
669
967
|
}
|
|
670
968
|
if (myChart === 'monthly') {
|
|
671
969
|
return from.toLocaleDateString('de-DE', { year: 'numeric', month: '2-digit' });
|
|
@@ -673,22 +971,12 @@ class statistics {
|
|
|
673
971
|
if (myChart === 'annual') {
|
|
674
972
|
return from.toLocaleDateString('de-DE', { year: 'numeric' });
|
|
675
973
|
}
|
|
676
|
-
|
|
677
974
|
return from.toLocaleDateString('de-DE', { year: 'numeric', month: '2-digit', day: '2-digit' });
|
|
678
|
-
/*
|
|
679
|
-
return myChart === 'hourly'
|
|
680
|
-
? `${to.toLocaleDateString('de-DE', { year: 'numeric', month: '2-digit', day: '2-digit' })} ${to.toLocaleTimeString('de-DE', {
|
|
681
|
-
hour12: false,
|
|
682
|
-
hour: '2-digit',
|
|
683
|
-
minute: '2-digit',
|
|
684
|
-
})}`
|
|
685
|
-
: from.toLocaleDateString('de-DE', { year: 'numeric', month: '2-digit', day: '2-digit' });
|
|
686
|
-
*/
|
|
687
975
|
});
|
|
688
976
|
|
|
689
977
|
const xAxisDataShort = myChart === 'hourly' ? xAxisData.map(label => label.split(' ')[1]) : xAxisData;
|
|
690
978
|
|
|
691
|
-
// ---
|
|
979
|
+
// --- Day areas (hourly only) ---
|
|
692
980
|
const dayAreas = [];
|
|
693
981
|
if (myChart === 'hourly' && xAxisData.length > 0) {
|
|
694
982
|
const dayBoundaries = [0];
|
|
@@ -734,19 +1022,15 @@ class statistics {
|
|
|
734
1022
|
const extract = key => data.map(e => Number(Number(e[key]?.value ?? 0).toFixed(3)));
|
|
735
1023
|
const negate = arr => arr.map(v => Number((-v).toFixed(3)));
|
|
736
1024
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
SOC: extract('SOC'),
|
|
745
|
-
gridExportNeg: negate(extract('gridExport')),
|
|
746
|
-
chargeCapacityNeg: negate(extract('chargeCapacity')),
|
|
747
|
-
};
|
|
1025
|
+
// Build seriesData from all this.stats entries (targetPath as key)
|
|
1026
|
+
const seriesData = {};
|
|
1027
|
+
for (const stat of this.stats) {
|
|
1028
|
+
const values = extract(stat.targetPath);
|
|
1029
|
+
seriesData[stat.targetPath] = values;
|
|
1030
|
+
seriesData[`${stat.targetPath}Neg`] = negate(values);
|
|
1031
|
+
}
|
|
748
1032
|
|
|
749
|
-
// --- Tooltip formatter
|
|
1033
|
+
// --- Tooltip formatter ---
|
|
750
1034
|
const tooltipFormatter = params => {
|
|
751
1035
|
if (!Array.isArray(params)) params = [params];
|
|
752
1036
|
return params
|
|
@@ -754,8 +1038,10 @@ class statistics {
|
|
|
754
1038
|
.map(p => {
|
|
755
1039
|
const negatedSeries = ['Grid Export', 'Charge'];
|
|
756
1040
|
const val = negatedSeries.includes(p.seriesName) ? Math.abs(p.value) : p.value;
|
|
757
|
-
const unit =
|
|
758
|
-
|
|
1041
|
+
const unit = ['SOC', 'Self-sufficiency', 'Self-consumption'].includes(p.seriesName) ? ' %' : ' kWh';
|
|
1042
|
+
const seriesName =
|
|
1043
|
+
myChart === 'hourly' && ['Self-sufficiency', 'Self-consumption'].includes(p.seriesName) ? `${p.seriesName} today` : p.seriesName;
|
|
1044
|
+
return `${p.marker}${seriesName}: <b>${val}${unit}</b>`;
|
|
759
1045
|
})
|
|
760
1046
|
.join('<br/>');
|
|
761
1047
|
};
|
|
@@ -769,12 +1055,9 @@ class statistics {
|
|
|
769
1055
|
|
|
770
1056
|
try {
|
|
771
1057
|
const templ = JSON.parse(templateStr);
|
|
772
|
-
|
|
773
1058
|
if (Object.keys(templ).length === 0) {
|
|
774
|
-
// Kein Template → built-in Default
|
|
775
1059
|
chartStr = this._buildDefaultChart(myChart, chartStyle, xAxisData, xAxisDataShort, dayAreas, seriesData);
|
|
776
1060
|
} else {
|
|
777
|
-
// Template vorhanden → mit javascript-stringify serialisieren
|
|
778
1061
|
chartStr = stringify(templ);
|
|
779
1062
|
}
|
|
780
1063
|
} catch (e) {
|
|
@@ -782,30 +1065,21 @@ class statistics {
|
|
|
782
1065
|
chartStr = this._buildDefaultChart(myChart, chartStyle, xAxisData, xAxisDataShort, dayAreas, seriesData);
|
|
783
1066
|
}
|
|
784
1067
|
|
|
785
|
-
// --- Replace
|
|
1068
|
+
// --- Replace placeholders ---
|
|
786
1069
|
chartStr = chartStr
|
|
787
|
-
// X-Achse
|
|
788
1070
|
.replace("'%%xAxisData%%'", JSON.stringify(xAxisData))
|
|
789
1071
|
.replace("'%%xAxisDataShort%%'", JSON.stringify(xAxisDataShort))
|
|
790
1072
|
.replace("'%%xAxisMax%%'", String(xAxisData.length - 1))
|
|
791
|
-
// Originaldaten (immer positiv)
|
|
792
|
-
.replace("'%%solarYield%%'", JSON.stringify(seriesData.solarYield))
|
|
793
|
-
.replace("'%%consumption%%'", JSON.stringify(seriesData.consumption))
|
|
794
|
-
.replace("'%%gridExport%%'", JSON.stringify(seriesData.gridExport))
|
|
795
|
-
.replace("'%%gridImport%%'", JSON.stringify(seriesData.gridImport))
|
|
796
|
-
.replace("'%%chargeCapacity%%'", JSON.stringify(seriesData.chargeCapacity))
|
|
797
|
-
.replace("'%%dischargeCapacity%%'", JSON.stringify(seriesData.dischargeCapacity))
|
|
798
|
-
.replace("'%%SOC%%'", JSON.stringify(seriesData.SOC))
|
|
799
|
-
// Negierte Varianten für gegenläufige Darstellung
|
|
800
|
-
.replace("'%%gridExportNeg%%'", JSON.stringify(seriesData.gridExportNeg))
|
|
801
|
-
.replace("'%%chargeCapacityNeg%%'", JSON.stringify(seriesData.chargeCapacityNeg))
|
|
802
|
-
// Sonstiges
|
|
803
|
-
.replace("'%%dayAreas%%'", JSON.stringify(dayAreas))
|
|
804
1073
|
.replace("'%%chartTitle%%'", JSON.stringify(`PV Statistics — ${myChart}`))
|
|
805
|
-
|
|
1074
|
+
.replace("'%%dayAreas%%'", JSON.stringify(dayAreas))
|
|
806
1075
|
.replace("'%%tooltipFormatter%%'", stringify(tooltipFormatter));
|
|
807
1076
|
|
|
808
|
-
//
|
|
1077
|
+
// All this.stats entries dynamically — both positive and negated
|
|
1078
|
+
for (const stat of this.stats) {
|
|
1079
|
+
const key = stat.targetPath;
|
|
1080
|
+
chartStr = chartStr.replace(`'%%${key}%%'`, JSON.stringify(seriesData[key])).replace(`'%%${key}Neg%%'`, JSON.stringify(seriesData[`${key}Neg`]));
|
|
1081
|
+
}
|
|
1082
|
+
|
|
809
1083
|
this.stateCache.set(outputStateId, chartStr, { type: 'string' });
|
|
810
1084
|
this.adapter.logger.debug(`statistics: flexCharts built for ${myChart}/${chartStyle}`);
|
|
811
1085
|
|
|
@@ -835,7 +1109,15 @@ class statistics {
|
|
|
835
1109
|
const negate = arr => arr.map(v => Number((-v).toFixed(3)));
|
|
836
1110
|
const showSOC = myChart === 'hourly';
|
|
837
1111
|
|
|
838
|
-
//
|
|
1112
|
+
// No-data hint — chart-type specific
|
|
1113
|
+
const noDataHints = {
|
|
1114
|
+
hourly: 'No data yet — first entry available after the next full hour.',
|
|
1115
|
+
daily: 'No data yet — first entry available tomorrow after midnight.',
|
|
1116
|
+
weekly: 'No data yet — first entry available after the current week ends.',
|
|
1117
|
+
monthly: 'No data yet — first entry available after the current month ends.',
|
|
1118
|
+
annual: 'No data yet — first entry available after the current year ends.',
|
|
1119
|
+
};
|
|
1120
|
+
|
|
839
1121
|
const tooltipFormatter = params => {
|
|
840
1122
|
if (!Array.isArray(params)) params = [params];
|
|
841
1123
|
return params
|
|
@@ -843,12 +1125,29 @@ class statistics {
|
|
|
843
1125
|
.map(p => {
|
|
844
1126
|
const negatedSeries = ['Grid Export', 'Charge'];
|
|
845
1127
|
const val = negatedSeries.includes(p.seriesName) ? Math.abs(p.value) : p.value;
|
|
846
|
-
const unit =
|
|
847
|
-
|
|
1128
|
+
const unit = ['SOC', 'Self-sufficiency', 'Self-consumption'].includes(p.seriesName) ? ' %' : ' kWh';
|
|
1129
|
+
const seriesName =
|
|
1130
|
+
myChart === 'hourly' && ['Self-sufficiency', 'Self-consumption'].includes(p.seriesName) ? `${p.seriesName} today` : p.seriesName;
|
|
1131
|
+
return `${p.marker}${seriesName}: <b>${val}${unit}</b>`;
|
|
848
1132
|
})
|
|
849
1133
|
.join('<br/>');
|
|
850
1134
|
};
|
|
851
1135
|
|
|
1136
|
+
// --- Slider start position — show recent entries by default ---
|
|
1137
|
+
// For hourly: show last 25 entries (~1 day)
|
|
1138
|
+
// For daily: show last 7 entries (~1 week)
|
|
1139
|
+
// For weekly: show last 8 entries (~2 months)
|
|
1140
|
+
// For monthly: show last 13 entries (~1 year)
|
|
1141
|
+
// For annual: show full range
|
|
1142
|
+
const sliderDefaults = {
|
|
1143
|
+
hourly: { start: Math.max(0, Math.round((1 - 25 / Math.max(xAxisData.length, 1)) * 100)), end: 100 },
|
|
1144
|
+
daily: { start: Math.max(0, Math.round((1 - 7 / Math.max(xAxisData.length, 1)) * 100)), end: 100 },
|
|
1145
|
+
weekly: { start: Math.max(0, Math.round((1 - 8 / Math.max(xAxisData.length, 1)) * 100)), end: 100 },
|
|
1146
|
+
monthly: { start: Math.max(0, Math.round((1 - 13 / Math.max(xAxisData.length, 1)) * 100)), end: 100 },
|
|
1147
|
+
annual: { start: 0, end: 100 },
|
|
1148
|
+
};
|
|
1149
|
+
const slider = sliderDefaults[myChart] ?? { start: 0, end: 100 };
|
|
1150
|
+
|
|
852
1151
|
const chart = {
|
|
853
1152
|
backgroundColor: '#fff',
|
|
854
1153
|
animation: false,
|
|
@@ -859,7 +1158,17 @@ class statistics {
|
|
|
859
1158
|
legend: {
|
|
860
1159
|
top: 35,
|
|
861
1160
|
left: 'center',
|
|
862
|
-
data: [
|
|
1161
|
+
data: [
|
|
1162
|
+
'Solar Yield',
|
|
1163
|
+
'Grid Export',
|
|
1164
|
+
'Grid Import',
|
|
1165
|
+
'Charge',
|
|
1166
|
+
'Discharge',
|
|
1167
|
+
...(showSOC ? ['SOC'] : []),
|
|
1168
|
+
'Self-sufficiency',
|
|
1169
|
+
'Self-consumption',
|
|
1170
|
+
'Consumption',
|
|
1171
|
+
],
|
|
863
1172
|
},
|
|
864
1173
|
tooltip: {
|
|
865
1174
|
trigger: 'axis',
|
|
@@ -888,9 +1197,25 @@ class statistics {
|
|
|
888
1197
|
saveAsImage: { show: true },
|
|
889
1198
|
},
|
|
890
1199
|
},
|
|
1200
|
+
// No-data graphic — shown only when data is empty
|
|
1201
|
+
graphic:
|
|
1202
|
+
xAxisData.length === 0
|
|
1203
|
+
? [
|
|
1204
|
+
{
|
|
1205
|
+
type: 'text',
|
|
1206
|
+
left: 'center',
|
|
1207
|
+
top: 'middle',
|
|
1208
|
+
style: {
|
|
1209
|
+
text: noDataHints[myChart] || 'No data available yet.',
|
|
1210
|
+
fontSize: 14,
|
|
1211
|
+
fill: '#999',
|
|
1212
|
+
},
|
|
1213
|
+
},
|
|
1214
|
+
]
|
|
1215
|
+
: [],
|
|
891
1216
|
grid: [
|
|
892
|
-
{ left: '8%', right: showSOC ? '8%' : '4%', top: 80, height: '
|
|
893
|
-
{ left: '8%', right: showSOC ? '8%' : '4%', top: '
|
|
1217
|
+
{ left: '8%', right: showSOC ? '8%' : '4%', top: 80, height: '45%' },
|
|
1218
|
+
{ left: '8%', right: showSOC ? '8%' : '4%', top: '72%', height: '15%' },
|
|
894
1219
|
],
|
|
895
1220
|
xAxis: [
|
|
896
1221
|
{
|
|
@@ -915,7 +1240,7 @@ class statistics {
|
|
|
915
1240
|
gridIndex: 1,
|
|
916
1241
|
data: xAxisData,
|
|
917
1242
|
scale: true,
|
|
918
|
-
boundaryGap:
|
|
1243
|
+
boundaryGap: chartStyle !== 'line',
|
|
919
1244
|
axisLine: { onZero: false },
|
|
920
1245
|
axisTick: { show: false },
|
|
921
1246
|
splitLine: { show: false },
|
|
@@ -925,7 +1250,7 @@ class statistics {
|
|
|
925
1250
|
},
|
|
926
1251
|
],
|
|
927
1252
|
yAxis: [
|
|
928
|
-
// Index 0 —
|
|
1253
|
+
// Index 0 — Energy left axis
|
|
929
1254
|
{
|
|
930
1255
|
scale: false,
|
|
931
1256
|
splitArea: { show: true },
|
|
@@ -936,23 +1261,19 @@ class statistics {
|
|
|
936
1261
|
splitLine: { show: true },
|
|
937
1262
|
axisLine: { show: true },
|
|
938
1263
|
},
|
|
939
|
-
// Index 1 — SOC
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
},
|
|
953
|
-
]
|
|
954
|
-
: []),
|
|
955
|
-
// Index 1 oder 2 — Consumption unteres Grid
|
|
1264
|
+
// Index 1 — SOC / ratio right axis
|
|
1265
|
+
{
|
|
1266
|
+
type: 'value',
|
|
1267
|
+
min: 0,
|
|
1268
|
+
max: 100,
|
|
1269
|
+
name: showSOC ? 'SOC / Ratio (%)' : 'Ratio (%)',
|
|
1270
|
+
nameLocation: 'middle',
|
|
1271
|
+
nameGap: 50,
|
|
1272
|
+
axisLabel: { formatter: '{value} %' },
|
|
1273
|
+
splitLine: { show: false },
|
|
1274
|
+
axisLine: { show: true },
|
|
1275
|
+
},
|
|
1276
|
+
// Index 2 — Consumption lower grid
|
|
956
1277
|
{
|
|
957
1278
|
scale: true,
|
|
958
1279
|
gridIndex: 1,
|
|
@@ -967,11 +1288,23 @@ class statistics {
|
|
|
967
1288
|
},
|
|
968
1289
|
],
|
|
969
1290
|
dataZoom: [
|
|
970
|
-
{
|
|
971
|
-
|
|
1291
|
+
{
|
|
1292
|
+
type: 'inside',
|
|
1293
|
+
xAxisIndex: [0, 1],
|
|
1294
|
+
start: slider.start,
|
|
1295
|
+
end: slider.end,
|
|
1296
|
+
},
|
|
1297
|
+
{
|
|
1298
|
+
show: true,
|
|
1299
|
+
xAxisIndex: [0, 1],
|
|
1300
|
+
type: 'slider',
|
|
1301
|
+
bottom: 5,
|
|
1302
|
+
start: slider.start,
|
|
1303
|
+
end: slider.end,
|
|
1304
|
+
},
|
|
972
1305
|
],
|
|
973
1306
|
series: [
|
|
974
|
-
// Positive
|
|
1307
|
+
// Positive values (above zero line)
|
|
975
1308
|
{
|
|
976
1309
|
name: 'Solar Yield',
|
|
977
1310
|
type: seriesType,
|
|
@@ -980,40 +1313,40 @@ class statistics {
|
|
|
980
1313
|
emphasis: { focus: 'series' },
|
|
981
1314
|
...lineOptions,
|
|
982
1315
|
},
|
|
1316
|
+
// Negative values (below zero line)
|
|
983
1317
|
{
|
|
984
|
-
name: 'Grid
|
|
1318
|
+
name: 'Grid Export',
|
|
985
1319
|
type: seriesType,
|
|
986
|
-
data: seriesData.
|
|
987
|
-
itemStyle: { color: '#
|
|
1320
|
+
data: negate(seriesData.gridExport),
|
|
1321
|
+
itemStyle: { color: '#5cb85c' },
|
|
988
1322
|
emphasis: { focus: 'series' },
|
|
989
1323
|
...lineOptions,
|
|
990
1324
|
},
|
|
991
1325
|
{
|
|
992
|
-
name: '
|
|
1326
|
+
name: 'Grid Import',
|
|
993
1327
|
type: seriesType,
|
|
994
|
-
data: seriesData.
|
|
995
|
-
itemStyle: { color: '#
|
|
1328
|
+
data: seriesData.gridImport,
|
|
1329
|
+
itemStyle: { color: '#ec0000' },
|
|
996
1330
|
emphasis: { focus: 'series' },
|
|
997
1331
|
...lineOptions,
|
|
998
1332
|
},
|
|
999
|
-
// Negative Werte (unterhalb Nulllinie)
|
|
1000
1333
|
{
|
|
1001
|
-
name: '
|
|
1334
|
+
name: 'Charge',
|
|
1002
1335
|
type: seriesType,
|
|
1003
|
-
data: negate(seriesData.
|
|
1004
|
-
itemStyle: { color: '#
|
|
1336
|
+
data: negate(seriesData.chargeCapacity),
|
|
1337
|
+
itemStyle: { color: '#5bc0de' },
|
|
1005
1338
|
emphasis: { focus: 'series' },
|
|
1006
1339
|
...lineOptions,
|
|
1007
1340
|
},
|
|
1008
1341
|
{
|
|
1009
|
-
name: '
|
|
1342
|
+
name: 'Discharge',
|
|
1010
1343
|
type: seriesType,
|
|
1011
|
-
data:
|
|
1012
|
-
itemStyle: { color: '#
|
|
1344
|
+
data: seriesData.dischargeCapacity,
|
|
1345
|
+
itemStyle: { color: '#ed50e0' },
|
|
1013
1346
|
emphasis: { focus: 'series' },
|
|
1014
1347
|
...lineOptions,
|
|
1015
1348
|
},
|
|
1016
|
-
// SOC —
|
|
1349
|
+
// SOC — hourly only, on right axis
|
|
1017
1350
|
...(showSOC
|
|
1018
1351
|
? [
|
|
1019
1352
|
{
|
|
@@ -1028,18 +1361,41 @@ class statistics {
|
|
|
1028
1361
|
},
|
|
1029
1362
|
]
|
|
1030
1363
|
: []),
|
|
1031
|
-
//
|
|
1032
|
-
|
|
1364
|
+
// Self-sufficiency — right axis
|
|
1365
|
+
{
|
|
1366
|
+
name: 'Self-sufficiency',
|
|
1367
|
+
type: 'line',
|
|
1368
|
+
yAxisIndex: 1,
|
|
1369
|
+
data: seriesData.selfSufficiency,
|
|
1370
|
+
itemStyle: { color: '#9c27b0' },
|
|
1371
|
+
lineStyle: { width: 2, type: 'dashed' },
|
|
1372
|
+
symbol: 'circle',
|
|
1373
|
+
symbolSize: 4,
|
|
1374
|
+
smooth: true,
|
|
1375
|
+
},
|
|
1376
|
+
// Self-consumption — right axis
|
|
1377
|
+
{
|
|
1378
|
+
name: 'Self-consumption',
|
|
1379
|
+
type: 'line',
|
|
1380
|
+
yAxisIndex: 1,
|
|
1381
|
+
data: seriesData.selfConsumption,
|
|
1382
|
+
itemStyle: { color: '#ff9800' },
|
|
1383
|
+
lineStyle: { width: 2, type: 'dashed' },
|
|
1384
|
+
symbol: 'circle',
|
|
1385
|
+
symbolSize: 4,
|
|
1386
|
+
smooth: true,
|
|
1387
|
+
},
|
|
1388
|
+
// Consumption in lower grid — always yAxisIndex 2
|
|
1033
1389
|
{
|
|
1034
1390
|
name: 'Consumption',
|
|
1035
1391
|
type: seriesType,
|
|
1036
1392
|
data: seriesData.consumption,
|
|
1037
1393
|
itemStyle: { color: '#337ab7' },
|
|
1038
1394
|
xAxisIndex: 1,
|
|
1039
|
-
yAxisIndex:
|
|
1395
|
+
yAxisIndex: 2,
|
|
1040
1396
|
...lineOptions,
|
|
1041
1397
|
},
|
|
1042
|
-
//
|
|
1398
|
+
// Day-break areas (hourly only)
|
|
1043
1399
|
...(dayAreas.length > 0
|
|
1044
1400
|
? [
|
|
1045
1401
|
{
|
|
@@ -1059,6 +1415,11 @@ class statistics {
|
|
|
1059
1415
|
return stringify(chart).replace("'%%xAxisFormatter%%'", stringify(xAxisFormatterHourly)).replace("'%%tooltipFormatter%%'", stringify(tooltipFormatter));
|
|
1060
1416
|
}
|
|
1061
1417
|
|
|
1418
|
+
/**
|
|
1419
|
+
* Merges two objects deeply.
|
|
1420
|
+
* @param target
|
|
1421
|
+
* @param source
|
|
1422
|
+
*/
|
|
1062
1423
|
_deepMerge(target, source) {
|
|
1063
1424
|
for (const key of Object.keys(source)) {
|
|
1064
1425
|
if (
|
|
@@ -1069,16 +1430,19 @@ class statistics {
|
|
|
1069
1430
|
typeof target[key] === 'object' &&
|
|
1070
1431
|
!Array.isArray(target[key])
|
|
1071
1432
|
) {
|
|
1072
|
-
// Rekursiv für verschachtelte Objekte
|
|
1073
1433
|
this._deepMerge(target[key], source[key]);
|
|
1074
1434
|
} else {
|
|
1075
|
-
// Primitive, Arrays direkt überschreiben
|
|
1076
1435
|
target[key] = source[key];
|
|
1077
1436
|
}
|
|
1078
1437
|
}
|
|
1079
1438
|
return target;
|
|
1080
1439
|
}
|
|
1081
1440
|
|
|
1441
|
+
/**
|
|
1442
|
+
* Handles a template state change — updates cache, acknowledges and rebuilds chart.
|
|
1443
|
+
* @param chartType
|
|
1444
|
+
* @param state
|
|
1445
|
+
*/
|
|
1082
1446
|
async handleTemplateChange(chartType, state) {
|
|
1083
1447
|
const templateStateId = `statistics.flexCharts.template.${chartType}`;
|
|
1084
1448
|
const template = this.stateCache.get(templateStateId)?.value;
|
|
@@ -1097,7 +1461,7 @@ class statistics {
|
|
|
1097
1461
|
/**
|
|
1098
1462
|
* Entry point for adapter to handle messages related to statistics/flexcharts.
|
|
1099
1463
|
*
|
|
1100
|
-
* @param {{chart?: string}} message
|
|
1464
|
+
* @param {{chart?: string, style?: string}} message
|
|
1101
1465
|
* @param {Function} callback
|
|
1102
1466
|
*/
|
|
1103
1467
|
handleFlexMessage(message, callback) {
|