iobroker.sigenergy 1.3.0 → 1.3.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 -0
- package/io-package.json +13 -13
- package/lib/modbus.js +11 -10
- package/lib/statistics.js +245 -196
- package/main.js +15 -4
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -156,6 +156,15 @@ MIT License — Copyright (c) 2025 ioBroker Community
|
|
|
156
156
|
---
|
|
157
157
|
|
|
158
158
|
## Changelog
|
|
159
|
+
### 1.3.4 (2026-03-13)
|
|
160
|
+
* (ssbingo) Fix CI: add mocha to devDependencies so test:package script can run
|
|
161
|
+
|
|
162
|
+
### 1.3.3 (2026-03-13)
|
|
163
|
+
* (ssbingo) Fix duplicate @param JSDoc warning caused by missing closing tag in modbus.js
|
|
164
|
+
|
|
165
|
+
### 1.3.2 (2026-03-13)
|
|
166
|
+
* (ssbingo) Code quality fixes: unused variables removed, JSDoc documentation completed, lexical declaration in switch-case fixed
|
|
167
|
+
|
|
159
168
|
### 1.3.0 (2026-03-13)
|
|
160
169
|
* (ssbingo) Multilingual documentation updated
|
|
161
170
|
|
package/io-package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "sigenergy",
|
|
4
|
-
"version": "1.3.
|
|
4
|
+
"version": "1.3.4",
|
|
5
5
|
"news": {
|
|
6
|
-
"1.3.
|
|
7
|
-
"en": "
|
|
8
|
-
"de": "
|
|
9
|
-
"ru": "
|
|
10
|
-
"pt": "
|
|
11
|
-
"nl": "
|
|
12
|
-
"fr": "
|
|
13
|
-
"it": "
|
|
14
|
-
"es": "
|
|
15
|
-
"pl": "
|
|
16
|
-
"uk": "
|
|
17
|
-
"zh-cn": "
|
|
6
|
+
"1.3.4": {
|
|
7
|
+
"en": "Fix CI: add mocha to devDependencies so test:package script can run",
|
|
8
|
+
"de": "CI-Fix: mocha zu devDependencies hinzugefügt, damit das test:package-Skript ausgeführt werden kann",
|
|
9
|
+
"ru": "Исправление CI: добавлен mocha в devDependencies для работы скрипта test:package",
|
|
10
|
+
"pt": "Correção CI: mocha adicionado às devDependencies para que o script test:package possa ser executado",
|
|
11
|
+
"nl": "CI-fix: mocha toegevoegd aan devDependencies zodat het test:package-script kan worden uitgevoerd",
|
|
12
|
+
"fr": "Correction CI : mocha ajouté aux devDependencies pour permettre l'exécution du script test:package",
|
|
13
|
+
"it": "Fix CI: mocha aggiunto alle devDependencies per consentire l'esecuzione dello script test:package",
|
|
14
|
+
"es": "Fix CI: mocha añadido a devDependencies para que el script test:package pueda ejecutarse",
|
|
15
|
+
"pl": "Poprawka CI: dodano mocha do devDependencies, aby skrypt test:package mógł działać",
|
|
16
|
+
"uk": "Виправлення CI: додано mocha до devDependencies для запуску скрипта test:package",
|
|
17
|
+
"zh-cn": "CI修复:将mocha添加到devDependencies以便test:package脚本可以运行"
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
20
|
"titleLang": {
|
package/lib/modbus.js
CHANGED
|
@@ -24,7 +24,7 @@ class ModbusConnection {
|
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Connect to Modbus device
|
|
27
|
-
* @returns {Promise<boolean>}
|
|
27
|
+
* @returns {Promise<boolean>} Resolves to true if connection succeeded, false otherwise
|
|
28
28
|
*/
|
|
29
29
|
async connect() {
|
|
30
30
|
if (this.connecting) return false;
|
|
@@ -83,7 +83,7 @@ class ModbusConnection {
|
|
|
83
83
|
if (this.client && this.client.isOpen) {
|
|
84
84
|
try {
|
|
85
85
|
await this.client.close();
|
|
86
|
-
} catch
|
|
86
|
+
} catch {
|
|
87
87
|
// ignore
|
|
88
88
|
}
|
|
89
89
|
}
|
|
@@ -96,7 +96,7 @@ class ModbusConnection {
|
|
|
96
96
|
* @param {number} slaveId - Slave address
|
|
97
97
|
* @param {number} address - Register start address
|
|
98
98
|
* @param {number} qty - Number of registers to read
|
|
99
|
-
* @returns {Promise<number[]>}
|
|
99
|
+
* @returns {Promise<number[]>} Resolves with an array of register values
|
|
100
100
|
*/
|
|
101
101
|
async readInputRegisters(slaveId, address, qty) {
|
|
102
102
|
if (!this.connected) {
|
|
@@ -112,7 +112,7 @@ class ModbusConnection {
|
|
|
112
112
|
* @param {number} slaveId - Slave address
|
|
113
113
|
* @param {number} address - Register start address
|
|
114
114
|
* @param {number} qty - Number of registers to read
|
|
115
|
-
* @returns {Promise<number[]>}
|
|
115
|
+
* @returns {Promise<number[]>} Resolves with an array of register values
|
|
116
116
|
*/
|
|
117
117
|
async readHoldingRegisters(slaveId, address, qty) {
|
|
118
118
|
if (!this.connected) {
|
|
@@ -128,7 +128,7 @@ class ModbusConnection {
|
|
|
128
128
|
* @param {number} slaveId - Slave address
|
|
129
129
|
* @param {number} address - Register address
|
|
130
130
|
* @param {number} value - Value to write
|
|
131
|
-
* @returns {Promise<void>}
|
|
131
|
+
* @returns {Promise<void>} Resolves when the register has been written
|
|
132
132
|
*/
|
|
133
133
|
async writeSingleRegister(slaveId, address, value) {
|
|
134
134
|
if (!this.connected) {
|
|
@@ -143,7 +143,7 @@ class ModbusConnection {
|
|
|
143
143
|
* @param {number} slaveId - Slave address
|
|
144
144
|
* @param {number} address - Start address
|
|
145
145
|
* @param {number[]} values - Values array
|
|
146
|
-
* @returns {Promise<void>}
|
|
146
|
+
* @returns {Promise<void>} Resolves when all registers have been written
|
|
147
147
|
*/
|
|
148
148
|
async writeMultipleRegisters(slaveId, address, values) {
|
|
149
149
|
if (!this.connected) {
|
|
@@ -155,7 +155,7 @@ class ModbusConnection {
|
|
|
155
155
|
|
|
156
156
|
/**
|
|
157
157
|
* Test connection - used from admin
|
|
158
|
-
* @returns {Promise<{success: boolean, message: string}>}
|
|
158
|
+
* @returns {Promise<{success: boolean, message: string}>} Result object with success flag and human-readable message
|
|
159
159
|
*/
|
|
160
160
|
async testConnection() {
|
|
161
161
|
try {
|
|
@@ -169,7 +169,7 @@ class ModbusConnection {
|
|
|
169
169
|
|
|
170
170
|
// Try reading plant system time as connectivity check
|
|
171
171
|
this.client.setID(247);
|
|
172
|
-
|
|
172
|
+
await this.client.readInputRegisters(30000, 2);
|
|
173
173
|
|
|
174
174
|
if (!wasConnected) {
|
|
175
175
|
await this.disconnect();
|
|
@@ -193,7 +193,7 @@ class ModbusConnection {
|
|
|
193
193
|
* @param {number[]} rawData - Raw register data
|
|
194
194
|
* @param {string} type - Data type (U16, S16, U32, S32, U64, STRING)
|
|
195
195
|
* @param {number|null} gain - Scaling factor
|
|
196
|
-
* @returns {number|string}
|
|
196
|
+
* @returns {number|string} Parsed and optionally scaled register value; string for STRING type
|
|
197
197
|
*/
|
|
198
198
|
static parseValue(rawData, type, gain) {
|
|
199
199
|
let raw;
|
|
@@ -221,7 +221,7 @@ class ModbusConnection {
|
|
|
221
221
|
rawData[2] * Math.pow(2, 16) +
|
|
222
222
|
rawData[3];
|
|
223
223
|
break;
|
|
224
|
-
case "STRING":
|
|
224
|
+
case "STRING": {
|
|
225
225
|
// Each register holds 2 ASCII chars
|
|
226
226
|
let str = "";
|
|
227
227
|
for (let i = 0; i < rawData.length; i++) {
|
|
@@ -231,6 +231,7 @@ class ModbusConnection {
|
|
|
231
231
|
if (low !== 0) str += String.fromCharCode(low);
|
|
232
232
|
}
|
|
233
233
|
return str.trim();
|
|
234
|
+
}
|
|
234
235
|
default:
|
|
235
236
|
raw = rawData[0];
|
|
236
237
|
}
|
package/lib/statistics.js
CHANGED
|
@@ -1,196 +1,245 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* StatisticsCalculator - Computes derived statistical values for Sigenergy inverters
|
|
5
|
-
* Includes house power, smoothing, and battery performance metrics.
|
|
6
|
-
*/
|
|
7
|
-
class StatisticsCalculator {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
this.
|
|
23
|
-
this.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* StatisticsCalculator - Computes derived statistical values for Sigenergy inverters
|
|
5
|
+
* Includes house power, smoothing, and battery performance metrics.
|
|
6
|
+
*/
|
|
7
|
+
class StatisticsCalculator {
|
|
8
|
+
/**
|
|
9
|
+
* Initialises internal history buffers and daily statistics tracking.
|
|
10
|
+
*/
|
|
11
|
+
constructor() {
|
|
12
|
+
this._history = {
|
|
13
|
+
essPower: [], // kW: >0 charging, <0 discharging
|
|
14
|
+
pvPower: [], // kW: PV generation
|
|
15
|
+
gridPower: [], // kW: >0 import, <0 export
|
|
16
|
+
housePower: [], // kW: Calculated consumption (smoothed)
|
|
17
|
+
soc: [], // %: Battery State of Charge
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// --- Smoothing & Filter Configuration ---
|
|
21
|
+
this._smoothingBuffer = [];
|
|
22
|
+
this._smoothingWindow = 5; // Average over the last 5 values
|
|
23
|
+
this._thresholdKW = 0.005; // Ignore noise below 5 Watts
|
|
24
|
+
|
|
25
|
+
this._maxHistory = 360; // 1 hour at 10s intervals
|
|
26
|
+
this._dayStart = this._getTodayStart();
|
|
27
|
+
this._dayStats = this._getDefaultDayStats();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns the Unix timestamp for the start of today (midnight).
|
|
32
|
+
* @returns {number} Milliseconds since epoch for today's midnight
|
|
33
|
+
*/
|
|
34
|
+
_getTodayStart() {
|
|
35
|
+
const d = new Date();
|
|
36
|
+
d.setHours(0, 0, 0, 0);
|
|
37
|
+
return d.getTime();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns a fresh default daily statistics object.
|
|
42
|
+
* @returns {{batteryFullTime: null, batteryEmptyTime: null, startSoc: null, maxSoc: number, minSoc: number, chargeStartTime: null}} Default day stats
|
|
43
|
+
*/
|
|
44
|
+
_getDefaultDayStats() {
|
|
45
|
+
return {
|
|
46
|
+
batteryFullTime: null,
|
|
47
|
+
batteryEmptyTime: null,
|
|
48
|
+
startSoc: null,
|
|
49
|
+
maxSoc: 0,
|
|
50
|
+
minSoc: 100,
|
|
51
|
+
chargeStartTime: null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Central calculation of house consumption with filtering and smoothing.
|
|
57
|
+
* Formula: PV + Grid - Battery
|
|
58
|
+
* @param {number} pv - PV generation power in kW
|
|
59
|
+
* @param {number} grid - Grid power in kW (positive = import, negative = export)
|
|
60
|
+
* @param {number} ess - Battery power in kW (positive = charging, negative = discharging)
|
|
61
|
+
* @returns {number} Smoothed house power in kW
|
|
62
|
+
*/
|
|
63
|
+
_calculateSmoothHousePower(pv, grid, ess) {
|
|
64
|
+
let rawPower = (pv || 0) + (grid || 0) - (ess || 0);
|
|
65
|
+
|
|
66
|
+
if (rawPower < this._thresholdKW) {
|
|
67
|
+
rawPower = 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this._smoothingBuffer.push(rawPower);
|
|
71
|
+
if (this._smoothingBuffer.length > this._smoothingWindow) {
|
|
72
|
+
this._smoothingBuffer.shift();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const sum = this._smoothingBuffer.reduce((a, b) => a + b, 0);
|
|
76
|
+
return sum / this._smoothingBuffer.length;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Ingests a new set of current measurements and updates internal history.
|
|
81
|
+
* Resets daily stats automatically on day change.
|
|
82
|
+
* @param {object} data - Current measurement values (pvPower, gridPower, essPower, soc, etc.)
|
|
83
|
+
*/
|
|
84
|
+
update(data) {
|
|
85
|
+
const now = Date.now();
|
|
86
|
+
|
|
87
|
+
const todayStart = this._getTodayStart();
|
|
88
|
+
if (todayStart !== this._dayStart) {
|
|
89
|
+
this._dayStart = todayStart;
|
|
90
|
+
this._dayStats = this._getDefaultDayStats();
|
|
91
|
+
this._smoothingBuffer = [];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (data.essPower !== undefined) this._history.essPower.push({ ts: now, v: data.essPower });
|
|
95
|
+
if (data.pvPower !== undefined) this._history.pvPower.push({ ts: now, v: data.pvPower });
|
|
96
|
+
if (data.gridPower !== undefined) this._history.gridPower.push({ ts: now, v: data.gridPower });
|
|
97
|
+
|
|
98
|
+
if (data.pvPower !== undefined && data.essPower !== undefined && data.gridPower !== undefined) {
|
|
99
|
+
const hp = this._calculateSmoothHousePower(data.pvPower, data.gridPower, data.essPower);
|
|
100
|
+
this._history.housePower.push({ ts: now, v: hp });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (data.soc !== undefined) {
|
|
104
|
+
const soc = data.soc;
|
|
105
|
+
this._history.soc.push({ ts: now, v: soc });
|
|
106
|
+
|
|
107
|
+
if (this._dayStats.startSoc === null) this._dayStats.startSoc = soc;
|
|
108
|
+
if (soc > this._dayStats.maxSoc) this._dayStats.maxSoc = soc;
|
|
109
|
+
if (soc < this._dayStats.minSoc) this._dayStats.minSoc = soc;
|
|
110
|
+
|
|
111
|
+
// Track daily battery metrics
|
|
112
|
+
if (soc >= 99.5 && this._dayStats.batteryFullTime === null) {
|
|
113
|
+
this._dayStats.batteryFullTime = now;
|
|
114
|
+
}
|
|
115
|
+
if (soc < 5 && this._dayStats.batteryEmptyTime === null) {
|
|
116
|
+
this._dayStats.batteryEmptyTime = now;
|
|
117
|
+
}
|
|
118
|
+
if (data.essPower > 0.1 && this._dayStats.chargeStartTime === null) {
|
|
119
|
+
this._dayStats.chargeStartTime = now;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
Object.keys(this._history).forEach(key => this._trimHistory(key));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Trims a history array to the configured maximum length.
|
|
128
|
+
* @param {string} key - Key of the history array to trim (e.g. "essPower")
|
|
129
|
+
*/
|
|
130
|
+
_trimHistory(key) {
|
|
131
|
+
if (this._history[key].length > this._maxHistory) {
|
|
132
|
+
this._history[key] = this._history[key].slice(-this._maxHistory);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// --- Battery Estimation Methods ---
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Estimates minutes until the battery is fully charged.
|
|
140
|
+
* @param {number} soc - Current state of charge in percent
|
|
141
|
+
* @param {number} essPower - Current charging power in kW (must be positive)
|
|
142
|
+
* @param {number} ratedCapacity - Rated battery capacity in kWh
|
|
143
|
+
* @returns {number|null} Minutes to full, or null if charging is not active / battery is full
|
|
144
|
+
*/
|
|
145
|
+
calcTimeToFull(soc, essPower, ratedCapacity) {
|
|
146
|
+
if (essPower <= 0.05 || soc >= 100 || !ratedCapacity) return null;
|
|
147
|
+
const remainingEnergy = ratedCapacity * (100 - soc) / 100;
|
|
148
|
+
return Math.round((remainingEnergy / essPower) * 60);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Estimates minutes of remaining battery discharge time.
|
|
153
|
+
* @param {number} soc - Current state of charge in percent
|
|
154
|
+
* @param {number} essPower - Current power in kW (must be negative for discharge)
|
|
155
|
+
* @param {number} ratedCapacity - Rated battery capacity in kWh
|
|
156
|
+
* @param {number} [cutoffSoc=10] - Minimum usable SoC threshold in percent
|
|
157
|
+
* @returns {number|null} Minutes remaining, or null if not discharging / already at cutoff
|
|
158
|
+
*/
|
|
159
|
+
calcTimeRemaining(soc, essPower, ratedCapacity, cutoffSoc = 10) {
|
|
160
|
+
if (essPower >= -0.05 || soc <= cutoffSoc || !ratedCapacity) return null;
|
|
161
|
+
const usableEnergy = ratedCapacity * (soc - cutoffSoc) / 100;
|
|
162
|
+
return Math.round((usableEnergy / Math.abs(essPower)) * 60);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Calculates how many minutes today the battery covered the load without grid import.
|
|
167
|
+
* @returns {number} Coverage duration in minutes
|
|
168
|
+
*/
|
|
169
|
+
calcBatteryCoverageToday() {
|
|
170
|
+
const gridHistory = this._history.gridPower.filter(e => e.ts >= this._dayStart);
|
|
171
|
+
const essHistory = this._history.essPower.filter(e => e.ts >= this._dayStart);
|
|
172
|
+
if (gridHistory.length < 2) return 0;
|
|
173
|
+
|
|
174
|
+
let coverageMs = 0;
|
|
175
|
+
for (let i = 1; i < gridHistory.length; i++) {
|
|
176
|
+
const dt = gridHistory[i].ts - gridHistory[i - 1].ts;
|
|
177
|
+
const gridVal = gridHistory[i].v;
|
|
178
|
+
// Find nearest ESS value
|
|
179
|
+
const essEntry = essHistory.reduce((prev, curr) =>
|
|
180
|
+
Math.abs(curr.ts - gridHistory[i].ts) < Math.abs(prev.ts - gridHistory[i].ts) ? curr : prev, essHistory[0]);
|
|
181
|
+
|
|
182
|
+
// Covered if battery is discharging and grid import is near zero
|
|
183
|
+
if (essEntry && essEntry.v < -0.05 && gridVal < 0.02) {
|
|
184
|
+
coverageMs += dt;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return Math.round(coverageMs / 60000);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Computes all configured statistics and returns them as a flat object.
|
|
192
|
+
* @param {object} cd - Current data snapshot (soc, essPower, pvPower, gridPower, ratedCapacity, cutoffSoc)
|
|
193
|
+
* @param {object} config - Adapter configuration flags (calcBatteryTimeToFull, calcAutarkyRate, etc.)
|
|
194
|
+
* @returns {object} Computed statistics keyed by state name
|
|
195
|
+
*/
|
|
196
|
+
getStats(cd, config) {
|
|
197
|
+
const stats = {};
|
|
198
|
+
const currentHousePower = this._smoothingBuffer.length > 0
|
|
199
|
+
? this._smoothingBuffer[this._smoothingBuffer.length - 1]
|
|
200
|
+
: 0;
|
|
201
|
+
|
|
202
|
+
// Battery Time Calculations
|
|
203
|
+
if (config.calcBatteryTimeToFull) {
|
|
204
|
+
stats.batteryTimeToFull = this.calcTimeToFull(cd.soc, cd.essPower, cd.ratedCapacity);
|
|
205
|
+
}
|
|
206
|
+
if (config.calcBatteryTimeRemaining) {
|
|
207
|
+
stats.batteryTimeRemaining = this.calcTimeRemaining(cd.soc, cd.essPower, cd.ratedCapacity, cd.cutoffSoc);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Daily Battery Statistics
|
|
211
|
+
if (config.calcBatteryDailyFull && this._dayStats.batteryFullTime) {
|
|
212
|
+
const start = this._dayStats.chargeStartTime || this._dayStart;
|
|
213
|
+
stats.batteryDailyChargeTime = Math.round((this._dayStats.batteryFullTime - start) / 60000);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (config.calcBatteryCoverageTime) {
|
|
217
|
+
stats.batteryCoverageToday = this.calcBatteryCoverageToday();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Efficiency Rates
|
|
221
|
+
if (config.calcSelfConsumptionRate) {
|
|
222
|
+
const export_ = cd.gridPower < 0 ? Math.abs(cd.gridPower) : 0;
|
|
223
|
+
const selfUsed = Math.max(0, (cd.pvPower || 0) - export_);
|
|
224
|
+
stats.selfConsumptionRate = cd.pvPower > 0.01
|
|
225
|
+
? Math.min(100, Math.round((selfUsed / cd.pvPower) * 1000) / 10)
|
|
226
|
+
: 0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (config.calcAutarkyRate) {
|
|
230
|
+
const gridImport = Math.max(0, cd.gridPower || 0);
|
|
231
|
+
const fromLocal = Math.max(0, currentHousePower - gridImport);
|
|
232
|
+
stats.autarkyRate = currentHousePower > 0.01
|
|
233
|
+
? Math.min(100, Math.round((fromLocal / currentHousePower) * 1000) / 10)
|
|
234
|
+
: 100;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
stats.housePower = currentHousePower;
|
|
238
|
+
stats.dayMaxSoc = this._dayStats.maxSoc;
|
|
239
|
+
stats.dayMinSoc = this._dayStats.minSoc;
|
|
240
|
+
|
|
241
|
+
return stats;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
module.exports = StatisticsCalculator;
|
package/main.js
CHANGED
|
@@ -25,12 +25,11 @@ const {
|
|
|
25
25
|
INVERTER_READ_REGISTERS,
|
|
26
26
|
DC_CHARGER_READ_REGISTERS,
|
|
27
27
|
AC_CHARGER_READ_REGISTERS,
|
|
28
|
-
RUNNING_STATES,
|
|
29
28
|
} = require("./lib/registers");
|
|
30
29
|
|
|
31
30
|
class Sigenergy extends utils.Adapter {
|
|
32
31
|
/**
|
|
33
|
-
* @param {Partial<utils.AdapterOptions>} [options]
|
|
32
|
+
* @param {Partial<utils.AdapterOptions>} [options] - Adapter options passed to the ioBroker core
|
|
34
33
|
*/
|
|
35
34
|
constructor(options) {
|
|
36
35
|
super({
|
|
@@ -280,6 +279,9 @@ class Sigenergy extends utils.Adapter {
|
|
|
280
279
|
/**
|
|
281
280
|
* Build optimized sequential read groups
|
|
282
281
|
* Groups consecutive registers to minimize read requests
|
|
282
|
+
* @param {object[]} registers - Array of register definitions to group
|
|
283
|
+
* @param {number} maxQty - Maximum number of registers allowed per read request
|
|
284
|
+
* @returns {object[]} Array of grouped register read requests
|
|
283
285
|
*/
|
|
284
286
|
_buildReadGroups(registers, maxQty) {
|
|
285
287
|
if (!registers || registers.length === 0) return [];
|
|
@@ -324,8 +326,11 @@ class Sigenergy extends utils.Adapter {
|
|
|
324
326
|
|
|
325
327
|
/**
|
|
326
328
|
* Process a read group and set states
|
|
329
|
+
* @param {object} group - A read group as produced by _buildReadGroups
|
|
330
|
+
* @param {number[]} rawData - Raw register data returned by the Modbus read
|
|
331
|
+
* @param {string} _section - Logical section name for logging (unused, reserved for future use)
|
|
327
332
|
*/
|
|
328
|
-
async _processReadGroup(group, rawData,
|
|
333
|
+
async _processReadGroup(group, rawData, _section) {
|
|
329
334
|
for (const reg of group.registers) {
|
|
330
335
|
try {
|
|
331
336
|
const slice = rawData.slice(reg.offset, reg.offset + reg.qty);
|
|
@@ -346,6 +351,8 @@ class Sigenergy extends utils.Adapter {
|
|
|
346
351
|
|
|
347
352
|
/**
|
|
348
353
|
* Store key values for statistics calculation
|
|
354
|
+
* @param {string} name - State ID of the register (e.g. "plant.essPower")
|
|
355
|
+
* @param {number|string} value - Parsed value of the register
|
|
349
356
|
*/
|
|
350
357
|
_storeCurrentData(name, value) {
|
|
351
358
|
const map = {
|
|
@@ -508,6 +515,8 @@ class Sigenergy extends utils.Adapter {
|
|
|
508
515
|
|
|
509
516
|
/**
|
|
510
517
|
* Handle state changes (for writable states)
|
|
518
|
+
* @param {string} id - Full state ID that changed
|
|
519
|
+
* @param {ioBroker.State | null | undefined} state - New state value, or null/undefined if deleted
|
|
511
520
|
*/
|
|
512
521
|
onStateChange(id, state) {
|
|
513
522
|
if (!state || state.ack) return;
|
|
@@ -517,6 +526,7 @@ class Sigenergy extends utils.Adapter {
|
|
|
517
526
|
|
|
518
527
|
/**
|
|
519
528
|
* Handle messages from admin (connection test, etc.)
|
|
529
|
+
* @param {ioBroker.Message} obj - Message object sent from admin or other adapters
|
|
520
530
|
*/
|
|
521
531
|
async onMessage(obj) {
|
|
522
532
|
if (!obj || !obj.command) return;
|
|
@@ -560,13 +570,14 @@ class Sigenergy extends utils.Adapter {
|
|
|
560
570
|
const { SerialPort } = require("serialport");
|
|
561
571
|
const ports = await SerialPort.list();
|
|
562
572
|
return ports.map(p => ({ value: p.path, label: `${p.path}${p.manufacturer ? ` (${p.manufacturer})` : ""}` }));
|
|
563
|
-
} catch
|
|
573
|
+
} catch {
|
|
564
574
|
return [];
|
|
565
575
|
}
|
|
566
576
|
}
|
|
567
577
|
|
|
568
578
|
/**
|
|
569
579
|
* Adapter shutdown
|
|
580
|
+
* @param {() => void} callback - Must be called after cleanup to signal shutdown completion
|
|
570
581
|
*/
|
|
571
582
|
async onUnload(callback) {
|
|
572
583
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.sigenergy",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
4
4
|
"description": "ioBroker adapter for Sigenergy solar energy systems via Modbus TCP/RTU",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "ssbingo",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"@iobroker/dev-server": "^0.8.0",
|
|
32
32
|
"@iobroker/testing": "^5.2.2",
|
|
33
33
|
"eslint": "^9.10.0",
|
|
34
|
+
"mocha": "^10.0.0",
|
|
34
35
|
"@alcalzone/release-script": "^5.1.1",
|
|
35
36
|
"@alcalzone/release-script-plugin-iobroker": "^5.1.2",
|
|
36
37
|
"@alcalzone/release-script-plugin-license": "^5.1.1",
|