iobroker.sun2000 2.4.5 → 2.5.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.
@@ -3,7 +3,7 @@
3
3
  /**
4
4
  * Serializes an object to JSON, converting functions to their string representation.
5
5
  *
6
- * @param {any} obj - The object to serialize.
6
+ * @param {object} obj - The object to serialize.
7
7
  * @returns {string} JSON string where functions are represented as strings.
8
8
  */
9
9
  function stringifyWithFunctions(obj) {
@@ -22,30 +22,8 @@ function stringifyWithFunctions(obj) {
22
22
  2,
23
23
  );
24
24
  }
25
- /*
26
- const FUNCTION_KEYS = ['formatter', 'render', 'parser'];
27
25
 
28
- function reviveFunctions(obj, key = '') {
29
- if (typeof obj === 'string' && FUNCTION_KEYS.includes(key)) {
30
- const str = obj.trim();
31
-
32
- try {
33
- return new Function(`return (${str})`)();
34
- } catch {
35
- return obj;
36
- }
37
- }
38
-
39
- if (typeof obj === 'object' && obj !== null) {
40
- for (const k in obj) {
41
- obj[k] = reviveFunctions(obj[k], k);
42
- }
43
- }
44
-
45
- return obj;
46
- }
47
- */
48
- const FUNCTION_KEYS = ['formatter', 'parser', 'render', 'labelFormatter', 'valueFormatter', 'position'];
26
+ const FUNCTION_KEYS = ['formatter', 'parser', 'render', 'labelFormatter', 'valueFormatter', 'position', 'interval'];
49
27
 
50
28
  function reviveFunctions(obj, key = '') {
51
29
  if (typeof obj === 'string' && FUNCTION_KEYS.includes(key)) {
@@ -109,7 +87,7 @@ function deepMerge(target, source) {
109
87
  *
110
88
  * @param {object} obj - The object to modify.
111
89
  * @param {string} path - Dot‑separated path (e.g., "a.b.c").
112
- * @param {any} value - The value to set at the specified path.
90
+ * @param {string} value - The value to set at the specified path.
113
91
  */
114
92
  function setByPath(obj, path, value) {
115
93
  const keys = path.split('.');
@@ -194,7 +194,7 @@ class ModbusServer {
194
194
  * @param {number} startAddr The start address of the register.
195
195
  * @param {number} length The length of the register.
196
196
  * @param {number} unitId The modbus unitId.
197
- * @param {Function} callback The callback function.
197
+ * @param {callback} callback The callback function.
198
198
  * @returns {Promise<void>}
199
199
  */
200
200
 
@@ -280,7 +280,7 @@ class ModbusServer {
280
280
  }
281
281
 
282
282
  wait(ms) {
283
- return new Promise(resolve => setTimeout(resolve, ms));
283
+ return new Promise(resolve => this.adapter.setTimeout(resolve, ms));
284
284
  }
285
285
  }
286
286
 
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,8 +12,17 @@ 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
 
25
+ /*
17
26
  for (const device of this.adapter.devices) {
18
27
  //DriverInfo Instance or Sdongle
19
28
  const handler = getDriverHandler(device.driverClass);
@@ -21,6 +30,7 @@ class Registers {
21
30
  device.instance = new handler(this, device);
22
31
  }
23
32
  }
33
+ */
24
34
 
25
35
  //Upgrade to v2.3.7 - deleted deprecated states
26
36
  if (
@@ -34,9 +44,84 @@ class Registers {
34
44
  }
35
45
  })
36
46
  );
47
+ //Upgrade to v2.5.0 - deleted deprecated states
48
+ /*
49
+ if (
50
+ tools.existsState(this.adapter, `collected.consumptionStart`, (err, exists) => {
51
+ if (!err && exists) {
52
+ tools.deleteState(this.adapter, `collected.consumptionStart`, (err, deleted) => {
53
+ if (!err && deleted) {
54
+ this.adapter.logger.debug('Deleted deprecated state collected.consumptionStart');
55
+ }
56
+ });
57
+ }
58
+ })
59
+ );
60
+ */
37
61
 
38
62
  //this.postProcessHooks = [];
39
63
  this.postProcessHooks = [
64
+ {
65
+ refresh: dataRefreshRate.high,
66
+ states: [
67
+ {
68
+ id: 'collected.sumDaily.houseConsumption',
69
+ name: 'daily sum of House consumption',
70
+ type: 'number',
71
+ unit: 'kWh',
72
+ role: 'value.power.consumed',
73
+ desc: 'riemann sum of Load power',
74
+ },
75
+ {
76
+ id: 'collected.sumDaily.activePower',
77
+ name: 'daily sum of Active power',
78
+ type: 'number',
79
+ unit: 'kWh',
80
+ role: 'value.power.produced',
81
+ desc: 'riemann sum of Power currently used',
82
+ },
83
+ {
84
+ id: 'collected.sumDaily.inputPower',
85
+ name: 'daily sum of Input power',
86
+ type: 'number',
87
+ unit: 'kWh',
88
+ role: 'value.power.produced',
89
+ desc: 'riemann sum of Power from solar',
90
+ },
91
+ {
92
+ id: 'collected.sumDaily.chargePower',
93
+ name: 'daily sum of Charge power',
94
+ type: 'number',
95
+ unit: 'kWh',
96
+ role: 'value.power.consumed',
97
+ desc: 'riemann sum of Charge power',
98
+ },
99
+ {
100
+ id: 'collected.sumDaily.dischargePower',
101
+ name: 'daily sum of Discharge power',
102
+ type: 'number',
103
+ unit: 'kWh',
104
+ role: 'value.power.produced',
105
+ desc: 'riemann sum of Discharge power',
106
+ },
107
+ {
108
+ id: 'collected.sumDaily.feed-inPower',
109
+ name: 'daily sum of Feed-in power',
110
+ type: 'number',
111
+ unit: 'kWh',
112
+ role: 'value.power.consumed',
113
+ desc: 'riemann sum of Feed-in power',
114
+ },
115
+ {
116
+ id: 'collected.sumDaily.feed-outPower',
117
+ name: 'daily sum of Feed-out power',
118
+ type: 'number',
119
+ unit: 'kWh',
120
+ role: 'value.power.produced',
121
+ desc: 'riemann sum of Feed-out power',
122
+ },
123
+ ],
124
+ },
40
125
  {
41
126
  refresh: dataRefreshRate.high,
42
127
  states: [
@@ -175,6 +260,23 @@ class Registers {
175
260
  if (houseConsum < 0) {
176
261
  houseConsum = 0;
177
262
  }
263
+
264
+ //dailySum
265
+ this.inPowerSum.add(inPower);
266
+ this.actPowerSum.add(actPower);
267
+ this.chargeSum.add(chargeDischarge > 0 ? chargeDischarge : 0);
268
+ this.dischargeSum.add(chargeDischarge < 0 ? -chargeDischarge : 0);
269
+ this.houseConsumSum.add(houseConsum);
270
+ this.feedinSum.add(feedinPower > 0 ? feedinPower : 0);
271
+ this.feedoutSum.add(feedinPower < 0 ? -feedinPower : 0);
272
+ this.stateCache.set('collected.sumDaily.houseConsumption', this.houseConsumSum.sum, { type: 'number' });
273
+ this.stateCache.set('collected.sumDaily.activePower', this.actPowerSum.sum, { type: 'number' });
274
+ this.stateCache.set('collected.sumDaily.inputPower', this.inPowerSum.sum, { type: 'number' });
275
+ this.stateCache.set('collected.sumDaily.chargePower', this.chargeSum.sum, { type: 'number' });
276
+ this.stateCache.set('collected.sumDaily.dischargePower', this.dischargeSum.sum, { type: 'number' });
277
+ this.stateCache.set('collected.sumDaily.feed-inPower', this.feedinSum.sum, { type: 'number' });
278
+ this.stateCache.set('collected.sumDaily.feed-outPower', this.feedoutSum.sum, { type: 'number' });
279
+
178
280
  //Überschuss (Differenz)
179
281
  const surplusArray = calcUsableSurplus.bind(this)();
180
282
 
@@ -203,7 +305,7 @@ class Registers {
203
305
  name: 'Daily energy yield',
204
306
  type: 'number',
205
307
  unit: 'kWh',
206
- role: 'value.power.consumption',
308
+ role: 'value.power.produced',
207
309
  desc: 'daily energy yield of the inverters',
208
310
  },
209
311
  {
@@ -211,7 +313,7 @@ class Registers {
211
313
  name: 'Daily input yield',
212
314
  type: 'number',
213
315
  unit: 'kWh',
214
- role: 'value.power.consumption',
316
+ role: 'value.power.produced',
215
317
  desc: 'yield from the portal',
216
318
  },
217
319
  {
@@ -219,32 +321,38 @@ class Registers {
219
321
  name: 'Daily solar yield',
220
322
  type: 'number',
221
323
  unit: 'kWh',
222
- role: 'value.power.consumption',
324
+ role: 'value.power.produced',
223
325
  desc: 'Riemann sum of input power with efficiency loss',
224
326
  },
225
- { id: 'collected.accumulatedEnergyYield', name: 'Accumulated energy yield', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
226
- { id: 'collected.consumptionSum', name: 'Consumption sum', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
227
- { id: 'collected.gridExportStart', name: 'Grid export start today', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
228
- { id: 'collected.gridImportStart', name: 'Grid import start today', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
229
- { id: 'collected.consumptionStart', name: 'Consumption start today', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
230
- { id: 'collected.gridExportToday', name: 'Grid export today', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
231
- { id: 'collected.gridImportToday', name: 'Grid import today', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
232
- { id: 'collected.consumptionToday', name: 'Consumption today', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
233
- { id: 'collected.totalCharge', name: 'Total charge of battery', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
234
- { id: 'collected.totalDischarge', name: 'Total discharge of battery', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
327
+ { id: 'collected.accumulatedEnergyYield', name: 'Accumulated energy yield', type: 'number', unit: 'kWh', role: 'value.power.produced' },
328
+ { id: 'collected.consumptionSum', name: 'Consumption sum', type: 'number', unit: 'kWh', role: 'value.power.consumed' },
329
+ { id: 'collected.gridExportStart', name: 'Grid export start today', type: 'number', unit: 'kWh', role: 'value.power.produced' },
330
+ { id: 'collected.gridImportStart', name: 'Grid import start today', type: 'number', unit: 'kWh', role: 'value.power.consumed' },
331
+ {
332
+ id: 'collected.consumptionStart',
333
+ name: 'deprecated - Consumption start today',
334
+ type: 'number',
335
+ unit: 'kWh',
336
+ role: 'value.power.consumed',
337
+ },
338
+ { id: 'collected.gridExportToday', name: 'Grid export today', type: 'number', unit: 'kWh', role: 'value.power.produced' },
339
+ { id: 'collected.gridImportToday', name: 'Grid import today', type: 'number', unit: 'kWh', role: 'value.power.consumed' },
340
+ { id: 'collected.consumptionToday', name: 'Consumption today', type: 'number', unit: 'kWh', role: 'value.power.consumed' },
341
+ { id: 'collected.totalCharge', name: 'Total charge of battery', type: 'number', unit: 'kWh', role: 'value.power.produced' },
342
+ { id: 'collected.totalDischarge', name: 'Total discharge of battery', type: 'number', unit: 'kWh', role: 'value.power.produced' },
235
343
  {
236
344
  id: 'collected.currentDayChargeCapacity',
237
345
  name: 'Current day charge capacity of battery',
238
346
  type: 'number',
239
347
  unit: 'kWh',
240
- role: 'value.power.consumption',
348
+ role: 'value.power.consumed',
241
349
  },
242
350
  {
243
351
  id: 'collected.currentDayDischargeCapacity',
244
352
  name: 'Current day discharge capacity of battery',
245
353
  type: 'number',
246
354
  unit: 'kWh',
247
- role: 'value.power.consumption',
355
+ role: 'value.power.produced',
248
356
  desc: '',
249
357
  },
250
358
  { id: 'collected.SOC', name: 'State of battery capacity', type: 'number', unit: '%', role: 'value.battery', desc: 'SOC' },
@@ -254,19 +362,9 @@ class Registers {
254
362
  name: 'daily external yield',
255
363
  type: 'number',
256
364
  unit: 'kWh',
257
- role: 'value.power.consumption',
365
+ role: 'value.power.produced',
258
366
  desc: 'Riemann sum of external power',
259
367
  },
260
- /*
261
- {
262
- id: 'collected.dailyActiveEnergy',
263
- name: 'Active Energy today',
264
- type: 'number',
265
- unit: 'kWh',
266
- role: 'value.power.consumption',
267
- desc: 'Amount of Riemann sum of sum of active power',
268
- },
269
- */
270
368
  ],
271
369
  fn: inverters => {
272
370
  let inYield = 0;
@@ -280,17 +378,20 @@ class Registers {
280
378
  let totalCharge = 0;
281
379
  let ratedCap = 0;
282
380
  let load = 0;
283
- let feedinEnergy;
284
- let supplyFromGrid;
381
+ let ready = false;
285
382
 
286
383
  for (const inverter of inverters) {
287
384
  if (inverter.driverClass != driverClasses.inverter) {
288
385
  continue;
289
386
  }
387
+ if (this.stateCache.get(`${inverter.path}.accumulatedEnergyYield`)?.value === undefined) {
388
+ continue; //wait until value is available - otherwise we would overwrite the value from the portal with 0
389
+ }
390
+ ready = true;
290
391
  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;
392
+ inYield += this.stateCache.get(`${inverter.path}.derived.dailyInputYield`)?.value ?? 0;
393
+ solarYield += this.stateCache.get(`${inverter.path}.derived.dailySolarYield`)?.value ?? 0;
394
+ enYield += this.stateCache.get(`${inverter.path}.accumulatedEnergyYield`)?.value ?? 0;
294
395
  activeEnergy += this.stateCache.get(`${inverter.path}.derived.dailyActiveEnergy`)?.value ?? 0;
295
396
  if (this.stateCache.get(`${inverter.path}.battery.ratedCapacity`)?.value > 0) {
296
397
  charge += this.stateCache.get(`${inverter.path}.battery.currentDayChargeCapacity`)?.value ?? 0;
@@ -300,9 +401,12 @@ class Registers {
300
401
  load +=
301
402
  this.stateCache.get(`${inverter.path}.battery.ratedCapacity`)?.value *
302
403
  this.stateCache.get(`${inverter.path}.battery.SOC`)?.value;
303
- ratedCap += this.stateCache.get(`${inverter.path}.battery.ratedCapacity`)?.value;
404
+ ratedCap += this.stateCache.get(`${inverter.path}.battery.ratedCapacity`)?.value ?? 0;
304
405
  }
305
406
  }
407
+ if (!ready) {
408
+ return; //no inverter ready yet - do not set values
409
+ }
306
410
  //this.stateCache.set('collected.dailyActiveEnergy', activeEnergy, { type: 'number' });
307
411
  this.stateCache.set('collected.dailyExternalYield', this.externalSum.sum, { type: 'number' }); //of externalPower
308
412
  this.stateCache.set('collected.dailyEnergyYield', outYield, { type: 'number' });
@@ -310,9 +414,10 @@ class Registers {
310
414
  this.stateCache.set('collected.dailySolarYield', solarYield, { type: 'number' });
311
415
  this.stateCache.set('collected.accumulatedEnergyYield', enYield, { type: 'number' });
312
416
  const sign = this.stateCache.get('meter.derived.signConventionForPowerFeed-in')?.value ?? 1;
417
+ let feedinEnergy;
418
+ let supplyFromGrid;
313
419
  if (sign === -1) {
314
420
  //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
421
  feedinEnergy = this.stateCache.get('meter.reverseActiveEnergy')?.value ?? 0;
317
422
  supplyFromGrid = this.stateCache.get('meter.positiveActiveEnergy')?.value ?? 0;
318
423
  } else {
@@ -354,7 +459,7 @@ class Registers {
354
459
  this.stateCache.set('collected.currentDayChargeCapacity', charge, { type: 'number' });
355
460
  this.stateCache.set('collected.currentDayDischargeCapacity', disCharge, { type: 'number' });
356
461
  this.stateCache.set('collected.ratedCapacity', ratedCap, { type: 'number' });
357
- this.stateCache.set('collected.SOC', Math.round(load / ratedCap), { type: 'number' });
462
+ this.stateCache.set('collected.SOC', ratedCap ? Math.round(load / ratedCap) : 0, { type: 'number' });
358
463
  },
359
464
  },
360
465
  ];
@@ -397,15 +502,14 @@ class Registers {
397
502
 
398
503
  async storeStates() {
399
504
  for (const stateEntry of this.stateCache.values()) {
400
- //if (stateEntry?.storeType === storeType.never) continue;
401
505
  if (stateEntry.stored) {
402
506
  continue;
403
507
  }
404
- //if (stateEntry?.storeType !== storeType.always && stateEntry.stored) continue;
405
508
  if (stateEntry.value !== null) {
406
509
  try {
407
510
  stateEntry.stored = true;
408
- await this.adapter.setState(stateEntry.id, { val: stateEntry.value, ack: true });
511
+ const valueToStore = typeof stateEntry.value === 'number' ? this.stateCache.round(stateEntry.value, 3) : stateEntry.value;
512
+ await this.adapter.setState(stateEntry.id, { val: valueToStore, ack: true });
409
513
  this.adapter.logger.debug(`Fetched ${stateEntry.id}, val=${stateEntry.value}`);
410
514
  } catch (err) {
411
515
  stateEntry.stored = false;
@@ -430,7 +534,7 @@ class Registers {
430
534
  * @returns {Promise<number>} - Returns a promise that resolves to the number of states updated.
431
535
  */
432
536
  async updateStates(device, modbusClient, refreshRate, duration) {
433
- //this.adapter.log.debug('### DeviceInfo: '+device.index+' '+JSON.stringify(device.instance.info));
537
+ //this.adapter.log.debug(`### DeviceInfo: ${device.index} ${JSON.stringify(device.instance?.info)}`);
434
538
  if (!device.instance) {
435
539
  const handler = getDriverHandler(device.driverClass);
436
540
  if (handler) {
@@ -475,11 +579,34 @@ class Registers {
475
579
  this.stateCache.set('collected.gridImportStart', state?.val, { type: 'number', stored: true });
476
580
  state = await this.adapter.getState('collected.gridImportStart');
477
581
  this.stateCache.set('collected.gridImportStart', state?.val, { type: 'number', stored: true });
582
+ //deprecated - only for compatibility reasons - will be removed in future versions
478
583
  state = await this.adapter.getState('collected.consumptionStart');
479
584
  this.stateCache.set('collected.consumptionStart', state?.val, { type: 'number', stored: true });
480
585
  state = await this.adapter.getState('collected.dailyExternalYield');
586
+ //riemann sum - set start value and timestamp for correct computation of daily yield
481
587
  this.stateCache.set('collected.dailyExternalYield', state?.val, { type: 'number', stored: true });
482
588
  this.externalSum.setStart(state?.val, state?.ts);
589
+ state = await this.adapter.getState('collected.sumDaily.houseConsumption');
590
+ this.stateCache.set('collected.sumDaily.houseConsumption', state?.val, { type: 'number', stored: true });
591
+ this.houseConsumSum.setStart(state?.val, state?.ts);
592
+ state = await this.adapter.getState('collected.sumDaily.activePower');
593
+ this.stateCache.set('collected.sumDaily.activePower', state?.val, { type: 'number', stored: true });
594
+ this.actPowerSum.setStart(state?.val, state?.ts);
595
+ state = await this.adapter.getState('collected.sumDaily.inputPower');
596
+ this.stateCache.set('collected.sumDaily.inputPower', state?.val, { type: 'number', stored: true });
597
+ this.inPowerSum.setStart(state?.val, state?.ts);
598
+ state = await this.adapter.getState('collected.sumDaily.chargePower');
599
+ this.stateCache.set('collected.sumDaily.chargePower', state?.val, { type: 'number', stored: true });
600
+ this.chargeSum.setStart(state?.val, state?.ts);
601
+ state = await this.adapter.getState('collected.sumDaily.dischargePower');
602
+ this.stateCache.set('collected.sumDaily.dischargePower', state?.val, { type: 'number', stored: true });
603
+ this.dischargeSum.setStart(state?.val, state?.ts);
604
+ state = await this.adapter.getState('collected.sumDaily.feed-inPower');
605
+ this.stateCache.set('collected.sumDaily.feed-inPower', state?.val, { type: 'number', stored: true });
606
+ this.feedinSum.setStart(state?.val, state?.ts);
607
+ state = await this.adapter.getState('collected.sumDaily.feed-outPower');
608
+ this.stateCache.set('collected.sumDaily.feed-outPower', state?.val, { type: 'number', stored: true });
609
+ this.feedoutSum.setStart(state?.val, state?.ts);
483
610
  }
484
611
 
485
612
  //state
@@ -552,6 +679,7 @@ class Registers {
552
679
  this.externalSum.reset(); //reset for next day
553
680
  this.stateCache.set('collected.dailyExternalYield', 0, { type: 'number' });
554
681
  // copy consumption Sum to Start for the next day
682
+ // deprecated - consumption start is not used anymore, but we set it to the current consumption sum to avoid confusion
555
683
  this.stateCache.set('collected.consumptionStart', this.stateCache.get('collected.consumptionSum')?.value ?? 0, { type: 'number' });
556
684
  for (const device of this.adapter.devices) {
557
685
  if (device.instance.mitnightProcess) {
@@ -560,6 +688,17 @@ class Registers {
560
688
  }
561
689
  this.storeStates();
562
690
  }
691
+
692
+ destroy() {
693
+ for (const device of this.adapter.devices) {
694
+ if (typeof device.instance.destroy === 'function') {
695
+ device.instance.destroy();
696
+ }
697
+ }
698
+ if (typeof this.statistics.destroy === 'function') {
699
+ this.statistics.destroy();
700
+ }
701
+ }
563
702
  }
564
703
 
565
704
  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
+ };