iobroker.sun2000 2.4.5 → 2.5.0
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 +5 -5
- package/admin/jsonConfig.json5 +33 -0
- package/io-package.json +16 -15
- package/lib/drivers/driver_emma.js +6 -6
- package/lib/drivers/driver_inverter.js +67 -23
- package/lib/json_helper.js +3 -25
- package/lib/modbus/modbus_server.js +1 -1
- package/lib/register.js +174 -26
- package/lib/statistics/breakdownChartHelper.js +131 -0
- package/lib/statistics/consumptionBreakdown.js +304 -0
- package/lib/statistics/statistics.js +1453 -0
- package/lib/tools.js +15 -3
- package/main.js +12 -0
- package/package.json +9 -10
- package/lib/statistics.js +0 -1412
package/lib/register.js
CHANGED
|
@@ -4,7 +4,7 @@ const { deviceType, driverClasses, dataRefreshRate } = require(`${__dirname}/typ
|
|
|
4
4
|
const { RiemannSum, StateMap } = require(`${__dirname}/tools.js`);
|
|
5
5
|
const getDriverHandler = require(`${__dirname}/drivers/index.js`);
|
|
6
6
|
const tools = require(`${__dirname}/tools.js`);
|
|
7
|
-
const statistics = require(`${__dirname}/statistics.js`);
|
|
7
|
+
const statistics = require(`${__dirname}/statistics/statistics.js`);
|
|
8
8
|
|
|
9
9
|
class Registers {
|
|
10
10
|
constructor(adapterInstance) {
|
|
@@ -12,6 +12,14 @@ class Registers {
|
|
|
12
12
|
this.stateCache = new StateMap();
|
|
13
13
|
|
|
14
14
|
this.externalSum = new RiemannSum();
|
|
15
|
+
this.inPowerSum = new RiemannSum();
|
|
16
|
+
this.actPowerSum = new RiemannSum();
|
|
17
|
+
this.chargeSum = new RiemannSum();
|
|
18
|
+
this.dischargeSum = new RiemannSum();
|
|
19
|
+
this.houseConsumSum = new RiemannSum();
|
|
20
|
+
this.feedinSum = new RiemannSum();
|
|
21
|
+
this.feedoutSum = new RiemannSum();
|
|
22
|
+
|
|
15
23
|
this.statistics = new statistics(adapterInstance, this.stateCache);
|
|
16
24
|
|
|
17
25
|
for (const device of this.adapter.devices) {
|
|
@@ -34,9 +42,84 @@ class Registers {
|
|
|
34
42
|
}
|
|
35
43
|
})
|
|
36
44
|
);
|
|
45
|
+
//Upgrade to v2.5.0 - deleted deprecated states
|
|
46
|
+
/*
|
|
47
|
+
if (
|
|
48
|
+
tools.existsState(this.adapter, `collected.consumptionStart`, (err, exists) => {
|
|
49
|
+
if (!err && exists) {
|
|
50
|
+
tools.deleteState(this.adapter, `collected.consumptionStart`, (err, deleted) => {
|
|
51
|
+
if (!err && deleted) {
|
|
52
|
+
this.adapter.logger.debug('Deleted deprecated state collected.consumptionStart');
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
*/
|
|
37
59
|
|
|
38
60
|
//this.postProcessHooks = [];
|
|
39
61
|
this.postProcessHooks = [
|
|
62
|
+
{
|
|
63
|
+
refresh: dataRefreshRate.high,
|
|
64
|
+
states: [
|
|
65
|
+
{
|
|
66
|
+
id: 'collected.sumDaily.houseConsumption',
|
|
67
|
+
name: 'daily sum of House consumption',
|
|
68
|
+
type: 'number',
|
|
69
|
+
unit: 'kWh',
|
|
70
|
+
role: 'value.power.consumed',
|
|
71
|
+
desc: 'riemann sum of Load power',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'collected.sumDaily.activePower',
|
|
75
|
+
name: 'daily sum of Active power',
|
|
76
|
+
type: 'number',
|
|
77
|
+
unit: 'kWh',
|
|
78
|
+
role: 'value.power.produced',
|
|
79
|
+
desc: 'riemann sum of Power currently used',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'collected.sumDaily.inputPower',
|
|
83
|
+
name: 'daily sum of Input power',
|
|
84
|
+
type: 'number',
|
|
85
|
+
unit: 'kWh',
|
|
86
|
+
role: 'value.power.produced',
|
|
87
|
+
desc: 'riemann sum of Power from solar',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'collected.sumDaily.chargePower',
|
|
91
|
+
name: 'daily sum of Charge power',
|
|
92
|
+
type: 'number',
|
|
93
|
+
unit: 'kWh',
|
|
94
|
+
role: 'value.power.consumed',
|
|
95
|
+
desc: 'riemann sum of Charge power',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: 'collected.sumDaily.dischargePower',
|
|
99
|
+
name: 'daily sum of Discharge power',
|
|
100
|
+
type: 'number',
|
|
101
|
+
unit: 'kWh',
|
|
102
|
+
role: 'value.power.produced',
|
|
103
|
+
desc: 'riemann sum of Discharge power',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'collected.sumDaily.feed-inPower',
|
|
107
|
+
name: 'daily sum of Feed-in power',
|
|
108
|
+
type: 'number',
|
|
109
|
+
unit: 'kWh',
|
|
110
|
+
role: 'value.power.consumed',
|
|
111
|
+
desc: 'riemann sum of Feed-in power',
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: 'collected.sumDaily.feed-outPower',
|
|
115
|
+
name: 'daily sum of Feed-out power',
|
|
116
|
+
type: 'number',
|
|
117
|
+
unit: 'kWh',
|
|
118
|
+
role: 'value.power.produced',
|
|
119
|
+
desc: 'riemann sum of Feed-out power',
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
},
|
|
40
123
|
{
|
|
41
124
|
refresh: dataRefreshRate.high,
|
|
42
125
|
states: [
|
|
@@ -175,6 +258,23 @@ class Registers {
|
|
|
175
258
|
if (houseConsum < 0) {
|
|
176
259
|
houseConsum = 0;
|
|
177
260
|
}
|
|
261
|
+
|
|
262
|
+
//dailySum
|
|
263
|
+
this.inPowerSum.add(inPower);
|
|
264
|
+
this.actPowerSum.add(actPower);
|
|
265
|
+
this.chargeSum.add(chargeDischarge > 0 ? chargeDischarge : 0);
|
|
266
|
+
this.dischargeSum.add(chargeDischarge < 0 ? -chargeDischarge : 0);
|
|
267
|
+
this.houseConsumSum.add(houseConsum);
|
|
268
|
+
this.feedinSum.add(feedinPower > 0 ? feedinPower : 0);
|
|
269
|
+
this.feedoutSum.add(feedinPower < 0 ? -feedinPower : 0);
|
|
270
|
+
this.stateCache.set('collected.sumDaily.houseConsumption', this.houseConsumSum.sum, { type: 'number' });
|
|
271
|
+
this.stateCache.set('collected.sumDaily.activePower', this.actPowerSum.sum, { type: 'number' });
|
|
272
|
+
this.stateCache.set('collected.sumDaily.inputPower', this.inPowerSum.sum, { type: 'number' });
|
|
273
|
+
this.stateCache.set('collected.sumDaily.chargePower', this.chargeSum.sum, { type: 'number' });
|
|
274
|
+
this.stateCache.set('collected.sumDaily.dischargePower', this.dischargeSum.sum, { type: 'number' });
|
|
275
|
+
this.stateCache.set('collected.sumDaily.feed-inPower', this.feedinSum.sum, { type: 'number' });
|
|
276
|
+
this.stateCache.set('collected.sumDaily.feed-outPower', this.feedoutSum.sum, { type: 'number' });
|
|
277
|
+
|
|
178
278
|
//Überschuss (Differenz)
|
|
179
279
|
const surplusArray = calcUsableSurplus.bind(this)();
|
|
180
280
|
|
|
@@ -203,7 +303,7 @@ class Registers {
|
|
|
203
303
|
name: 'Daily energy yield',
|
|
204
304
|
type: 'number',
|
|
205
305
|
unit: 'kWh',
|
|
206
|
-
role: 'value.power.
|
|
306
|
+
role: 'value.power.produced',
|
|
207
307
|
desc: 'daily energy yield of the inverters',
|
|
208
308
|
},
|
|
209
309
|
{
|
|
@@ -211,7 +311,7 @@ class Registers {
|
|
|
211
311
|
name: 'Daily input yield',
|
|
212
312
|
type: 'number',
|
|
213
313
|
unit: 'kWh',
|
|
214
|
-
role: 'value.power.
|
|
314
|
+
role: 'value.power.produced',
|
|
215
315
|
desc: 'yield from the portal',
|
|
216
316
|
},
|
|
217
317
|
{
|
|
@@ -219,32 +319,38 @@ class Registers {
|
|
|
219
319
|
name: 'Daily solar yield',
|
|
220
320
|
type: 'number',
|
|
221
321
|
unit: 'kWh',
|
|
222
|
-
role: 'value.power.
|
|
322
|
+
role: 'value.power.produced',
|
|
223
323
|
desc: 'Riemann sum of input power with efficiency loss',
|
|
224
324
|
},
|
|
225
|
-
{ id: 'collected.accumulatedEnergyYield', name: 'Accumulated energy yield', type: 'number', unit: 'kWh', role: 'value.power.
|
|
226
|
-
{ id: 'collected.consumptionSum', name: 'Consumption sum', type: 'number', unit: 'kWh', role: 'value.power.
|
|
227
|
-
{ id: 'collected.gridExportStart', name: 'Grid export start today', type: 'number', unit: 'kWh', role: 'value.power.
|
|
228
|
-
{ id: 'collected.gridImportStart', name: 'Grid import start today', type: 'number', unit: 'kWh', role: 'value.power.
|
|
229
|
-
{
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
325
|
+
{ id: 'collected.accumulatedEnergyYield', name: 'Accumulated energy yield', type: 'number', unit: 'kWh', role: 'value.power.produced' },
|
|
326
|
+
{ id: 'collected.consumptionSum', name: 'Consumption sum', type: 'number', unit: 'kWh', role: 'value.power.consumed' },
|
|
327
|
+
{ id: 'collected.gridExportStart', name: 'Grid export start today', type: 'number', unit: 'kWh', role: 'value.power.produced' },
|
|
328
|
+
{ id: 'collected.gridImportStart', name: 'Grid import start today', type: 'number', unit: 'kWh', role: 'value.power.consumed' },
|
|
329
|
+
{
|
|
330
|
+
id: 'collected.consumptionStart',
|
|
331
|
+
name: 'deprecated - Consumption start today',
|
|
332
|
+
type: 'number',
|
|
333
|
+
unit: 'kWh',
|
|
334
|
+
role: 'value.power.consumed',
|
|
335
|
+
},
|
|
336
|
+
{ id: 'collected.gridExportToday', name: 'Grid export today', type: 'number', unit: 'kWh', role: 'value.power.produced' },
|
|
337
|
+
{ id: 'collected.gridImportToday', name: 'Grid import today', type: 'number', unit: 'kWh', role: 'value.power.consumed' },
|
|
338
|
+
{ id: 'collected.consumptionToday', name: 'Consumption today', type: 'number', unit: 'kWh', role: 'value.power.consumed' },
|
|
339
|
+
{ id: 'collected.totalCharge', name: 'Total charge of battery', type: 'number', unit: 'kWh', role: 'value.power.produced' },
|
|
340
|
+
{ id: 'collected.totalDischarge', name: 'Total discharge of battery', type: 'number', unit: 'kWh', role: 'value.power.produced' },
|
|
235
341
|
{
|
|
236
342
|
id: 'collected.currentDayChargeCapacity',
|
|
237
343
|
name: 'Current day charge capacity of battery',
|
|
238
344
|
type: 'number',
|
|
239
345
|
unit: 'kWh',
|
|
240
|
-
role: 'value.power.
|
|
346
|
+
role: 'value.power.consumed',
|
|
241
347
|
},
|
|
242
348
|
{
|
|
243
349
|
id: 'collected.currentDayDischargeCapacity',
|
|
244
350
|
name: 'Current day discharge capacity of battery',
|
|
245
351
|
type: 'number',
|
|
246
352
|
unit: 'kWh',
|
|
247
|
-
role: 'value.power.
|
|
353
|
+
role: 'value.power.produced',
|
|
248
354
|
desc: '',
|
|
249
355
|
},
|
|
250
356
|
{ id: 'collected.SOC', name: 'State of battery capacity', type: 'number', unit: '%', role: 'value.battery', desc: 'SOC' },
|
|
@@ -254,7 +360,7 @@ class Registers {
|
|
|
254
360
|
name: 'daily external yield',
|
|
255
361
|
type: 'number',
|
|
256
362
|
unit: 'kWh',
|
|
257
|
-
role: 'value.power.
|
|
363
|
+
role: 'value.power.produced',
|
|
258
364
|
desc: 'Riemann sum of external power',
|
|
259
365
|
},
|
|
260
366
|
/*
|
|
@@ -263,7 +369,7 @@ class Registers {
|
|
|
263
369
|
name: 'Active Energy today',
|
|
264
370
|
type: 'number',
|
|
265
371
|
unit: 'kWh',
|
|
266
|
-
role: 'value.power.
|
|
372
|
+
role: 'value.power.produced',
|
|
267
373
|
desc: 'Amount of Riemann sum of sum of active power',
|
|
268
374
|
},
|
|
269
375
|
*/
|
|
@@ -280,17 +386,20 @@ class Registers {
|
|
|
280
386
|
let totalCharge = 0;
|
|
281
387
|
let ratedCap = 0;
|
|
282
388
|
let load = 0;
|
|
283
|
-
let
|
|
284
|
-
let supplyFromGrid;
|
|
389
|
+
let ready = false;
|
|
285
390
|
|
|
286
391
|
for (const inverter of inverters) {
|
|
287
392
|
if (inverter.driverClass != driverClasses.inverter) {
|
|
288
393
|
continue;
|
|
289
394
|
}
|
|
395
|
+
if (this.stateCache.get(`${inverter.path}.accumulatedEnergyYield`)?.value === undefined) {
|
|
396
|
+
continue; //wait until value is available - otherwise we would overwrite the value from the portal with 0
|
|
397
|
+
}
|
|
398
|
+
ready = true;
|
|
290
399
|
outYield += this.stateCache.get(`${inverter.path}.dailyEnergyYield`)?.value ?? 0;
|
|
291
|
-
inYield += this.stateCache.get(`${inverter.path}.derived.dailyInputYield`)?.value;
|
|
292
|
-
solarYield += this.stateCache.get(`${inverter.path}.derived.dailySolarYield`)?.value;
|
|
293
|
-
enYield += this.stateCache.get(`${inverter.path}.accumulatedEnergyYield`)?.value;
|
|
400
|
+
inYield += this.stateCache.get(`${inverter.path}.derived.dailyInputYield`)?.value ?? 0;
|
|
401
|
+
solarYield += this.stateCache.get(`${inverter.path}.derived.dailySolarYield`)?.value ?? 0;
|
|
402
|
+
enYield += this.stateCache.get(`${inverter.path}.accumulatedEnergyYield`)?.value ?? 0;
|
|
294
403
|
activeEnergy += this.stateCache.get(`${inverter.path}.derived.dailyActiveEnergy`)?.value ?? 0;
|
|
295
404
|
if (this.stateCache.get(`${inverter.path}.battery.ratedCapacity`)?.value > 0) {
|
|
296
405
|
charge += this.stateCache.get(`${inverter.path}.battery.currentDayChargeCapacity`)?.value ?? 0;
|
|
@@ -300,9 +409,12 @@ class Registers {
|
|
|
300
409
|
load +=
|
|
301
410
|
this.stateCache.get(`${inverter.path}.battery.ratedCapacity`)?.value *
|
|
302
411
|
this.stateCache.get(`${inverter.path}.battery.SOC`)?.value;
|
|
303
|
-
ratedCap += this.stateCache.get(`${inverter.path}.battery.ratedCapacity`)?.value;
|
|
412
|
+
ratedCap += this.stateCache.get(`${inverter.path}.battery.ratedCapacity`)?.value ?? 0;
|
|
304
413
|
}
|
|
305
414
|
}
|
|
415
|
+
if (!ready) {
|
|
416
|
+
return; //no inverter ready yet - do not set values
|
|
417
|
+
}
|
|
306
418
|
//this.stateCache.set('collected.dailyActiveEnergy', activeEnergy, { type: 'number' });
|
|
307
419
|
this.stateCache.set('collected.dailyExternalYield', this.externalSum.sum, { type: 'number' }); //of externalPower
|
|
308
420
|
this.stateCache.set('collected.dailyEnergyYield', outYield, { type: 'number' });
|
|
@@ -310,9 +422,10 @@ class Registers {
|
|
|
310
422
|
this.stateCache.set('collected.dailySolarYield', solarYield, { type: 'number' });
|
|
311
423
|
this.stateCache.set('collected.accumulatedEnergyYield', enYield, { type: 'number' });
|
|
312
424
|
const sign = this.stateCache.get('meter.derived.signConventionForPowerFeed-in')?.value ?? 1;
|
|
425
|
+
let feedinEnergy;
|
|
426
|
+
let supplyFromGrid;
|
|
313
427
|
if (sign === -1) {
|
|
314
428
|
//Emma Meter Power is positive when power is taken from grid
|
|
315
|
-
//Emma Meter Power is negative when power is feed in to grid
|
|
316
429
|
feedinEnergy = this.stateCache.get('meter.reverseActiveEnergy')?.value ?? 0;
|
|
317
430
|
supplyFromGrid = this.stateCache.get('meter.positiveActiveEnergy')?.value ?? 0;
|
|
318
431
|
} else {
|
|
@@ -354,7 +467,7 @@ class Registers {
|
|
|
354
467
|
this.stateCache.set('collected.currentDayChargeCapacity', charge, { type: 'number' });
|
|
355
468
|
this.stateCache.set('collected.currentDayDischargeCapacity', disCharge, { type: 'number' });
|
|
356
469
|
this.stateCache.set('collected.ratedCapacity', ratedCap, { type: 'number' });
|
|
357
|
-
this.stateCache.set('collected.SOC', Math.round(load / ratedCap), { type: 'number' });
|
|
470
|
+
this.stateCache.set('collected.SOC', ratedCap ? Math.round(load / ratedCap) : 0, { type: 'number' });
|
|
358
471
|
},
|
|
359
472
|
},
|
|
360
473
|
];
|
|
@@ -475,11 +588,34 @@ class Registers {
|
|
|
475
588
|
this.stateCache.set('collected.gridImportStart', state?.val, { type: 'number', stored: true });
|
|
476
589
|
state = await this.adapter.getState('collected.gridImportStart');
|
|
477
590
|
this.stateCache.set('collected.gridImportStart', state?.val, { type: 'number', stored: true });
|
|
591
|
+
//deprecated - only for compatibility reasons - will be removed in future versions
|
|
478
592
|
state = await this.adapter.getState('collected.consumptionStart');
|
|
479
593
|
this.stateCache.set('collected.consumptionStart', state?.val, { type: 'number', stored: true });
|
|
480
594
|
state = await this.adapter.getState('collected.dailyExternalYield');
|
|
595
|
+
//riemann sum - set start value and timestamp for correct computation of daily yield
|
|
481
596
|
this.stateCache.set('collected.dailyExternalYield', state?.val, { type: 'number', stored: true });
|
|
482
597
|
this.externalSum.setStart(state?.val, state?.ts);
|
|
598
|
+
state = await this.adapter.getState('collected.sumDaily.houseConsumption');
|
|
599
|
+
this.stateCache.set('collected.sumDaily.houseConsumption', state?.val, { type: 'number', stored: true });
|
|
600
|
+
this.houseConsumSum.setStart(state?.val, state?.ts);
|
|
601
|
+
state = await this.adapter.getState('collected.sumDaily.activePower');
|
|
602
|
+
this.stateCache.set('collected.sumDaily.activePower', state?.val, { type: 'number', stored: true });
|
|
603
|
+
this.actPowerSum.setStart(state?.val, state?.ts);
|
|
604
|
+
state = await this.adapter.getState('collected.sumDaily.inputPower');
|
|
605
|
+
this.stateCache.set('collected.sumDaily.inputPower', state?.val, { type: 'number', stored: true });
|
|
606
|
+
this.inPowerSum.setStart(state?.val, state?.ts);
|
|
607
|
+
state = await this.adapter.getState('collected.sumDaily.chargePower');
|
|
608
|
+
this.stateCache.set('collected.sumDaily.chargePower', state?.val, { type: 'number', stored: true });
|
|
609
|
+
this.chargeSum.setStart(state?.val, state?.ts);
|
|
610
|
+
state = await this.adapter.getState('collected.sumDaily.dischargePower');
|
|
611
|
+
this.stateCache.set('collected.sumDaily.dischargePower', state?.val, { type: 'number', stored: true });
|
|
612
|
+
this.dischargeSum.setStart(state?.val, state?.ts);
|
|
613
|
+
state = await this.adapter.getState('collected.sumDaily.feed-inPower');
|
|
614
|
+
this.stateCache.set('collected.sumDaily.feed-inPower', state?.val, { type: 'number', stored: true });
|
|
615
|
+
this.feedinSum.setStart(state?.val, state?.ts);
|
|
616
|
+
state = await this.adapter.getState('collected.sumDaily.feed-outPower');
|
|
617
|
+
this.stateCache.set('collected.sumDaily.feed-outPower', state?.val, { type: 'number', stored: true });
|
|
618
|
+
this.feedoutSum.setStart(state?.val, state?.ts);
|
|
483
619
|
}
|
|
484
620
|
|
|
485
621
|
//state
|
|
@@ -552,6 +688,7 @@ class Registers {
|
|
|
552
688
|
this.externalSum.reset(); //reset for next day
|
|
553
689
|
this.stateCache.set('collected.dailyExternalYield', 0, { type: 'number' });
|
|
554
690
|
// copy consumption Sum to Start for the next day
|
|
691
|
+
// deprecated - consumption start is not used anymore, but we set it to the current consumption sum to avoid confusion
|
|
555
692
|
this.stateCache.set('collected.consumptionStart', this.stateCache.get('collected.consumptionSum')?.value ?? 0, { type: 'number' });
|
|
556
693
|
for (const device of this.adapter.devices) {
|
|
557
694
|
if (device.instance.mitnightProcess) {
|
|
@@ -560,6 +697,17 @@ class Registers {
|
|
|
560
697
|
}
|
|
561
698
|
this.storeStates();
|
|
562
699
|
}
|
|
700
|
+
|
|
701
|
+
destroy() {
|
|
702
|
+
for (const device of this.adapter.devices) {
|
|
703
|
+
if (typeof device.instance.destroy === 'function') {
|
|
704
|
+
device.instance.destroy();
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (typeof this.statistics.destroy === 'function') {
|
|
708
|
+
this.statistics.destroy();
|
|
709
|
+
}
|
|
710
|
+
}
|
|
563
711
|
}
|
|
564
712
|
|
|
565
713
|
module.exports = Registers;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* breakdownChartHelper.js
|
|
5
|
+
*
|
|
6
|
+
* Provides all eCharts-specific helpers related to the consumption breakdown feature.
|
|
7
|
+
*
|
|
8
|
+
* Responsibilities:
|
|
9
|
+
* - Building eCharts series objects for breakdown entries (stacked bars in the
|
|
10
|
+
* lower consumption grid: xAxisIndex 1 / yAxisIndex 2)
|
|
11
|
+
* - Merging breakdown series names into the chart legend array
|
|
12
|
+
* - Replacing breakdown data placeholders in chart template strings
|
|
13
|
+
* (%%targetPath%% → serialised data array)
|
|
14
|
+
*
|
|
15
|
+
* This module has no knowledge of ioBroker states, the stateCache, or scheduling.
|
|
16
|
+
* It receives everything it needs as arguments, which makes it easy to unit-test.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Constants
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Fallback colour palette applied when a breakdown entry does not specify a colour.
|
|
25
|
+
* Cycles through the list if more entries exist than colours.
|
|
26
|
+
*/
|
|
27
|
+
const DEFAULT_COLORS = ['#e91e63', '#9c27b0', '#673ab7', '#3f51b5', '#03a9f4', '#009688', '#8bc34a', '#ff5722', '#795548', '#607d8b'];
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Public API
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Builds an array of eCharts series configurations for the given breakdown entries.
|
|
35
|
+
*
|
|
36
|
+
* Each series is rendered as a stacked bar in the lower consumption sub-grid
|
|
37
|
+
* (xAxisIndex 1, yAxisIndex 2) with stack key "consumptionBreakdown", so all
|
|
38
|
+
* breakdown bars pile on top of one another.
|
|
39
|
+
*
|
|
40
|
+
* @param {import('./consumptionBreakdown').ConsumptionBreakdownEntry[]} entries
|
|
41
|
+
* The currently active breakdown entries (from ConsumptionBreakdown.entries).
|
|
42
|
+
* @param {string} [seriesType]
|
|
43
|
+
* eCharts series type passed in from the caller ('bar' or 'line').
|
|
44
|
+
* @param options - additional options to merge into each series config (e.g. for line styling)
|
|
45
|
+
* @param {string} valueFormatter string
|
|
46
|
+
* @returns {Array} eCharts series array (may be empty when entries is empty)
|
|
47
|
+
*/
|
|
48
|
+
function buildBreakdownSeries(entries, seriesType = 'bar', options = {}, valueFormatter) {
|
|
49
|
+
if (!entries || entries.length === 0) return [];
|
|
50
|
+
|
|
51
|
+
return entries.map((bd, idx) => {
|
|
52
|
+
const color = bd.color || DEFAULT_COLORS[idx % DEFAULT_COLORS.length];
|
|
53
|
+
//const unit = bd.unit || 'kWh';
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
name: bd.name,
|
|
57
|
+
type: seriesType,
|
|
58
|
+
stack: 'consumptionBreakdown', // same stack key as the Consumption series
|
|
59
|
+
xAxisIndex: 1,
|
|
60
|
+
yAxisIndex: 2,
|
|
61
|
+
data: `%%${bd.targetPath}%%`, // ensure it's an array (empty if undefined)
|
|
62
|
+
itemStyle: { color },
|
|
63
|
+
emphasis: { focus: 'series' },
|
|
64
|
+
//tooltip: { valueFormatter: value => `${value} ${unit}` },
|
|
65
|
+
tooltip: { valueFormatter: valueFormatter },
|
|
66
|
+
...options,
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Merges breakdown series names into an existing legend data array.
|
|
73
|
+
*
|
|
74
|
+
* Appends names that are not already present; mutates and returns the array.
|
|
75
|
+
*
|
|
76
|
+
* @param {string[]} legendData
|
|
77
|
+
* Existing legend labels (built-in series names).
|
|
78
|
+
* @param {import('./consumptionBreakdown').ConsumptionBreakdownEntry[]} entries
|
|
79
|
+
* Active breakdown entries whose names should appear in the legend.
|
|
80
|
+
* @returns {string[]} The same legendData array with breakdown names appended.
|
|
81
|
+
*/
|
|
82
|
+
function mergeBreakdownLegend(legendData, entries) {
|
|
83
|
+
if (!entries || entries.length === 0) return legendData;
|
|
84
|
+
|
|
85
|
+
for (const bd of entries) {
|
|
86
|
+
if (!legendData.includes(bd.name)) {
|
|
87
|
+
legendData.push(bd.name);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return legendData;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Replaces breakdown data placeholders inside a serialised chart string.
|
|
95
|
+
*
|
|
96
|
+
* The template may contain placeholder tokens of the form '%%targetPath%%'
|
|
97
|
+
* (with surrounding single-quotes, as produced by javascript-stringify).
|
|
98
|
+
* Each is replaced with the JSON-serialised numeric data array.
|
|
99
|
+
*
|
|
100
|
+
* This function is intentionally a string-level operation so it works both
|
|
101
|
+
* for custom user templates (loaded from the state) and for the built-in
|
|
102
|
+
* template produced by _buildDefaultTemplate().
|
|
103
|
+
*
|
|
104
|
+
* @param {string} chartStr
|
|
105
|
+
* The chart configuration as a raw string (output of stringify()).
|
|
106
|
+
* @param {import('./consumptionBreakdown').ConsumptionBreakdownEntry[]} entries
|
|
107
|
+
* Active breakdown entries.
|
|
108
|
+
* @param {Record<string, number[]>} seriesData
|
|
109
|
+
* Map of targetPath → data array.
|
|
110
|
+
* @returns {string} The chart string with placeholders replaced.
|
|
111
|
+
*/
|
|
112
|
+
function replacePlaceholders(chartStr, entries, seriesData) {
|
|
113
|
+
if (!entries || entries.length === 0) return chartStr;
|
|
114
|
+
|
|
115
|
+
for (const bd of entries) {
|
|
116
|
+
const placeholder = `'%%${bd.targetPath}%%'`;
|
|
117
|
+
const value = JSON.stringify(seriesData[bd.targetPath] ?? []);
|
|
118
|
+
chartStr = chartStr.replaceAll(placeholder, value);
|
|
119
|
+
}
|
|
120
|
+
return chartStr;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Exports
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
buildBreakdownSeries,
|
|
129
|
+
mergeBreakdownLegend,
|
|
130
|
+
replacePlaceholders,
|
|
131
|
+
};
|