iobroker.sigenergy 3.1.2 → 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 +19 -1
- package/io-package.json +41 -2
- package/lib/readGroups.js +217 -0
- package/lib/registers.js +89 -12
- package/main.js +131 -242
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ Supports the Sigenergy Modbus Protocol V2.9 (released 2026-05-13).
|
|
|
49
49
|
| Component | Minimum version |
|
|
50
50
|
|-----------|-----------------|
|
|
51
51
|
| **js-controller** | >= 6.0.11 |
|
|
52
|
-
| **admin** | >= 8.
|
|
52
|
+
| **admin** | >= 7.8.23 |
|
|
53
53
|
| **Node.js** | >= 22 |
|
|
54
54
|
|
|
55
55
|
---
|
|
@@ -255,6 +255,24 @@ 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
|
+
|
|
267
|
+
### 3.2.0 (2026-09-04)
|
|
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
|
|
269
|
+
- (MMeinhardt1) feat: added EMS work mode 6 (Virtual Power Plant) to the register description and the mode map
|
|
270
|
+
- (MMeinhardt1) change: the `*HM` duration states now hold a bare `H:MM` value with unit `h`, instead of embedding an `h` suffix in the value itself
|
|
271
|
+
- (MMeinhardt1) fix: statistics objects are created with `extendObjectAsync`, so corrected units and roles reach installations where the object already exists
|
|
272
|
+
- (ssbingo) chore: admin minimum requirement lowered back to >= 7.8.23 to keep serving admin 7.x installations
|
|
273
|
+
- (ssbingo) fix: restored the JSDoc parameter descriptions on `_formatMinutesAsHM` to keep `npm run lint` warning-free
|
|
274
|
+
- (ssbingo) chore: repository checker findings — track `.vscode/settings.json` deliberately, bump `@alcalzone/release-script-plugin-license` to 5.2.2, pin `testing-action-deploy` to the major version again
|
|
275
|
+
|
|
258
276
|
### 3.1.2 (2026-08-06)
|
|
259
277
|
- (ssbingo) chore: resolved all 28 npm audit findings (3 critical, 11 high) — development tooling only, the published adapter is unchanged
|
|
260
278
|
- (ssbingo) chore: removed `@iobroker/dev-server`; it was the sole source of the critical findings via the deprecated `request` and `xmldom` packages, for which no fixed release exists. This also removes the `npm start` / `npm run watch` scripts
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
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
|
+
},
|
|
19
|
+
"3.2.0": {
|
|
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",
|
|
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",
|
|
22
|
+
"ru": "(MMeinhardt1) feat: режим работы EMS в виде текста, режим Virtual Power Plant, значения h:mm теперь имеют единицу 'h' вместо суффикса; объекты статистики обновляются при каждом запуске. (ssbingo) chore: требование к admin снижено до >=7.8.23",
|
|
23
|
+
"pt": "(MMeinhardt1) feat: modo de trabalho EMS como texto, modo Virtual Power Plant, valores h:mm passam a ter a unidade 'h' em vez de sufixo; os objetos de estatística são atualizados em cada arranque. (ssbingo) chore: requisito do admin reduzido para >=7.8.23",
|
|
24
|
+
"nl": "(MMeinhardt1) feat: EMS-werkmodus als tekst, modus Virtual Power Plant, h:mm-waarden krijgen nu eenheid 'h' in plaats van een achtervoegsel; statistiekobjecten worden bij elke start bijgewerkt. (ssbingo) chore: admin-vereiste terug naar >=7.8.23",
|
|
25
|
+
"fr": "(MMeinhardt1) feat : mode de fonctionnement EMS en texte clair, mode Virtual Power Plant, les valeurs h:mm portent désormais l'unité « h » au lieu d'un suffixe ; les objets de statistiques sont mis à jour à chaque démarrage. (ssbingo) chore : exigence admin ramenée à >=7.8.23",
|
|
26
|
+
"it": "(MMeinhardt1) feat: modalità di lavoro EMS come testo, modalità Virtual Power Plant, i valori h:mm ora hanno l'unità 'h' invece di un suffisso; gli oggetti statistici vengono aggiornati a ogni avvio. (ssbingo) chore: requisito admin riportato a >=7.8.23",
|
|
27
|
+
"es": "(MMeinhardt1) feat: modo de trabajo EMS como texto, modo Virtual Power Plant, los valores h:mm llevan ahora la unidad 'h' en lugar de un sufijo; los objetos de estadísticas se actualizan en cada arranque. (ssbingo) chore: requisito de admin reducido a >=7.8.23",
|
|
28
|
+
"pl": "(MMeinhardt1) feat: tryb pracy EMS jako tekst, tryb Virtual Power Plant, wartości h:mm mają teraz jednostkę 'h' zamiast sufiksu; obiekty statystyk są aktualizowane przy każdym starcie. (ssbingo) chore: wymaganie admin obniżone do >=7.8.23",
|
|
29
|
+
"uk": "(MMeinhardt1) feat: режим роботи EMS як текст, режим Virtual Power Plant, значення h:mm тепер мають одиницю 'h' замість суфікса; об'єкти статистики оновлюються при кожному запуску. (ssbingo) chore: вимогу до admin знижено до >=7.8.23",
|
|
30
|
+
"zh-cn": "(MMeinhardt1) feat: EMS 工作模式以纯文本提供、新增 Virtual Power Plant 模式、h:mm 值改用单位 'h' 而非后缀;统计对象在每次启动时更新。(ssbingo) chore: admin 要求下调回 >=7.8.23"
|
|
31
|
+
},
|
|
6
32
|
"3.1.2": {
|
|
7
33
|
"en": "(ssbingo) chore: all npm audit findings resolved (28 -> 0); removed @iobroker/dev-server, which pulled in unpatchable deprecated packages. Development tooling only — the adapter itself is unchanged.",
|
|
8
34
|
"de": "(ssbingo) chore: alle npm-audit-Findings behoben (28 -> 0); @iobroker/dev-server entfernt, das nicht patchbare veraltete Pakete mitzog. Betrifft nur das Entwicklungs-Tooling — der Adapter selbst ist unverändert.",
|
|
@@ -121,7 +147,7 @@
|
|
|
121
147
|
],
|
|
122
148
|
"globalDependencies": [
|
|
123
149
|
{
|
|
124
|
-
"admin": ">=8.
|
|
150
|
+
"admin": ">=7.8.23"
|
|
125
151
|
}
|
|
126
152
|
],
|
|
127
153
|
"licenseInformation": {
|
|
@@ -245,6 +271,19 @@
|
|
|
245
271
|
},
|
|
246
272
|
"native": {}
|
|
247
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
|
+
},
|
|
248
287
|
{
|
|
249
288
|
"_id": "info.modelType",
|
|
250
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
|
@@ -37,7 +37,7 @@ const PLANT_READ_REGISTERS = [
|
|
|
37
37
|
gain: null,
|
|
38
38
|
unit: '',
|
|
39
39
|
role: 'value',
|
|
40
|
-
desc: 'EMS work mode: 0=MaxSelfConsumption,1=AI,2=TOU,5=FullFeedIn,7=RemoteEMS,9=Custom',
|
|
40
|
+
desc: 'EMS work mode: 0=MaxSelfConsumption,1=AI,2=TOU,5=FullFeedIn,6=VirtualPowerPlant,7=RemoteEMS,9=Custom',
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
addr: 30004,
|
|
@@ -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)
|
|
@@ -3313,6 +3313,7 @@ const EMS_WORK_MODES = {
|
|
|
3313
3313
|
1: 'AI Mode',
|
|
3314
3314
|
2: 'TOU',
|
|
3315
3315
|
5: 'Full Feed-in to Grid',
|
|
3316
|
+
6: 'Virtual Power Plant',
|
|
3316
3317
|
7: 'Remote EMS',
|
|
3317
3318
|
9: 'Custom',
|
|
3318
3319
|
};
|
|
@@ -4980,6 +4981,80 @@ const DC_CHARGER_RUNNING_STATES = {
|
|
|
4980
4981
|
0x0a: 'Preparing (Insulation)',
|
|
4981
4982
|
};
|
|
4982
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
|
+
|
|
4983
5058
|
module.exports = {
|
|
4984
5059
|
PLANT_READ_REGISTERS,
|
|
4985
5060
|
PLANT_WRITE_REGISTERS,
|
|
@@ -4998,4 +5073,6 @@ module.exports = {
|
|
|
4998
5073
|
EMS_WORK_MODES,
|
|
4999
5074
|
REMOTE_EMS_MODES,
|
|
5000
5075
|
DC_CHARGER_RUNNING_STATES,
|
|
5076
|
+
PROTOCOL_SINCE,
|
|
5077
|
+
applySince,
|
|
5001
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
|
/**
|
|
@@ -1128,6 +983,7 @@ class Sigenergy extends utils.Adapter {
|
|
|
1128
983
|
'plant.essSoc': 'soc',
|
|
1129
984
|
'plant.essRatedEnergyCapacity': 'ratedCapacity',
|
|
1130
985
|
'plant.essDischargeSOC': 'cutoffSoc',
|
|
986
|
+
'plant.emsWorkMode': 'emsWorkMode',
|
|
1131
987
|
'inverter.pv1Voltage': 'pv1Voltage',
|
|
1132
988
|
'inverter.pv1Current': 'pv1Current',
|
|
1133
989
|
'inverter.pv2Voltage': 'pv2Voltage',
|
|
@@ -1141,15 +997,16 @@ class Sigenergy extends utils.Adapter {
|
|
|
1141
997
|
}
|
|
1142
998
|
|
|
1143
999
|
/**
|
|
1144
|
-
* Format a minute value as an "H:
|
|
1145
|
-
* Hours are not capped at 24.
|
|
1146
|
-
* null-vs-undefined write
|
|
1000
|
+
* Format a minute value as an "H:MM" string (e.g. 83 -> "1:23").
|
|
1001
|
+
* Hours are not capped at 24.
|
|
1002
|
+
* Distinguishes three cases so the caller's null-vs-undefined write
|
|
1003
|
+
* semantics (see _updateStatistics) are preserved:
|
|
1147
1004
|
* - undefined (statistic disabled via config) -> undefined (skip write)
|
|
1148
1005
|
* - null / NaN (statistic enabled but currently N/A) -> null (clears stale value)
|
|
1149
|
-
* - number -> formatted "H:
|
|
1006
|
+
* - number -> formatted "H:MM" string
|
|
1150
1007
|
*
|
|
1151
1008
|
* @param {number|null|undefined} totalMinutes - Duration in minutes, or null/undefined per the cases above
|
|
1152
|
-
* @returns {string|null|undefined} Formatted "H:
|
|
1009
|
+
* @returns {string|null|undefined} Formatted "H:MM" string, or null/undefined mirroring the input case
|
|
1153
1010
|
*/
|
|
1154
1011
|
_formatMinutesAsHM(totalMinutes) {
|
|
1155
1012
|
if (totalMinutes === undefined) {
|
|
@@ -1162,7 +1019,26 @@ class Sigenergy extends utils.Adapter {
|
|
|
1162
1019
|
const abs = Math.round(Math.abs(totalMinutes));
|
|
1163
1020
|
const hours = Math.floor(abs / 60);
|
|
1164
1021
|
const minutes = abs % 60;
|
|
1165
|
-
return `${sign}${hours}:${String(minutes).padStart(2, '0')}
|
|
1022
|
+
return `${sign}${hours}:${String(minutes).padStart(2, '0')}`;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Look up the plain-text label for the numeric EMS work mode (register
|
|
1027
|
+
* 30003 / plant.emsWorkMode), using the same EMS_WORK_MODES map already
|
|
1028
|
+
* used for the `common.states` metadata on plant.emsWorkMode. Unlike that
|
|
1029
|
+
* metadata (only understood by UIs/tools that render `common.states`),
|
|
1030
|
+
* this writes the human-readable text directly as the state value, so any
|
|
1031
|
+
* consumer (VIS, Node-RED, external scripts) can use it without needing
|
|
1032
|
+
* the lookup table.
|
|
1033
|
+
*
|
|
1034
|
+
* @param {number|undefined} mode - Raw value of plant.emsWorkMode
|
|
1035
|
+
* @returns {string|undefined} Plain-text label, or undefined if mode hasn't been read yet
|
|
1036
|
+
*/
|
|
1037
|
+
_formatEmsWorkMode(mode) {
|
|
1038
|
+
if (mode === undefined) {
|
|
1039
|
+
return undefined;
|
|
1040
|
+
}
|
|
1041
|
+
return EMS_WORK_MODES[mode] || `Unknown (${mode})`;
|
|
1166
1042
|
}
|
|
1167
1043
|
|
|
1168
1044
|
/**
|
|
@@ -1193,6 +1069,7 @@ class Sigenergy extends utils.Adapter {
|
|
|
1193
1069
|
'statistics.housePower': statsValues.housePower,
|
|
1194
1070
|
'statistics.currentSoc': statsValues.currentSoc,
|
|
1195
1071
|
'statistics.currentPvPower': statsValues.currentPvPower,
|
|
1072
|
+
'statistics.emsWorkMode': this._formatEmsWorkMode(this._currentData.emsWorkMode),
|
|
1196
1073
|
'statistics.dayMaxSoc': statsValues.dayMaxSoc,
|
|
1197
1074
|
'statistics.dayMinSoc': statsValues.dayMinSoc,
|
|
1198
1075
|
};
|
|
@@ -1674,7 +1551,7 @@ class Sigenergy extends utils.Adapter {
|
|
|
1674
1551
|
id: 'statistics.batteryTimeToFullHM',
|
|
1675
1552
|
name: 'Time until battery is fully charged (h:mm)',
|
|
1676
1553
|
type: 'string',
|
|
1677
|
-
unit: '',
|
|
1554
|
+
unit: 'h',
|
|
1678
1555
|
role: 'text',
|
|
1679
1556
|
},
|
|
1680
1557
|
{
|
|
@@ -1688,7 +1565,7 @@ class Sigenergy extends utils.Adapter {
|
|
|
1688
1565
|
id: 'statistics.batteryTimeRemainingHM',
|
|
1689
1566
|
name: 'Battery time remaining at current load (h:mm)',
|
|
1690
1567
|
type: 'string',
|
|
1691
|
-
unit: '',
|
|
1568
|
+
unit: 'h',
|
|
1692
1569
|
role: 'text',
|
|
1693
1570
|
},
|
|
1694
1571
|
{
|
|
@@ -1702,7 +1579,7 @@ class Sigenergy extends utils.Adapter {
|
|
|
1702
1579
|
id: 'statistics.batteryDailyChargeTimeHM',
|
|
1703
1580
|
name: 'Today: cumulative time spent charging (h:mm)',
|
|
1704
1581
|
type: 'string',
|
|
1705
|
-
unit: '',
|
|
1582
|
+
unit: 'h',
|
|
1706
1583
|
role: 'text',
|
|
1707
1584
|
},
|
|
1708
1585
|
{
|
|
@@ -1716,7 +1593,7 @@ class Sigenergy extends utils.Adapter {
|
|
|
1716
1593
|
id: 'statistics.batteryCoverageTodayHM',
|
|
1717
1594
|
name: 'Today: time battery covered consumption (h:mm)',
|
|
1718
1595
|
type: 'string',
|
|
1719
|
-
unit: '',
|
|
1596
|
+
unit: 'h',
|
|
1720
1597
|
role: 'text',
|
|
1721
1598
|
},
|
|
1722
1599
|
{
|
|
@@ -1782,6 +1659,13 @@ class Sigenergy extends utils.Adapter {
|
|
|
1782
1659
|
unit: 'kW',
|
|
1783
1660
|
role: 'value.power',
|
|
1784
1661
|
},
|
|
1662
|
+
{
|
|
1663
|
+
id: 'statistics.emsWorkMode',
|
|
1664
|
+
name: 'EMS work mode (plain text)',
|
|
1665
|
+
type: 'string',
|
|
1666
|
+
unit: '',
|
|
1667
|
+
role: 'text',
|
|
1668
|
+
},
|
|
1785
1669
|
{
|
|
1786
1670
|
id: 'statistics.dayMaxSoc',
|
|
1787
1671
|
name: 'Today maximum SOC',
|
|
@@ -1812,7 +1696,12 @@ class Sigenergy extends utils.Adapter {
|
|
|
1812
1696
|
},
|
|
1813
1697
|
native: {},
|
|
1814
1698
|
};
|
|
1815
|
-
|
|
1699
|
+
// extendObjectAsync (not setObjectNotExistsAsync) so that changes to
|
|
1700
|
+
// an existing definition (e.g. a corrected unit or role) are applied
|
|
1701
|
+
// on every adapter start, instead of being silently ignored forever
|
|
1702
|
+
// once the object has been created once. The state's current value
|
|
1703
|
+
// is untouched by this — only the object's common metadata is merged.
|
|
1704
|
+
await this.extendObjectAsync(s.id, statObj);
|
|
1816
1705
|
}
|
|
1817
1706
|
}
|
|
1818
1707
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.sigenergy",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "ioBroker adapter for Sigenergy solar energy systems via Modbus TCP/RTU",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "ssbingo",
|
|
@@ -30,12 +30,12 @@
|
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@alcalzone/release-script": "^5.2.1",
|
|
32
32
|
"@alcalzone/release-script-plugin-iobroker": "^5.2.0",
|
|
33
|
-
"@alcalzone/release-script-plugin-license": "^5.2.
|
|
33
|
+
"@alcalzone/release-script-plugin-license": "^5.2.2",
|
|
34
34
|
"@alcalzone/release-script-plugin-manual-review": "^5.2.0",
|
|
35
35
|
"@iobroker/adapter-dev": "^1.5.0",
|
|
36
36
|
"@iobroker/eslint-config": "^2.3.4",
|
|
37
37
|
"@iobroker/testing": "^5.3.0",
|
|
38
|
-
"@tsconfig/node22": "^22.0.
|
|
38
|
+
"@tsconfig/node22": "^22.0.6",
|
|
39
39
|
"@types/node": "^22.20.1"
|
|
40
40
|
},
|
|
41
41
|
"main": "main.js",
|