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 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.0",
4
+ "version": "1.3.4",
5
5
  "news": {
6
- "1.3.0": {
7
- "en": "Multilingual documentation updated",
8
- "de": "Dokumentation mehrsprachig angepasst",
9
- "ru": "Многоязычная документация обновлена",
10
- "pt": "Documentação multilingue atualizada",
11
- "nl": "Meertalige documentatie bijgewerkt",
12
- "fr": "Documentation multilingue mise à jour",
13
- "it": "Documentazione multilingue aggiornata",
14
- "es": "Documentación multilingüe actualizada",
15
- "pl": "Wielojęzyczna dokumentacja zaktualizowana",
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 (e) {
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
- const result = await this.client.readInputRegisters(30000, 2);
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
- constructor() {
9
- this._history = {
10
- essPower: [], // kW: >0 charging, <0 discharging
11
- pvPower: [], // kW: PV generation
12
- gridPower: [], // kW: >0 import, <0 export
13
- housePower: [], // kW: Calculated consumption (smoothed)
14
- soc: [], // %: Battery State of Charge
15
- };
16
-
17
- // --- Smoothing & Filter Configuration ---
18
- this._smoothingBuffer = [];
19
- this._smoothingWindow = 5; // Average over the last 5 values
20
- this._thresholdKW = 0.005; // Ignore noise below 5 Watts
21
-
22
- this._maxHistory = 360; // 1 hour at 10s intervals
23
- this._dayStart = this._getTodayStart();
24
- this._dayStats = this._getDefaultDayStats();
25
- }
26
-
27
- _getTodayStart() {
28
- const d = new Date();
29
- d.setHours(0, 0, 0, 0);
30
- return d.getTime();
31
- }
32
-
33
- _getDefaultDayStats() {
34
- return {
35
- batteryFullTime: null,
36
- batteryEmptyTime: null,
37
- startSoc: null,
38
- maxSoc: 0,
39
- minSoc: 100,
40
- chargeStartTime: null,
41
- };
42
- }
43
-
44
- /**
45
- * Central calculation of house consumption with filtering
46
- * Formula: PV + Grid - Battery
47
- */
48
- _calculateSmoothHousePower(pv, grid, ess) {
49
- let rawPower = (pv || 0) + (grid || 0) - (ess || 0);
50
-
51
- if (rawPower < this._thresholdKW) {
52
- rawPower = 0;
53
- }
54
-
55
- this._smoothingBuffer.push(rawPower);
56
- if (this._smoothingBuffer.length > this._smoothingWindow) {
57
- this._smoothingBuffer.shift();
58
- }
59
-
60
- const sum = this._smoothingBuffer.reduce((a, b) => a + b, 0);
61
- return sum / this._smoothingBuffer.length;
62
- }
63
-
64
- update(data) {
65
- const now = Date.now();
66
-
67
- const todayStart = this._getTodayStart();
68
- if (todayStart !== this._dayStart) {
69
- this._dayStart = todayStart;
70
- this._dayStats = this._getDefaultDayStats();
71
- this._smoothingBuffer = [];
72
- }
73
-
74
- if (data.essPower !== undefined) this._history.essPower.push({ ts: now, v: data.essPower });
75
- if (data.pvPower !== undefined) this._history.pvPower.push({ ts: now, v: data.pvPower });
76
- if (data.gridPower !== undefined) this._history.gridPower.push({ ts: now, v: data.gridPower });
77
-
78
- if (data.pvPower !== undefined && data.essPower !== undefined && data.gridPower !== undefined) {
79
- const hp = this._calculateSmoothHousePower(data.pvPower, data.gridPower, data.essPower);
80
- this._history.housePower.push({ ts: now, v: hp });
81
- }
82
-
83
- if (data.soc !== undefined) {
84
- const soc = data.soc;
85
- this._history.soc.push({ ts: now, v: soc });
86
-
87
- if (this._dayStats.startSoc === null) this._dayStats.startSoc = soc;
88
- if (soc > this._dayStats.maxSoc) this._dayStats.maxSoc = soc;
89
- if (soc < this._dayStats.minSoc) this._dayStats.minSoc = soc;
90
-
91
- // Track daily battery metrics
92
- if (soc >= 99.5 && this._dayStats.batteryFullTime === null) {
93
- this._dayStats.batteryFullTime = now;
94
- }
95
- if (soc < 5 && this._dayStats.batteryEmptyTime === null) {
96
- this._dayStats.batteryEmptyTime = now;
97
- }
98
- if (data.essPower > 0.1 && this._dayStats.chargeStartTime === null) {
99
- this._dayStats.chargeStartTime = now;
100
- }
101
- }
102
-
103
- Object.keys(this._history).forEach(key => this._trimHistory(key));
104
- }
105
-
106
- _trimHistory(key) {
107
- if (this._history[key].length > this._maxHistory) {
108
- this._history[key] = this._history[key].slice(-this._maxHistory);
109
- }
110
- }
111
-
112
- // --- Battery Estimation Methods ---
113
-
114
- calcTimeToFull(soc, essPower, ratedCapacity) {
115
- if (essPower <= 0.05 || soc >= 100 || !ratedCapacity) return null;
116
- const remainingEnergy = ratedCapacity * (100 - soc) / 100;
117
- return Math.round((remainingEnergy / essPower) * 60);
118
- }
119
-
120
- calcTimeRemaining(soc, essPower, ratedCapacity, cutoffSoc = 10) {
121
- if (essPower >= -0.05 || soc <= cutoffSoc || !ratedCapacity) return null;
122
- const usableEnergy = ratedCapacity * (soc - cutoffSoc) / 100;
123
- return Math.round((usableEnergy / Math.abs(essPower)) * 60);
124
- }
125
-
126
- calcBatteryCoverageToday() {
127
- const gridHistory = this._history.gridPower.filter(e => e.ts >= this._dayStart);
128
- const essHistory = this._history.essPower.filter(e => e.ts >= this._dayStart);
129
- if (gridHistory.length < 2) return 0;
130
-
131
- let coverageMs = 0;
132
- for (let i = 1; i < gridHistory.length; i++) {
133
- const dt = gridHistory[i].ts - gridHistory[i - 1].ts;
134
- const gridVal = gridHistory[i].v;
135
- // Find nearest ESS value
136
- const essEntry = essHistory.reduce((prev, curr) =>
137
- Math.abs(curr.ts - gridHistory[i].ts) < Math.abs(prev.ts - gridHistory[i].ts) ? curr : prev, essHistory[0]);
138
-
139
- // Covered if battery is discharging and grid import is near zero
140
- if (essEntry && essEntry.v < -0.05 && gridVal < 0.02) {
141
- coverageMs += dt;
142
- }
143
- }
144
- return Math.round(coverageMs / 60000);
145
- }
146
-
147
- getStats(cd, config) {
148
- const stats = {};
149
- const currentHousePower = this._smoothingBuffer.length > 0
150
- ? this._smoothingBuffer[this._smoothingBuffer.length - 1]
151
- : 0;
152
-
153
- // Battery Time Calculations
154
- if (config.calcBatteryTimeToFull) {
155
- stats.batteryTimeToFull = this.calcTimeToFull(cd.soc, cd.essPower, cd.ratedCapacity);
156
- }
157
- if (config.calcBatteryTimeRemaining) {
158
- stats.batteryTimeRemaining = this.calcTimeRemaining(cd.soc, cd.essPower, cd.ratedCapacity, cd.cutoffSoc);
159
- }
160
-
161
- // Daily Battery Statistics
162
- if (config.calcBatteryDailyFull && this._dayStats.batteryFullTime) {
163
- const start = this._dayStats.chargeStartTime || this._dayStart;
164
- stats.batteryDailyChargeTime = Math.round((this._dayStats.batteryFullTime - start) / 60000);
165
- }
166
-
167
- if (config.calcBatteryCoverageTime) {
168
- stats.batteryCoverageToday = this.calcBatteryCoverageToday();
169
- }
170
-
171
- // Efficiency Rates
172
- if (config.calcSelfConsumptionRate) {
173
- const export_ = cd.gridPower < 0 ? Math.abs(cd.gridPower) : 0;
174
- const selfUsed = Math.max(0, (cd.pvPower || 0) - export_);
175
- stats.selfConsumptionRate = cd.pvPower > 0.01
176
- ? Math.min(100, Math.round((selfUsed / cd.pvPower) * 1000) / 10)
177
- : 0;
178
- }
179
-
180
- if (config.calcAutarkyRate) {
181
- const gridImport = Math.max(0, cd.gridPower || 0);
182
- const fromLocal = Math.max(0, currentHousePower - gridImport);
183
- stats.autarkyRate = currentHousePower > 0.01
184
- ? Math.min(100, Math.round((fromLocal / currentHousePower) * 1000) / 10)
185
- : 100;
186
- }
187
-
188
- stats.housePower = currentHousePower;
189
- stats.dayMaxSoc = this._dayStats.maxSoc;
190
- stats.dayMinSoc = this._dayStats.minSoc;
191
-
192
- return stats;
193
- }
194
- }
195
-
196
- module.exports = StatisticsCalculator;
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, section) {
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 (e) {
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.0",
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",