iobroker.sigenergy 3.2.0 → 3.3.0
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 +27 -1
- package/lib/readGroups.js +217 -0
- package/lib/registers.js +87 -11
- package/main.js +86 -231
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -255,6 +255,15 @@ Status and power readings for the DC charger.
|
|
|
255
255
|
|
|
256
256
|
## Changelog
|
|
257
257
|
|
|
258
|
+
### 3.3.0 (2026-09-07)
|
|
259
|
+
- (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
|
|
260
|
+
- (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
|
|
261
|
+
- (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
|
|
262
|
+
- (ssbingo) chore: grouped register reading moved to `lib/readGroups.js` and covered by unit tests
|
|
263
|
+
- (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
|
|
264
|
+
- (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
|
|
265
|
+
- (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
|
|
266
|
+
|
|
258
267
|
### 3.2.0 (2026-09-04)
|
|
259
268
|
- (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
269
|
- (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,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "sigenergy",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.3.0",
|
|
5
5
|
"news": {
|
|
6
|
+
"3.3.0": {
|
|
7
|
+
"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",
|
|
8
|
+
"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",
|
|
9
|
+
"ru": "(ssbingo) feat: регистры фильтруются по обнаруженной версии протокола Modbus, поэтому устройство больше не запрашивается о регистрах, неизвестных его прошивке; отклонённые группы регистров перечитываются по одному, поэтому один неподдерживаемый регистр больше не блокирует всю группу; новое состояние info.protocolVersion",
|
|
10
|
+
"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",
|
|
11
|
+
"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",
|
|
12
|
+
"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",
|
|
13
|
+
"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",
|
|
14
|
+
"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",
|
|
15
|
+
"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",
|
|
16
|
+
"uk": "(ssbingo) feat: регістри фільтруються за виявленою версією протоколу Modbus, тому пристрій більше не запитують про регістри, невідомі його прошивці; відхилені групи регістрів тепер перечитуються по одному, тож один непідтримуваний регістр більше не блокує всю групу; новий стан info.protocolVersion",
|
|
17
|
+
"zh-cn": "(ssbingo) feat: 按检测到的 Modbus 协议版本过滤寄存器,不再向设备请求其固件不支持的寄存器;被拒绝的寄存器组改为逐个读取,单个不支持的寄存器不再导致整组失效;新增状态 info.protocolVersion"
|
|
18
|
+
},
|
|
6
19
|
"3.2.0": {
|
|
7
20
|
"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
21
|
"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 +271,19 @@
|
|
|
258
271
|
},
|
|
259
272
|
"native": {}
|
|
260
273
|
},
|
|
274
|
+
{
|
|
275
|
+
"_id": "info.protocolVersion",
|
|
276
|
+
"type": "state",
|
|
277
|
+
"common": {
|
|
278
|
+
"name": "Highest Modbus protocol version confirmed by probing (0 = below V2.6 or not detected yet)",
|
|
279
|
+
"type": "number",
|
|
280
|
+
"role": "value",
|
|
281
|
+
"read": true,
|
|
282
|
+
"write": false,
|
|
283
|
+
"def": 0
|
|
284
|
+
},
|
|
285
|
+
"native": {}
|
|
286
|
+
},
|
|
261
287
|
{
|
|
262
288
|
"_id": "info.modelType",
|
|
263
289
|
"type": "state",
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
this.
|
|
147
|
-
this.
|
|
148
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
/**
|