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.
- package/README.md +15 -13
- package/admin/jsonConfig.json5 +33 -0
- package/io-package.json +39 -38
- package/lib/controls/inverter_service_queue.js +1 -1
- package/lib/drivers/driver_base.js +2 -1
- package/lib/drivers/driver_emma.js +14 -16
- package/lib/drivers/driver_inverter.js +70 -23
- package/lib/json_helper.js +3 -25
- package/lib/modbus/modbus_server.js +2 -2
- package/lib/register.js +178 -39
- package/lib/statistics/breakdownChartHelper.js +131 -0
- package/lib/statistics/consumptionBreakdown.js +304 -0
- package/lib/statistics/statistics.js +1458 -0
- package/lib/tools.js +24 -5
- package/main.js +18 -5
- package/package.json +12 -13
- package/lib/statistics.js +0 -1412
package/lib/json_helper.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Serializes an object to JSON, converting functions to their string representation.
|
|
5
5
|
*
|
|
6
|
-
* @param {
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
+
};
|