iobroker.sigenergy 3.2.0 → 3.3.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 CHANGED
@@ -255,6 +255,19 @@ Status and power readings for the DC charger.
255
255
 
256
256
  ## Changelog
257
257
 
258
+ ### 3.3.1 (2026-09-07)
259
+ - (ssbingo) fix: registers the device marks as not valid are no longer turned into plausible looking measurements. The protocol signals this by setting all bits ("Range:[0, 0xFFFFFFFE]. With value 0xFFFFFFFF, register is not valid.") and a device answers that way for a register that does not currently apply or that its firmware does not implement. The raw sentinel was scaled by the register gain, so it arrived as a reading that looks real: observed on a SigenStor EC 10.0 TP **with** a DC charger installed, `dcCharger.runningState` = 65535 (Appendix 14 only defines 0x00-0x0A), `dcCharger.dischargingCurrent` = 6553.5 A, `dcCharger.currentDischargingCapacity` = 42949672.95 kWh and `dcCharger.currentDischargingDuration` = 4294967295 s, while the neighbouring DC charger registers (rated power, PV generation, totals) returned real values. The same pattern appears outside the DC charger, e.g. `plant.currentCtrlCmdValue` = 655.35 %, `plant.mergedAlarm7` = 65535, `inverter.essMaxBatteryCellVoltage` = 65.535 V and the ESS/PCS/grid limit registers as 4294967.295 kW. Such registers now report no value instead
260
+ - (ssbingo) fix: a register that reports no value no longer feeds the statistics calculation, which keeps the last known good reading instead
261
+
262
+ ### 3.3.0 (2026-09-07)
263
+ - (ssbingo) fix: when the device rejects a grouped register read with a Modbus exception, the registers of that group are now read one by one and only the register(s) the device does not implement are excluded from further polling. Previously a single unsupported register silently disabled its whole group for the rest of the adapter runtime, e.g. `dcCharger.runningState` (31513) never received a value when the discharging registers 31514–31518 of the same group were rejected
264
+ - (ssbingo) feat: new state `info.protocolVersion` (number, e.g. `2.9`) next to the textual `info.protocolLevel`, so widgets and scripts can adapt to the detected protocol version without parsing strings
265
+ - (ssbingo) feat: DC charger registers carry the protocol version that introduced them (`since`, from the V2.9 revision history: 31509–31511 V2.6, 31513 V2.8, 31514–31525 V2.9); registers newer than the detected protocol level are not requested, so a V2.8 device no longer gets its DC charger group rejected because of the V2.9 discharging registers
266
+ - (ssbingo) chore: grouped register reading moved to `lib/readGroups.js` and covered by unit tests
267
+ - (ssbingo) feat: all registers dated by the V2.9 revision history now carry `since` via a central `PROTOCOL_SINCE` table in `lib/registers.js` (plant 30088–30192 V2.6, 30194–30268 V2.7, 30276–30285 V2.8, 30272/30274/30286 V2.9, 40046–40048 V2.6, 40049–40068 V2.8, 40157–40159 V2.9, ESS preheating 50000–50183 V2.9; inverter 30613–30619 V2.6, 31066–31105 V2.8; DC charger 41002/41004 V2.9; all PSS and PID registers V2.9) and are skipped on devices with an older protocol level
268
+ - (ssbingo) fix: the one-time control register read (FC03) and the SigenMicro reader use the same per-register fallback as the input register readers; the control registers are read after the protocol detection so the version gating applies to them as well; ESS preheating polling is skipped below protocol V2.9
269
+ - (ssbingo) fix: corrected the protocol version marker in the names of the eleven cumulative energy states 30228-30268 from `(V2.8)` to `(V2.7)`; the revision history of the V2.9 protocol document lists the cumulative energy interface 30196-30268 under V2.7
270
+
258
271
  ### 3.2.0 (2026-09-04)
259
272
  - (MMeinhardt1) feat: new state `statistics.emsWorkMode` exposing the EMS work mode as plain text, so consumers do not need the `common.states` lookup table
260
273
  - (MMeinhardt1) feat: added EMS work mode 6 (Virtual Power Plant) to the register description and the mode map
package/io-package.json CHANGED
@@ -1,8 +1,34 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "sigenergy",
4
- "version": "3.2.0",
4
+ "version": "3.3.1",
5
5
  "news": {
6
+ "3.3.1": {
7
+ "en": "(ssbingo) fix: registers the device marks as not valid (all bits set, 0xFFFF / 0xFFFFFFFF) are no longer scaled by the register gain into plausible looking measurements — observed as a 6553.5 A discharging current, 42949672.95 kWh and an out-of-range running state 65535 on the DC charger, and as 655.35 % or 65.535 V elsewhere. Such registers now report no value",
8
+ "de": "(ssbingo) fix: Register, die das Gerät als ungültig kennzeichnet (alle Bits gesetzt, 0xFFFF / 0xFFFFFFFF), werden nicht mehr über den Gain in plausibel aussehende Messwerte umgerechnet — sichtbar als 6553,5 A Entladestrom, 42949672,95 kWh und Betriebszustand 65535 beim DC-Charger sowie als 655,35 % oder 65,535 V an anderer Stelle. Solche Register liefern jetzt keinen Wert",
9
+ "ru": "(ssbingo) fix: регистры, помеченные устройством как недействительные (все биты установлены, 0xFFFF / 0xFFFFFFFF), больше не пересчитываются через коэффициент в правдоподобные значения — это проявлялось как ток разряда 6553,5 A, 42949672,95 кВт·ч и состояние 65535 у DC-зарядки, а также как 655,35 % или 65,535 В в других местах. Теперь такие регистры не возвращают значения",
10
+ "pt": "(ssbingo) fix: os registos que o dispositivo marca como inválidos (todos os bits a 1, 0xFFFF / 0xFFFFFFFF) deixam de ser convertidos pelo ganho em medições aparentemente plausíveis — surgiam como 6553,5 A de corrente de descarga, 42949672,95 kWh e o estado 65535 no carregador DC, e como 655,35 % ou 65,535 V noutros pontos. Esses registos passam a não devolver valor",
11
+ "nl": "(ssbingo) fix: registers die het apparaat als ongeldig markeert (alle bits gezet, 0xFFFF / 0xFFFFFFFF) worden niet langer via de gain omgerekend naar plausibel ogende metingen — zichtbaar als 6553,5 A ontlaadstroom, 42949672,95 kWh en status 65535 bij de DC-lader, en als 655,35 % of 65,535 V elders. Zulke registers geven nu geen waarde meer",
12
+ "fr": "(ssbingo) fix : les registres que l'appareil marque comme non valides (tous les bits à 1, 0xFFFF / 0xFFFFFFFF) ne sont plus convertis par le gain en mesures d'apparence plausible — cela apparaissait comme un courant de décharge de 6553,5 A, 42949672,95 kWh et l'état 65535 sur le chargeur DC, et comme 655,35 % ou 65,535 V ailleurs. Ces registres ne renvoient plus de valeur",
13
+ "it": "(ssbingo) fix: i registri che il dispositivo segnala come non validi (tutti i bit a 1, 0xFFFF / 0xFFFFFFFF) non vengono più convertiti dal gain in misure apparentemente plausibili — comparivano come corrente di scarica di 6553,5 A, 42949672,95 kWh e stato 65535 sul caricatore DC, e come 655,35 % o 65,535 V altrove. Tali registri ora non restituiscono alcun valore",
14
+ "es": "(ssbingo) fix: los registros que el dispositivo marca como no válidos (todos los bits a 1, 0xFFFF / 0xFFFFFFFF) ya no se convierten mediante la ganancia en mediciones de apariencia plausible — aparecían como una corriente de descarga de 6553,5 A, 42949672,95 kWh y el estado 65535 en el cargador DC, y como 655,35 % o 65,535 V en otros puntos. Esos registros ya no devuelven valor",
15
+ "pl": "(ssbingo) fix: rejestry oznaczone przez urządzenie jako nieprawidłowe (wszystkie bity ustawione, 0xFFFF / 0xFFFFFFFF) nie są już przeliczane przez wzmocnienie na wiarygodnie wyglądające pomiary — objawiało się to prądem rozładowania 6553,5 A, 42949672,95 kWh i stanem 65535 na ładowarce DC oraz 655,35 % lub 65,535 V w innych miejscach. Takie rejestry nie zwracają teraz wartości",
16
+ "uk": "(ssbingo) fix: регістри, позначені пристроєм як недійсні (усі біти встановлено, 0xFFFF / 0xFFFFFFFF), більше не перетворюються через коефіцієнт на правдоподібні вимірювання — це виглядало як струм розряду 6553,5 A, 42949672,95 кВт·год і стан 65535 на DC-зарядці та як 655,35 % або 65,535 В в інших місцях. Тепер такі регістри не повертають значення",
17
+ "zh-cn": "(ssbingo) fix: 设备标记为无效的寄存器(全部位为 1,0xFFFF / 0xFFFFFFFF)不再经增益换算成看似合理的测量值——此前表现为直流充电桩的 6553.5 A 放电电流、42949672.95 kWh 和超出范围的运行状态 65535,以及其他位置的 655.35 % 或 65.535 V。此类寄存器现在不再返回数值"
18
+ },
19
+ "3.3.0": {
20
+ "en": "(ssbingo) feat: registers are gated by the detected Modbus protocol version, so a device is no longer asked for registers its firmware does not know; rejected register groups are now retried register by register, so one unsupported register no longer silences its whole group; new state info.protocolVersion",
21
+ "de": "(ssbingo) feat: Register werden anhand der erkannten Modbus-Protokollversion gefiltert, damit ein Gerät nicht mehr nach Registern gefragt wird, die seine Firmware nicht kennt; abgelehnte Registergruppen werden jetzt einzeln nachgelesen, sodass ein nicht unterstütztes Register nicht mehr die ganze Gruppe stilllegt; neuer State info.protocolVersion",
22
+ "ru": "(ssbingo) feat: регистры фильтруются по обнаруженной версии протокола Modbus, поэтому устройство больше не запрашивается о регистрах, неизвестных его прошивке; отклонённые группы регистров перечитываются по одному, поэтому один неподдерживаемый регистр больше не блокирует всю группу; новое состояние info.protocolVersion",
23
+ "pt": "(ssbingo) feat: os registos são filtrados pela versão do protocolo Modbus detetada, pelo que o dispositivo deixa de ser questionado sobre registos que o seu firmware desconhece; os grupos rejeitados passam a ser lidos registo a registo, pelo que um registo não suportado deixa de silenciar todo o grupo; novo estado info.protocolVersion",
24
+ "nl": "(ssbingo) feat: registers worden gefilterd op de gedetecteerde Modbus-protocolversie, zodat een apparaat niet langer wordt bevraagd over registers die zijn firmware niet kent; geweigerde registergroepen worden nu register voor register gelezen, zodat één niet-ondersteund register niet meer de hele groep stillegt; nieuwe state info.protocolVersion",
25
+ "fr": "(ssbingo) feat : les registres sont filtrés selon la version du protocole Modbus détectée, de sorte qu'un appareil n'est plus interrogé sur des registres que son micrologiciel ignore ; les groupes rejetés sont désormais relus registre par registre, de sorte qu'un registre non pris en charge ne neutralise plus tout son groupe ; nouvel état info.protocolVersion",
26
+ "it": "(ssbingo) feat: i registri vengono filtrati in base alla versione del protocollo Modbus rilevata, così il dispositivo non viene più interrogato su registri che il suo firmware non conosce; i gruppi rifiutati vengono ora letti registro per registro, così un registro non supportato non azzera più l'intero gruppo; nuovo stato info.protocolVersion",
27
+ "es": "(ssbingo) feat: los registros se filtran según la versión del protocolo Modbus detectada, de modo que ya no se consulta al dispositivo por registros que su firmware desconoce; los grupos rechazados se leen ahora registro a registro, de modo que un registro no soportado ya no silencia todo su grupo; nuevo estado info.protocolVersion",
28
+ "pl": "(ssbingo) feat: rejestry są filtrowane według wykrytej wersji protokołu Modbus, więc urządzenie nie jest już pytane o rejestry nieznane jego oprogramowaniu; odrzucone grupy rejestrów są teraz odczytywane pojedynczo, więc jeden nieobsługiwany rejestr nie wyłącza już całej grupy; nowy stan info.protocolVersion",
29
+ "uk": "(ssbingo) feat: регістри фільтруються за виявленою версією протоколу Modbus, тому пристрій більше не запитують про регістри, невідомі його прошивці; відхилені групи регістрів тепер перечитуються по одному, тож один непідтримуваний регістр більше не блокує всю групу; новий стан info.protocolVersion",
30
+ "zh-cn": "(ssbingo) feat: 按检测到的 Modbus 协议版本过滤寄存器,不再向设备请求其固件不支持的寄存器;被拒绝的寄存器组改为逐个读取,单个不支持的寄存器不再导致整组失效;新增状态 info.protocolVersion"
31
+ },
6
32
  "3.2.0": {
7
33
  "en": "(MMeinhardt1) feat: EMS work mode as plain text, Virtual Power Plant mode, h:mm values now carry unit 'h' instead of a suffix; statistics objects are updated on every start. (ssbingo) chore: admin requirement lowered back to >=7.8.23",
8
34
  "de": "(MMeinhardt1) feat: EMS-Betriebsmodus als Klartext, Modus Virtual Power Plant, h:mm-Werte tragen jetzt die Einheit 'h' statt eines Suffix; Statistik-Objekte werden bei jedem Start aktualisiert. (ssbingo) chore: admin-Anforderung zurück auf >=7.8.23",
@@ -258,6 +284,19 @@
258
284
  },
259
285
  "native": {}
260
286
  },
287
+ {
288
+ "_id": "info.protocolVersion",
289
+ "type": "state",
290
+ "common": {
291
+ "name": "Highest Modbus protocol version confirmed by probing (0 = below V2.6 or not detected yet)",
292
+ "type": "number",
293
+ "role": "value",
294
+ "read": true,
295
+ "write": false,
296
+ "def": 0
297
+ },
298
+ "native": {}
299
+ },
261
300
  {
262
301
  "_id": "info.modelType",
263
302
  "type": "state",
package/lib/modbus.js CHANGED
@@ -269,13 +269,38 @@ class ModbusConnection {
269
269
  }
270
270
  }
271
271
 
272
+ /**
273
+ * Values the protocol uses to mark an unsigned register as "not valid",
274
+ * i.e. all bits set. The V2.9 document spells this out for the power
275
+ * limitation registers ("Range:[0, 0xFFFFFFFE]. With value 0xFFFFFFFF,
276
+ * register is not valid.") and devices apply the same convention to
277
+ * registers they do not implement — e.g. a SigenStor without a DC charger
278
+ * answers 0xFFFF for [DC Charger] running state 31513.
279
+ *
280
+ * Only the unsigned types are listed. The document does not define a
281
+ * sentinel for the signed types, and 0x7FFF / 0x7FFFFFFF are legal
282
+ * measurements there, so those must not be filtered.
283
+ */
284
+ static get INVALID_RAW() {
285
+ return {
286
+ U16: 0xffff,
287
+ U32: 0xffffffff,
288
+ U64: 18446744073709552000, // 2^64 - 1, as close as a double gets
289
+ };
290
+ }
291
+
272
292
  /**
273
293
  * Parse raw register data into a typed value
274
294
  *
295
+ * Returns null when the device marked the register as not valid (see
296
+ * INVALID_RAW). Without this, the sentinel is scaled by the gain and ends
297
+ * up as a plausible looking measurement — 0xFFFF with gain 10 becomes
298
+ * "6553.5 A", 0xFFFFFFFF with gain 1000 becomes "4294967.295 kW".
299
+ *
275
300
  * @param {number[]} rawData - Raw register data
276
301
  * @param {string} type - Data type (U16, S16, U32, S32, U64, STRING)
277
302
  * @param {number|null} gain - Scaling factor
278
- * @returns {number|string|null} Decoded value
303
+ * @returns {number|string|null} Decoded value, or null if the register is not valid
279
304
  */
280
305
  static parseValue(rawData, type, gain) {
281
306
  let raw;
@@ -327,6 +352,11 @@ class ModbusConnection {
327
352
  raw = rawData[0];
328
353
  }
329
354
 
355
+ const invalid = ModbusConnection.INVALID_RAW[type];
356
+ if (invalid !== undefined && raw === invalid) {
357
+ return null;
358
+ }
359
+
330
360
  if (gain && gain !== 1 && gain !== null) {
331
361
  return raw / gain;
332
362
  }
@@ -0,0 +1,217 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Grouped Modbus register reads.
5
+ *
6
+ * buildReadGroups() merges consecutive register definitions into as few
7
+ * Modbus requests as possible. readRegisterGroups() executes those requests
8
+ * and, when the device rejects a whole group with a Modbus exception
9
+ * (ILLEGAL FUNCTION / ILLEGAL DATA ADDRESS), falls back to reading the
10
+ * registers of that group one by one. Registers the device rejects
11
+ * individually are remembered and excluded from later polls, so a single
12
+ * unsupported register (e.g. one introduced in a newer protocol version or
13
+ * one that does not apply to the installed hardware) no longer prevents the
14
+ * other registers of its group from being read.
15
+ */
16
+
17
+ /**
18
+ * @typedef {object} ReadLog
19
+ * @property {(msg: string) => void} debug - debug logger
20
+ * @property {(msg: string) => void} info - info logger
21
+ * @property {(msg: string) => void} warn - warn logger
22
+ */
23
+
24
+ /**
25
+ * @typedef {object} ModbusError
26
+ * @property {string} message - error message
27
+ * @property {number} [modbusCode] - Modbus exception code, set by modbus-serial on exception responses
28
+ */
29
+
30
+ /**
31
+ * Build optimized sequential read groups
32
+ *
33
+ * @param {object[]} registers - Register definitions ({ addr, qty, ... })
34
+ * @param {number} maxQty - Maximum registers per group
35
+ * @returns {object[]} Grouped register batches ({ startAddr, totalQty, registers })
36
+ */
37
+ function buildReadGroups(registers, maxQty) {
38
+ if (!registers || registers.length === 0) {
39
+ return [];
40
+ }
41
+
42
+ const sorted = [...registers].sort((a, b) => a.addr - b.addr);
43
+ const groups = [];
44
+ let currentGroup = null;
45
+
46
+ for (const reg of sorted) {
47
+ if (!currentGroup) {
48
+ currentGroup = {
49
+ startAddr: reg.addr,
50
+ totalQty: reg.qty,
51
+ registers: [{ ...reg, offset: 0 }],
52
+ };
53
+ } else {
54
+ const expectedNext = currentGroup.startAddr + currentGroup.totalQty;
55
+ const gap = reg.addr - expectedNext;
56
+
57
+ if (gap >= 0 && gap <= 4 && currentGroup.totalQty + gap + reg.qty <= maxQty) {
58
+ const offset = reg.addr - currentGroup.startAddr;
59
+ currentGroup.totalQty = Math.max(currentGroup.totalQty, offset + reg.qty);
60
+ currentGroup.registers.push({ ...reg, offset });
61
+ } else {
62
+ groups.push(currentGroup);
63
+ currentGroup = {
64
+ startAddr: reg.addr,
65
+ totalQty: reg.qty,
66
+ registers: [{ ...reg, offset: 0 }],
67
+ };
68
+ }
69
+ }
70
+ }
71
+ if (currentGroup) {
72
+ groups.push(currentGroup);
73
+ }
74
+ return groups;
75
+ }
76
+
77
+ /**
78
+ * Whether an error is a Modbus exception proving that the device does not
79
+ * implement the requested register(s). modbus-serial sets `modbusCode` on
80
+ * exception responses: 1 = ILLEGAL FUNCTION, 2 = ILLEGAL DATA ADDRESS.
81
+ * Other exception codes and transport errors (timeouts, socket errors)
82
+ * prove nothing about the register and must not exclude it.
83
+ *
84
+ * @param {ModbusError | null | undefined} err - Error thrown by the Modbus client
85
+ * @returns {boolean} true if the device rejected the register(s)
86
+ */
87
+ function isRegisterRejected(err) {
88
+ return !!err && (err.modbusCode === 1 || err.modbusCode === 2);
89
+ }
90
+
91
+ /**
92
+ * Remember a register the device rejected and log it once.
93
+ *
94
+ * @param {string} section - Section name for the log message
95
+ * @param {object} reg - Register definition
96
+ * @param {Set<number>} unsupported - Set of rejected register addresses
97
+ * @param {ModbusError} err - The Modbus exception
98
+ * @param {ReadLog} log - Logger
99
+ */
100
+ function markUnsupported(section, reg, unsupported, err, log) {
101
+ unsupported.add(reg.addr);
102
+ log.info(
103
+ `[${section}] register ${reg.addr} (${reg.name}) is not supported by this device ` +
104
+ `(${err.message}) - excluded from further polling`,
105
+ );
106
+ }
107
+
108
+ /**
109
+ * Read the registers of a rejected group one by one.
110
+ *
111
+ * @param {object} opts - Options, see readRegisterGroups()
112
+ * @param {string} opts.section - Section name
113
+ * @param {object} opts.group - The rejected group
114
+ * @param {Set<number>} opts.unsupported - Set of rejected register addresses
115
+ * @param {(addr: number, qty: number) => Promise<number[]>} opts.read - Register read function
116
+ * @param {(group: object, raw: number[]) => Promise<void>} opts.process - Group processing function
117
+ * @param {(ms: number) => Promise<void>} opts.sleep - Pause function
118
+ * @param {() => boolean} opts.isStopped - Shutdown check
119
+ * @param {ReadLog} opts.log - Logger
120
+ * @returns {Promise<void>} resolves when all registers were tried
121
+ */
122
+ async function readGroupIndividually(opts) {
123
+ const { section, group, unsupported, read, process, sleep, isStopped, log } = opts;
124
+ for (const reg of group.registers) {
125
+ if (isStopped()) {
126
+ return;
127
+ }
128
+ try {
129
+ const raw = await read(reg.addr, reg.qty);
130
+ await process({ startAddr: reg.addr, totalQty: reg.qty, registers: [{ ...reg, offset: 0 }] }, raw);
131
+ } catch (err) {
132
+ if (isRegisterRejected(err)) {
133
+ markUnsupported(section, reg, unsupported, err, log);
134
+ } else {
135
+ log.debug(`[${section}] read error at ${reg.addr} (${reg.name}): ${err.message}`);
136
+ }
137
+ }
138
+ await sleep(50);
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Read a register set in grouped requests with per-register fallback.
144
+ *
145
+ * @param {object} opts - Options
146
+ * @param {string} opts.section - Section name used in log messages (e.g. 'dcCharger')
147
+ * @param {object[]} opts.registers - Register definitions to read
148
+ * @param {number} opts.batchSize - Maximum registers per Modbus request
149
+ * @param {Set<number>} opts.unsupported - Addresses rejected by the device; extended by this call and excluded from grouping
150
+ * @param {Set<number>} opts.errorLogged - Group start addresses whose transport error was already logged at warn level
151
+ * @param {(addr: number, qty: number) => Promise<number[]>} opts.read - Reads `qty` registers starting at `addr`
152
+ * @param {(group: object, raw: number[]) => Promise<void>} opts.process - Processes the raw data of one group
153
+ * @param {(ms: number) => Promise<void>} opts.sleep - Pause between requests
154
+ * @param {() => boolean} opts.isStopped - Returns true while the adapter is shutting down
155
+ * @param {ReadLog} opts.log - Logger
156
+ * @returns {Promise<void>} resolves when all groups were processed
157
+ */
158
+ async function readRegisterGroups(opts) {
159
+ const { section, batchSize, unsupported, errorLogged, read, process, sleep, isStopped, log } = opts;
160
+ const registers = opts.registers.filter(r => !unsupported.has(r.addr));
161
+ const groups = buildReadGroups(registers, batchSize);
162
+ log.debug(`[${section}] reading ${registers.length} register(s) in ${groups.length} group(s)`);
163
+
164
+ for (const group of groups) {
165
+ if (isStopped()) {
166
+ return;
167
+ }
168
+ try {
169
+ const raw = await read(group.startAddr, group.totalQty);
170
+ await process(group, raw);
171
+ await sleep(100);
172
+ } catch (err) {
173
+ if (!isRegisterRejected(err)) {
174
+ // Transport error, timeout or a non-conclusive exception: keep the
175
+ // group, warn once and simply retry on the next poll cycle.
176
+ if (!errorLogged.has(group.startAddr)) {
177
+ errorLogged.add(group.startAddr);
178
+ log.warn(
179
+ `[${section}] register read error at ${group.startAddr} (qty ${group.totalQty}): ${err.message}`,
180
+ );
181
+ } else {
182
+ log.debug(`[${section}] read error at ${group.startAddr}: ${err.message}`);
183
+ }
184
+ continue;
185
+ }
186
+ if (group.registers.length === 1) {
187
+ markUnsupported(section, group.registers[0], unsupported, err, log);
188
+ continue;
189
+ }
190
+ log.debug(
191
+ `[${section}] group ${group.startAddr} (qty ${group.totalQty}) rejected by device ` +
192
+ `(${err.message}) - reading its registers individually`,
193
+ );
194
+ await readGroupIndividually({ section, group, unsupported, read, process, sleep, isStopped, log });
195
+ }
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Split registers into those supported by the detected protocol version and
201
+ * those introduced in a newer version (`reg.since`). With an unknown version
202
+ * (0 / undefined) nothing is skipped.
203
+ *
204
+ * @param {object[]} registers - Register definitions, optionally with `since`
205
+ * @param {number} protocolVersion - Detected protocol version (e.g. 2.8), 0 = unknown
206
+ * @returns {{ active: object[], skipped: object[] }} partitioned registers
207
+ */
208
+ function filterByProtocolVersion(registers, protocolVersion) {
209
+ if (!protocolVersion || protocolVersion <= 0) {
210
+ return { active: registers, skipped: [] };
211
+ }
212
+ const skipped = registers.filter(r => typeof r.since === 'number' && r.since > protocolVersion);
213
+ const active = skipped.length > 0 ? registers.filter(r => !skipped.includes(r)) : registers;
214
+ return { active, skipped };
215
+ }
216
+
217
+ module.exports = { buildReadGroups, readRegisterGroups, isRegisterRejected, filterByProtocolVersion };
package/lib/registers.js CHANGED
@@ -1155,7 +1155,7 @@ const PLANT_READ_REGISTERS = [
1155
1155
  gain: 100,
1156
1156
  unit: 'kWh',
1157
1157
  role: 'value.energy',
1158
- desc: 'Total common loads consumption (V2.8)',
1158
+ desc: 'Total common loads consumption (V2.7)',
1159
1159
  feature: 'enableCumulativeEnergy',
1160
1160
  },
1161
1161
  {
@@ -1166,7 +1166,7 @@ const PLANT_READ_REGISTERS = [
1166
1166
  gain: 100,
1167
1167
  unit: 'kWh',
1168
1168
  role: 'value.energy',
1169
- desc: 'Total EVAC charged (V2.8)',
1169
+ desc: 'Total EVAC charged (V2.7)',
1170
1170
  feature: 'enableCumulativeEnergy',
1171
1171
  },
1172
1172
  {
@@ -1177,7 +1177,7 @@ const PLANT_READ_REGISTERS = [
1177
1177
  gain: 100,
1178
1178
  unit: 'kWh',
1179
1179
  role: 'value.energy',
1180
- desc: 'Total self PV generation (V2.8)',
1180
+ desc: 'Total self PV generation (V2.7)',
1181
1181
  feature: 'enableCumulativeEnergy',
1182
1182
  },
1183
1183
  {
@@ -1188,7 +1188,7 @@ const PLANT_READ_REGISTERS = [
1188
1188
  gain: 100,
1189
1189
  unit: 'kWh',
1190
1190
  role: 'value.energy',
1191
- desc: 'Total 3rd party generation (V2.8)',
1191
+ desc: 'Total 3rd party generation (V2.7)',
1192
1192
  feature: 'enableCumulativeEnergy',
1193
1193
  },
1194
1194
  {
@@ -1199,7 +1199,7 @@ const PLANT_READ_REGISTERS = [
1199
1199
  gain: 100,
1200
1200
  unit: 'kWh',
1201
1201
  role: 'value.energy',
1202
- desc: 'Total ESS charged (V2.8)',
1202
+ desc: 'Total ESS charged (V2.7)',
1203
1203
  feature: 'enableCumulativeEnergy',
1204
1204
  },
1205
1205
  {
@@ -1210,7 +1210,7 @@ const PLANT_READ_REGISTERS = [
1210
1210
  gain: 100,
1211
1211
  unit: 'kWh',
1212
1212
  role: 'value.energy',
1213
- desc: 'Total ESS discharged (V2.8)',
1213
+ desc: 'Total ESS discharged (V2.7)',
1214
1214
  feature: 'enableCumulativeEnergy',
1215
1215
  },
1216
1216
  {
@@ -1221,7 +1221,7 @@ const PLANT_READ_REGISTERS = [
1221
1221
  gain: 100,
1222
1222
  unit: 'kWh',
1223
1223
  role: 'value.energy',
1224
- desc: 'Total EVDC charged (V2.8)',
1224
+ desc: 'Total EVDC charged (V2.7)',
1225
1225
  feature: 'enableCumulativeEnergy',
1226
1226
  },
1227
1227
  {
@@ -1232,7 +1232,7 @@ const PLANT_READ_REGISTERS = [
1232
1232
  gain: 100,
1233
1233
  unit: 'kWh',
1234
1234
  role: 'value.energy',
1235
- desc: 'Total EVDC discharged (V2.8)',
1235
+ desc: 'Total EVDC discharged (V2.7)',
1236
1236
  feature: 'enableCumulativeEnergy',
1237
1237
  },
1238
1238
  {
@@ -1243,7 +1243,7 @@ const PLANT_READ_REGISTERS = [
1243
1243
  gain: 100,
1244
1244
  unit: 'kWh',
1245
1245
  role: 'value.energy',
1246
- desc: 'Total imported energy (V2.8)',
1246
+ desc: 'Total imported energy (V2.7)',
1247
1247
  feature: 'enableCumulativeEnergy',
1248
1248
  },
1249
1249
  {
@@ -1254,7 +1254,7 @@ const PLANT_READ_REGISTERS = [
1254
1254
  gain: 100,
1255
1255
  unit: 'kWh',
1256
1256
  role: 'value.energy',
1257
- desc: 'Total exported energy (V2.8)',
1257
+ desc: 'Total exported energy (V2.7)',
1258
1258
  feature: 'enableCumulativeEnergy',
1259
1259
  },
1260
1260
  {
@@ -1265,7 +1265,7 @@ const PLANT_READ_REGISTERS = [
1265
1265
  gain: 100,
1266
1266
  unit: 'kWh',
1267
1267
  role: 'value.energy',
1268
- desc: 'Total oil generator energy (V2.8)',
1268
+ desc: 'Total oil generator energy (V2.7)',
1269
1269
  feature: 'enableCumulativeEnergy',
1270
1270
  },
1271
1271
  // V2.9 — separate FC04 group (gap > 4 from 30087)
@@ -4981,6 +4981,80 @@ const DC_CHARGER_RUNNING_STATES = {
4981
4981
  0x0a: 'Preparing (Insulation)',
4982
4982
  };
4983
4983
 
4984
+ // ============================================================
4985
+ // Protocol version gating
4986
+ // ============================================================
4987
+ // Register address ranges and the Sigenergy Modbus protocol version that
4988
+ // introduced them, taken from the revision history of the protocol document
4989
+ // V2.9. Applied below as `since` to every register definition inside a range
4990
+ // (explicit `since` fields on a register take precedence). Registers newer
4991
+ // than the protocol level detected on the device are not polled, see
4992
+ // main.js _readRegisterGroups() / lib/readGroups.js filterByProtocolVersion().
4993
+ // Address spaces are per Modbus slave, so the ranges are kept per device family.
4994
+ const PROTOCOL_SINCE = {
4995
+ plant: [
4996
+ // V2.6 (2025-03-31)
4997
+ { from: 30088, to: 30192, since: 2.6 }, // PV total generation, total load consumption, smart load 1-24
4998
+ { from: 40046, to: 40048, since: 2.6 }, // ESS backup SOC, charge / discharge cut-off SOC
4999
+ // V2.7 (2025-05-23) - the register table of the document labels 30228-30268
5000
+ // "New statistics interface", the revision history dates 30196-30268 to V2.7
5001
+ { from: 30194, to: 30268, since: 2.7 }, // third party inverter power, cumulative energy registers
5002
+ // V2.8 (2025-11-28)
5003
+ { from: 30276, to: 30285, since: 2.8 }, // grid code rated frequency / voltage, current control command, merged alarm 6 / 7, general / total load power
5004
+ { from: 40049, to: 40068, since: 2.8 }, // active power regulation gradient, grid code registers
5005
+ // V2.9 (2026-05-13)
5006
+ { from: 30272, to: 30275, since: 2.9 }, // PV total daily generation, PV total generation of previous day
5007
+ { from: 30286, to: 30286, since: 2.9 }, // ESS average cell temperature
5008
+ { from: 40157, to: 40159, since: 2.9 }, // PCC power factor targets, grid power loss lockout alarm clear
5009
+ { from: 50000, to: 50183, since: 2.9 }, // ESS preheating TOU registers
5010
+ ],
5011
+ inverter: [
5012
+ // V2.6 (2025-03-31)
5013
+ { from: 30613, to: 30619, since: 2.6 }, // power adjustment feedback values
5014
+ { from: 31509, to: 31512, since: 2.6 }, // [DC Charger] PV daily / total generation
5015
+ // V2.8 (2025-11-28)
5016
+ { from: 31066, to: 31105, since: 2.8 }, // PV voltage / current of additional strings
5017
+ { from: 31513, to: 31513, since: 2.8 }, // [DC Charger] running state
5018
+ // V2.9 (2026-05-13)
5019
+ { from: 31514, to: 31526, since: 2.9 }, // [DC Charger] discharging registers
5020
+ { from: 41002, to: 41005, since: 2.9 }, // [DC Charger] max charging / discharging power limit
5021
+ ],
5022
+ pss: [{ from: 0, to: 65535, since: 2.9 }], // PSS registers were added in V2.9
5023
+ pid: [{ from: 0, to: 65535, since: 2.9 }], // PID registers were added in V2.9
5024
+ };
5025
+
5026
+ /**
5027
+ * Set `since` on every register whose address falls into one of the ranges.
5028
+ *
5029
+ * @param {object[]} registers - Register definitions (mutated)
5030
+ * @param {{ from: number, to: number, since: number }[]} ranges - Address ranges with protocol version
5031
+ * @returns {object[]} the same array, for chaining
5032
+ */
5033
+ function applySince(registers, ranges) {
5034
+ for (const reg of registers) {
5035
+ if (reg.since !== undefined) {
5036
+ continue;
5037
+ }
5038
+ const hit = ranges.find(r => reg.addr >= r.from && reg.addr <= r.to);
5039
+ if (hit) {
5040
+ reg.since = hit.since;
5041
+ }
5042
+ }
5043
+ return registers;
5044
+ }
5045
+
5046
+ applySince(PLANT_READ_REGISTERS, PROTOCOL_SINCE.plant);
5047
+ applySince(PLANT_WRITE_REGISTERS, PROTOCOL_SINCE.plant);
5048
+ applySince(ESS_PREHEATING_WRITE_REGISTERS, PROTOCOL_SINCE.plant);
5049
+ applySince(INVERTER_READ_REGISTERS, PROTOCOL_SINCE.inverter);
5050
+ applySince(INVERTER_WRITE_REGISTERS, PROTOCOL_SINCE.inverter);
5051
+ applySince(DC_CHARGER_READ_REGISTERS, PROTOCOL_SINCE.inverter);
5052
+ applySince(DC_CHARGER_WRITE_REGISTERS, PROTOCOL_SINCE.inverter);
5053
+ applySince(PSS_READ_REGISTERS, PROTOCOL_SINCE.pss);
5054
+ applySince(PSS_WRITE_REGISTERS, PROTOCOL_SINCE.pss);
5055
+ applySince(PID_READ_REGISTERS, PROTOCOL_SINCE.pid);
5056
+ applySince(PID_WRITE_REGISTERS, PROTOCOL_SINCE.pid);
5057
+
4984
5058
  module.exports = {
4985
5059
  PLANT_READ_REGISTERS,
4986
5060
  PLANT_WRITE_REGISTERS,
@@ -4999,4 +5073,6 @@ module.exports = {
4999
5073
  EMS_WORK_MODES,
5000
5074
  REMOTE_EMS_MODES,
5001
5075
  DC_CHARGER_RUNNING_STATES,
5076
+ PROTOCOL_SINCE,
5077
+ applySince,
5002
5078
  };
package/main.js CHANGED
@@ -28,6 +28,7 @@ const {
28
28
  REMOTE_EMS_MODES,
29
29
  DC_CHARGER_RUNNING_STATES,
30
30
  } = require('./lib/registers');
31
+ const { buildReadGroups, readRegisterGroups, filterByProtocolVersion } = require('./lib/readGroups');
31
32
  const SigenMicroScanner = require('./lib/scanner');
32
33
  const { getRegistersForDevice } = require('./lib/sigenMicroRegisters');
33
34
  const adapterVersion = require('./package.json').version;
@@ -134,6 +135,7 @@ class Sigenergy extends utils.Adapter {
134
135
  this._stopped = false;
135
136
  this._controlRegistersRead = false;
136
137
  this._protocolDetected = false;
138
+ this._protocolVersion = 0;
137
139
  this._emergencyWasOffGrid = false;
138
140
  this._emergencyStabilityTimer = null;
139
141
  this._emergencySwitchCount = 0;
@@ -141,12 +143,11 @@ class Sigenergy extends utils.Adapter {
141
143
  this._modelVerified = false;
142
144
  this._pvStringCount = 0;
143
145
  this._essPreheatingUnsupported = false;
144
- this._plantUnsupportedGroups = new Set();
145
- this._inverterUnsupportedGroups = new Set();
146
- this._acChargerUnsupportedGroups = new Set();
147
- this._dcChargerUnsupportedGroups = new Set();
148
- this._pssUnsupportedGroups = new Set();
149
- this._pidUnsupportedGroups = new Set();
146
+ // Per section: register addresses the device rejected (excluded from
147
+ // polling) and group start addresses whose read error was already logged.
148
+ this._unsupportedRegisters = {};
149
+ this._readErrorLogged = {};
150
+ this._sinceSkipLogged = {};
150
151
 
151
152
  this.on('ready', this.onReady.bind(this));
152
153
  this.on('stateChange', this.onStateChange.bind(this));
@@ -403,15 +404,17 @@ class Sigenergy extends utils.Adapter {
403
404
 
404
405
  try {
405
406
  if (this._caps.plant) {
406
- if (!this._controlRegistersRead) {
407
- await this._readControlRegisters();
408
- this._controlRegistersRead = true;
409
- }
410
407
  if (!this._protocolDetected) {
411
408
  // Only latch the flag if detection was conclusive (no transport
412
409
  // errors). Otherwise retry on the next poll cycle.
413
410
  this._protocolDetected = await this._detectProtocolLevel();
414
411
  }
412
+ // Read the control registers after the protocol detection, so
413
+ // registers newer than the detected protocol level are skipped.
414
+ if (!this._controlRegistersRead) {
415
+ await this._readControlRegisters();
416
+ this._controlRegistersRead = true;
417
+ }
415
418
  if (!this._modelVerified) {
416
419
  await this._verifyModelType();
417
420
  }
@@ -525,31 +528,58 @@ class Sigenergy extends utils.Adapter {
525
528
  return true;
526
529
  }
527
530
 
531
+ /**
532
+ * Read the input registers of one section (plant, inverter, acCharger,
533
+ * dcCharger, pss, pid) in grouped FC04 requests. If the device rejects a
534
+ * whole group, its registers are read one by one and only the rejected
535
+ * ones are excluded from further polls (see lib/readGroups.js).
536
+ *
537
+ * @param {string} section - Section name (log tag, key for the unsupported-register bookkeeping)
538
+ * @param {number} slaveId - Modbus slave id
539
+ * @param {object[]} registers - Register definitions
540
+ * @param {number} batchSize - Maximum registers per request
541
+ * @param {boolean} [holding] - Read holding registers (FC03) instead of input registers (FC04)
542
+ * @returns {Promise<void>} resolves when the section was read
543
+ */
544
+ async _readRegisterGroups(section, slaveId, registers, batchSize, holding = false) {
545
+ if (!this._unsupportedRegisters[section]) {
546
+ this._unsupportedRegisters[section] = new Set();
547
+ this._readErrorLogged[section] = new Set();
548
+ }
549
+ // Skip registers introduced in a newer protocol version than the one
550
+ // detected on the device (reg.since, only once detection succeeded).
551
+ const { active, skipped } = filterByProtocolVersion(registers, this._protocolVersion);
552
+ registers = active;
553
+ if (skipped.length > 0 && !this._sinceSkipLogged[section]) {
554
+ this._sinceSkipLogged[section] = true;
555
+ const list = skipped.map(r => `${r.addr} (${r.name}, V${r.since})`).join(', ');
556
+ this.log.info(
557
+ `[${section}] ${skipped.length} register(s) require a newer protocol version than detected ` +
558
+ `(V${this._protocolVersion}) and are not polled: ${list}`,
559
+ );
560
+ }
561
+ this.log.debug(`Reading ${section} (slaveId=${slaveId})`);
562
+ await readRegisterGroups({
563
+ section,
564
+ registers,
565
+ batchSize,
566
+ unsupported: this._unsupportedRegisters[section],
567
+ errorLogged: this._readErrorLogged[section],
568
+ read: (addr, qty) =>
569
+ holding
570
+ ? this.modbus.readHoldingRegisters(slaveId, addr, qty)
571
+ : this.modbus.readInputRegisters(slaveId, addr, qty),
572
+ process: (group, raw) => this._processReadGroup(group, raw, section),
573
+ sleep: ms => this._sleep(ms),
574
+ isStopped: () => this._stopped,
575
+ log: this.log,
576
+ });
577
+ }
578
+
528
579
  async _readPlant() {
529
580
  const plantId = this.config.plantId || 247;
530
- const batchSize = 120;
531
581
  const activeRegs = PLANT_READ_REGISTERS.filter(r => this._regActive(r));
532
- const groups = this._buildReadGroups(activeRegs, batchSize);
533
- this.log.debug(`Reading plant (slaveId=${plantId}): ${groups.length} group(s), ${activeRegs.length} registers`);
534
-
535
- for (const group of groups) {
536
- if (this._stopped) {
537
- return;
538
- }
539
- try {
540
- this.log.debug(`[plant] FC04 addr=${group.startAddr} qty=${group.totalQty}`);
541
- const raw = await this.modbus.readInputRegisters(plantId, group.startAddr, group.totalQty);
542
- await this._processReadGroup(group, raw, 'plant');
543
- await this._sleep(100);
544
- } catch (err) {
545
- if (!this._plantUnsupportedGroups.has(group.startAddr)) {
546
- this._plantUnsupportedGroups.add(group.startAddr);
547
- this.log.warn(`Plant register read error at ${group.startAddr}: ${err.message}`);
548
- } else {
549
- this.log.debug(`[plant] read error at ${group.startAddr}: ${err.message}`);
550
- }
551
- }
552
- }
582
+ await this._readRegisterGroups('plant', plantId, activeRegs, 120);
553
583
  }
554
584
 
555
585
  /**
@@ -557,30 +587,8 @@ class Sigenergy extends utils.Adapter {
557
587
  */
558
588
  async _readInverter() {
559
589
  const inverterId = this.config.inverterId || 1;
560
- const batchSize = 60;
561
590
  const activeRegs = INVERTER_READ_REGISTERS.filter(r => this._regActive(r));
562
- const groups = this._buildReadGroups(activeRegs, batchSize);
563
- this.log.debug(
564
- `Reading inverter (slaveId=${inverterId}): ${groups.length} group(s),` + ` ${activeRegs.length} registers`,
565
- );
566
-
567
- for (const group of groups) {
568
- if (this._stopped) {
569
- return;
570
- }
571
- try {
572
- const raw = await this.modbus.readInputRegisters(inverterId, group.startAddr, group.totalQty);
573
- await this._processReadGroup(group, raw, 'inverter');
574
- await this._sleep(100);
575
- } catch (err) {
576
- if (!this._inverterUnsupportedGroups.has(group.startAddr)) {
577
- this._inverterUnsupportedGroups.add(group.startAddr);
578
- this.log.warn(`Inverter register read error at ${group.startAddr}: ${err.message}`);
579
- } else {
580
- this.log.debug(`[inverter] read error at ${group.startAddr}: ${err.message}`);
581
- }
582
- }
583
- }
591
+ await this._readRegisterGroups('inverter', inverterId, activeRegs, 60);
584
592
  }
585
593
 
586
594
  /**
@@ -588,27 +596,7 @@ class Sigenergy extends utils.Adapter {
588
596
  */
589
597
  async _readAcCharger() {
590
598
  const acChargerId = this.config.acChargerId || 2;
591
- const batchSize = 30;
592
- const groups = this._buildReadGroups(AC_CHARGER_READ_REGISTERS, batchSize);
593
- this.log.debug(`Reading AC charger (slaveId=${acChargerId}): ${groups.length} group(s)`);
594
-
595
- for (const group of groups) {
596
- if (this._stopped) {
597
- return;
598
- }
599
- try {
600
- const raw = await this.modbus.readInputRegisters(acChargerId, group.startAddr, group.totalQty);
601
- await this._processReadGroup(group, raw, 'acCharger');
602
- await this._sleep(100);
603
- } catch (err) {
604
- if (!this._acChargerUnsupportedGroups.has(group.startAddr)) {
605
- this._acChargerUnsupportedGroups.add(group.startAddr);
606
- this.log.warn(`AC Charger register read error at ${group.startAddr}: ${err.message}`);
607
- } else {
608
- this.log.debug(`[acCharger] read error at ${group.startAddr}: ${err.message}`);
609
- }
610
- }
611
- }
599
+ await this._readRegisterGroups('acCharger', acChargerId, AC_CHARGER_READ_REGISTERS, 30);
612
600
  }
613
601
 
614
602
  /**
@@ -616,77 +604,17 @@ class Sigenergy extends utils.Adapter {
616
604
  */
617
605
  async _readDcCharger() {
618
606
  const inverterId = this.config.inverterId || 1;
619
- const batchSize = 10;
620
- const groups = this._buildReadGroups(DC_CHARGER_READ_REGISTERS, batchSize);
621
- this.log.debug(`Reading DC charger via inverter (slaveId=${inverterId}): ${groups.length} group(s)`);
622
-
623
- for (const group of groups) {
624
- if (this._stopped) {
625
- return;
626
- }
627
- try {
628
- const raw = await this.modbus.readInputRegisters(inverterId, group.startAddr, group.totalQty);
629
- await this._processReadGroup(group, raw, 'dcCharger');
630
- await this._sleep(100);
631
- } catch (err) {
632
- if (!this._dcChargerUnsupportedGroups.has(group.startAddr)) {
633
- this._dcChargerUnsupportedGroups.add(group.startAddr);
634
- this.log.warn(`DC Charger register read error at ${group.startAddr}: ${err.message}`);
635
- } else {
636
- this.log.debug(`[dcCharger] read error at ${group.startAddr}: ${err.message}`);
637
- }
638
- }
639
- }
607
+ await this._readRegisterGroups('dcCharger', inverterId, DC_CHARGER_READ_REGISTERS, 10);
640
608
  }
641
609
 
642
610
  async _readPss() {
643
611
  const pssId = this.config.pssSlaveId || 5;
644
- const batchSize = 50;
645
- const groups = this._buildReadGroups(PSS_READ_REGISTERS, batchSize);
646
- this.log.debug(`Reading PSS (slaveId=${pssId}): ${groups.length} group(s)`);
647
-
648
- for (const group of groups) {
649
- if (this._stopped) {
650
- return;
651
- }
652
- try {
653
- const raw = await this.modbus.readInputRegisters(pssId, group.startAddr, group.totalQty);
654
- await this._processReadGroup(group, raw, 'pss');
655
- await this._sleep(100);
656
- } catch (err) {
657
- if (!this._pssUnsupportedGroups.has(group.startAddr)) {
658
- this._pssUnsupportedGroups.add(group.startAddr);
659
- this.log.warn(`PSS register read error at ${group.startAddr}: ${err.message}`);
660
- } else {
661
- this.log.debug(`[pss] read error at ${group.startAddr}: ${err.message}`);
662
- }
663
- }
664
- }
612
+ await this._readRegisterGroups('pss', pssId, PSS_READ_REGISTERS, 50);
665
613
  }
666
614
 
667
615
  async _readPid() {
668
616
  const pidId = this.config.pidSlaveId || 6;
669
- const batchSize = 50;
670
- const groups = this._buildReadGroups(PID_READ_REGISTERS, batchSize);
671
- this.log.debug(`Reading PID (slaveId=${pidId}): ${groups.length} group(s)`);
672
-
673
- for (const group of groups) {
674
- if (this._stopped) {
675
- return;
676
- }
677
- try {
678
- const raw = await this.modbus.readInputRegisters(pidId, group.startAddr, group.totalQty);
679
- await this._processReadGroup(group, raw, 'pid');
680
- await this._sleep(100);
681
- } catch (err) {
682
- if (!this._pidUnsupportedGroups.has(group.startAddr)) {
683
- this._pidUnsupportedGroups.add(group.startAddr);
684
- this.log.warn(`PID register read error at ${group.startAddr}: ${err.message}`);
685
- } else {
686
- this.log.debug(`[pid] read error at ${group.startAddr}: ${err.message}`);
687
- }
688
- }
689
- }
617
+ await this._readRegisterGroups('pid', pidId, PID_READ_REGISTERS, 50);
690
618
  }
691
619
 
692
620
  async _readEssPreheating() {
@@ -698,7 +626,14 @@ class Sigenergy extends utils.Adapter {
698
626
  }
699
627
  const plantId = this.config.plantId || 247;
700
628
  const batchSize = 50;
701
- const groups = this._buildReadGroups(ESS_PREHEATING_WRITE_REGISTERS, batchSize);
629
+ const { active, skipped } = filterByProtocolVersion(ESS_PREHEATING_WRITE_REGISTERS, this._protocolVersion);
630
+ if (active.length === 0 && skipped.length > 0) {
631
+ this.log.debug(
632
+ `[essPreheating] Skipping — registers require protocol V2.9, detected V${this._protocolVersion}`,
633
+ );
634
+ return;
635
+ }
636
+ const groups = this._buildReadGroups(active, batchSize);
702
637
  this.log.debug(`Reading ESS Preheating (slaveId=${plantId}): ${groups.length} group(s)`);
703
638
 
704
639
  let failCount = 0;
@@ -896,8 +831,12 @@ class Sigenergy extends utils.Adapter {
896
831
  }
897
832
 
898
833
  const levelStr = detectedLevel ? `>=${detectedLevel}` : 'pre-V2.6';
834
+ // Numeric twin of info.protocolLevel for consumers (widgets, scripts):
835
+ // highest confirmed protocol version, 0 = below V2.6.
836
+ this._protocolVersion = detectedLevel ? parseFloat(detectedLevel.replace(/^V/, '')) : 0;
899
837
  this.log.info(`Detected protocol level: ${levelStr} (firmware: ${firmwareVersion})`);
900
838
  await this.setStateAsync('info.protocolLevel', { val: levelStr, ack: true });
839
+ await this.setStateAsync('info.protocolVersion', { val: this._protocolVersion, ack: true });
901
840
  return true;
902
841
  }
903
842
 
@@ -950,51 +889,18 @@ class Sigenergy extends utils.Adapter {
950
889
 
951
890
  const plantRw = PLANT_WRITE_REGISTERS.filter(r => r.perm === 'RW' && this._regActive(r));
952
891
  if (plantRw.length > 0) {
953
- for (const group of this._buildReadGroups(plantRw, 50)) {
954
- if (this._stopped) {
955
- return;
956
- }
957
- try {
958
- const raw = await this.modbus.readHoldingRegisters(plantId, group.startAddr, group.totalQty);
959
- await this._processReadGroup(group, raw, 'control');
960
- await this._sleep(100);
961
- } catch (err) {
962
- this.log.debug(`[controlRegs] plant addr=${group.startAddr}: ${err.message}`);
963
- }
964
- }
892
+ await this._readRegisterGroups('control.plant', plantId, plantRw, 50, true);
965
893
  }
966
894
 
967
895
  const invRw = INVERTER_WRITE_REGISTERS.filter(r => r.perm === 'RW' && this._regActive(r));
968
896
  if (invRw.length > 0) {
969
- for (const group of this._buildReadGroups(invRw, 50)) {
970
- if (this._stopped) {
971
- return;
972
- }
973
- try {
974
- const raw = await this.modbus.readHoldingRegisters(inverterId, group.startAddr, group.totalQty);
975
- await this._processReadGroup(group, raw, 'control');
976
- await this._sleep(100);
977
- } catch (err) {
978
- this.log.debug(`[controlRegs] inverter addr=${group.startAddr}: ${err.message}`);
979
- }
980
- }
897
+ await this._readRegisterGroups('control.inverter', inverterId, invRw, 50, true);
981
898
  }
982
899
 
983
900
  if (this.config.hasDcCharger) {
984
901
  const dcRw = DC_CHARGER_WRITE_REGISTERS.filter(r => r.perm === 'RW');
985
902
  if (dcRw.length > 0) {
986
- for (const group of this._buildReadGroups(dcRw, 50)) {
987
- if (this._stopped) {
988
- return;
989
- }
990
- try {
991
- const raw = await this.modbus.readHoldingRegisters(inverterId, group.startAddr, group.totalQty);
992
- await this._processReadGroup(group, raw, 'control');
993
- await this._sleep(100);
994
- } catch (err) {
995
- this.log.debug(`[controlRegs] dcCharger addr=${group.startAddr}: ${err.message}`);
996
- }
997
- }
903
+ await this._readRegisterGroups('control.dcCharger', inverterId, dcRw, 50, true);
998
904
  }
999
905
  }
1000
906
  }
@@ -1015,73 +921,22 @@ class Sigenergy extends utils.Adapter {
1015
921
  return;
1016
922
  }
1017
923
  const registers = getRegistersForDevice(device.slaveId);
1018
- const batchSize = 30;
1019
- const groups = this._buildReadGroups(registers, batchSize);
1020
924
  this.log.debug(
1021
- `[SigenMicro] device slaveId=${device.slaveId}` +
1022
- ` model='${device.model}': ${groups.length} group(s), ${registers.length} registers`,
925
+ `[SigenMicro] device slaveId=${device.slaveId} model='${device.model}': ${registers.length} registers`,
1023
926
  );
1024
-
1025
- for (const group of groups) {
1026
- if (this._stopped) {
1027
- return;
1028
- }
1029
- try {
1030
- const raw = await this.modbus.readInputRegisters(device.slaveId, group.startAddr, group.totalQty);
1031
- await this._processReadGroup(group, raw, `sigenmicro.${device.slaveId}`);
1032
- await this._sleep(100);
1033
- } catch (err) {
1034
- this.log.warn(`SigenMicro ID ${device.slaveId} read error at ${group.startAddr}: ${err.message}`);
1035
- }
1036
- }
927
+ await this._readRegisterGroups(`sigenmicro.${device.slaveId}`, device.slaveId, registers, 30);
1037
928
  }
1038
929
  }
1039
930
 
1040
931
  /**
1041
- * Build optimized sequential read groups
932
+ * Build optimized sequential read groups (see lib/readGroups.js)
1042
933
  *
1043
934
  * @param {object[]} registers - Register definitions
1044
935
  * @param {number} maxQty - Maximum registers per group
1045
936
  * @returns {object[]} Grouped register batches
1046
937
  */
1047
938
  _buildReadGroups(registers, maxQty) {
1048
- if (!registers || registers.length === 0) {
1049
- return [];
1050
- }
1051
-
1052
- const sorted = [...registers].sort((a, b) => a.addr - b.addr);
1053
- const groups = [];
1054
- let currentGroup = null;
1055
-
1056
- for (const reg of sorted) {
1057
- if (!currentGroup) {
1058
- currentGroup = {
1059
- startAddr: reg.addr,
1060
- totalQty: reg.qty,
1061
- registers: [{ ...reg, offset: 0 }],
1062
- };
1063
- } else {
1064
- const expectedNext = currentGroup.startAddr + currentGroup.totalQty;
1065
- const gap = reg.addr - expectedNext;
1066
-
1067
- if (gap >= 0 && gap <= 4 && currentGroup.totalQty + gap + reg.qty <= maxQty) {
1068
- const offset = reg.addr - currentGroup.startAddr;
1069
- currentGroup.totalQty = Math.max(currentGroup.totalQty, offset + reg.qty);
1070
- currentGroup.registers.push({ ...reg, offset });
1071
- } else {
1072
- groups.push(currentGroup);
1073
- currentGroup = {
1074
- startAddr: reg.addr,
1075
- totalQty: reg.qty,
1076
- registers: [{ ...reg, offset: 0 }],
1077
- };
1078
- }
1079
- }
1080
- }
1081
- if (currentGroup) {
1082
- groups.push(currentGroup);
1083
- }
1084
- return groups;
939
+ return buildReadGroups(registers, maxQty);
1085
940
  }
1086
941
 
1087
942
  /**
@@ -1136,9 +991,16 @@ class Sigenergy extends utils.Adapter {
1136
991
  'inverter.pv3Voltage': 'pv3Voltage',
1137
992
  'inverter.pv3Current': 'pv3Current',
1138
993
  };
1139
- if (map[name] !== undefined) {
1140
- this._currentData[map[name]] = value;
994
+ if (map[name] === undefined) {
995
+ return;
996
+ }
997
+ // A null value means the device marked the register as not valid
998
+ // (see ModbusConnection.INVALID_RAW). Keep the last known good value
999
+ // instead, so the statistics are not poisoned by a single bad poll.
1000
+ if (value === null) {
1001
+ return;
1141
1002
  }
1003
+ this._currentData[map[name]] = value;
1142
1004
  }
1143
1005
 
1144
1006
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.sigenergy",
3
- "version": "3.2.0",
3
+ "version": "3.3.1",
4
4
  "description": "ioBroker adapter for Sigenergy solar energy systems via Modbus TCP/RTU",
5
5
  "author": {
6
6
  "name": "ssbingo",