iobroker.sun2000 0.1.2 → 0.2.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 +28 -8
- package/io-package.json +69 -70
- package/lib/modbus_connect.js +13 -18
- package/lib/register.js +157 -55
- package/lib/tools.js +79 -0
- package/main.js +89 -70
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
|
|
15
15
|
Read register data from Huawei SUN2000 inverter and LUNA2000 battery using Modbus TCP.
|
|
16
16
|
|
|
17
|
+
Feel free to follow the discussions in the german [iobroker forum](https://forum.iobroker.net/topic/71768/test-adapter-sun2000-v0-1-x-huawei-wechselrichter)
|
|
18
|
+
|
|
17
19
|
Modbus interface definition (Issue 5, 2023-02-16):
|
|
18
20
|
https://forum.iobroker.net/assets/uploads/files/1699119419919-solar-inverter-modbus-interface-definitions-v5.pdf
|
|
19
21
|
|
|
20
|
-
The development of this adapter was inspired by discussions from the forum thread https://forum.iobroker.net/topic/53005/huawei-sun2000-iobroker-via-js-script-funktioniert and the iobroker javascript https://github.com/ChrisBCH/SunLuna2000_iobroker.
|
|
21
|
-
|
|
22
22
|
## Supported hardware
|
|
23
23
|
|
|
24
24
|
* HUAWEI Inverter (SUN2000 Serie) M1
|
|
@@ -26,13 +26,21 @@ The development of this adapter was inspired by discussions from the forum threa
|
|
|
26
26
|
* HUAWEI Luna2000 Battery
|
|
27
27
|
* HUAWEI Smart Power Sensor DTSU666-H or DDSU666-H
|
|
28
28
|
|
|
29
|
+
[Huawei product information](https://solar.huawei.com/en/professionals/all-products?residential-smart-pv)
|
|
30
|
+
|
|
29
31
|
## Feature list
|
|
30
32
|
|
|
31
33
|
* Maximum 5 inverters (master/slave) can be processed, each with a battery module (max. 30kWh).
|
|
32
|
-
*
|
|
34
|
+
* Real-time values such as input power, output power, charging/discharging power and the grid consumption are read out at a fixed interval.
|
|
33
35
|
* States are only written for changed data from the inverter. This relieves the burden on the iobroker instance.
|
|
34
36
|
* The states “inputPower” or “activePower” in the “collected” path can be monitored with a “was updated” trigger element. Because these states are always written within the set interval.
|
|
35
37
|
|
|
38
|
+
## Settings
|
|
39
|
+
|
|
40
|
+
* `address`: Inverter IP address
|
|
41
|
+
* `port`: Inverter modbus port (default: 502)
|
|
42
|
+
* `modbusIds`: inverter IDs, separated with "," (default: 1, max. 5 inverters)
|
|
43
|
+
* `updateInterval`: Fast update interval (default: 20 sec, smallest 5 seconds per inverter)
|
|
36
44
|
|
|
37
45
|
## Configure inverters
|
|
38
46
|
|
|
@@ -47,12 +55,12 @@ You may also need a password to connect to the inverters own WLAN: `Changeme`
|
|
|
47
55
|
After login on the inverter go to `Settings` (Einstellungen) > `Communication configuration` (Kommunikationskonfiguration) > `Dongle parameter settings` (Dongle‐Parametereinstellungen) > `Modbus TCP` > Activate the `connection without restriction` (Verbindung uneingeschränkt aktivieren). You can also enter the Modbus comm address at the same time read out.
|
|
48
56
|
If you use two inverters, then connect to the second inverter and read the communication address there too.
|
|
49
57
|
|
|
50
|
-
|
|
58
|
+
[How activate 'Modbus TCP' - from huawei forum](https://forum.huawei.com/enterprise/en/modbus-tcp-guide/thread/789585-100027)
|
|
59
|
+
|
|
60
|
+
## Inspiration
|
|
61
|
+
|
|
62
|
+
The development of this adapter was inspired by discussions from the forum thread https://forum.iobroker.net/topic/53005/huawei-sun2000-iobroker-via-js-script-funktioniert and the iobroker javascript https://github.com/ChrisBCH/SunLuna2000_iobroker.
|
|
51
63
|
|
|
52
|
-
* `address`: Inverter IP address
|
|
53
|
-
* `port`: Inverter modbus port (default: 502)
|
|
54
|
-
* `modbusIds`: inverter IDs, separated with "," (default: 1, max. 5 inverters)
|
|
55
|
-
* `updateInterval`: Fast update interval (default: 20 sec, smallest 5 seconds per inverter)
|
|
56
64
|
|
|
57
65
|
## Changelog
|
|
58
66
|
|
|
@@ -60,6 +68,18 @@ If you use two inverters, then connect to the second inverter and read the commu
|
|
|
60
68
|
Placeholder for the next version (at the beginning of the line):
|
|
61
69
|
### **WORK IN PROGRESS**
|
|
62
70
|
-->
|
|
71
|
+
### 0.2.0 (2024-01-24)
|
|
72
|
+
* [Add sun2000 to latest](https://github.com/ioBroker/ioBroker.repositories/pull/3219)
|
|
73
|
+
* improve error handling (#34)
|
|
74
|
+
* add simple optimizer info
|
|
75
|
+
* Riemann sum of input power with energy loss for new state `dailySolarYield`
|
|
76
|
+
* try to recreate the `yield today` from the fusion portal
|
|
77
|
+
|
|
78
|
+
### 0.1.3 (2024-01-17)
|
|
79
|
+
* display the data from PV strings (#27)
|
|
80
|
+
* optimize the timing of interval loop
|
|
81
|
+
* improved handling of read timeouts from more then 2 inverters
|
|
82
|
+
|
|
63
83
|
### 0.1.2 (2024-01-12)
|
|
64
84
|
* fix: no Data if interval less 20 sec (#24)
|
|
65
85
|
* prepare collected values more precisely
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "sun2000",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"news": {
|
|
6
|
+
"0.2.0": {
|
|
7
|
+
"en": "improve error handling (#34)\nadd simple optimizer info \nRiemann sum of input power with energy loss for new state `dailySolarYield`\ntry to recreate the `yield today` from the fusion portal",
|
|
8
|
+
"de": "fehlerbehandlung verbessern (#34)\neinfache optimierer-info hinzufügen\nRiemann Summe der Eingangsleistung mit Energieverlust für neuen Zustand `dailySolarYield `\nversuchen, den `yield heute` aus dem fusionsportal wiederherzustellen",
|
|
9
|
+
"ru": "улучшить обработку ошибок (#34)\nдобавить простую информацию о оптимизации\nRiemann Сумма входной мощности с потерей энергии для нового состояния `dailySolarYield \"\nпопытаться воссоздать «урожай сегодня» с портала синтеза",
|
|
10
|
+
"pt": "melhorar o manuseio de erros (#34)\nadicionar informações de otimização simples\nRiemann soma de energia de entrada com perda de energia para novo estado `dailySolarYield \"\ntentar recriar o `yield hoje` do portal de fusão",
|
|
11
|
+
"nl": "verbetering van de foutafhandeling (#34)\nvoeg eenvoudige optimalisatie info toe\nRiemann-som van het ingangsvermogen met energieverlies voor nieuwe toestand Wat\nproberen om de te recreëren van vandaag van de fusie portal",
|
|
12
|
+
"fr": "améliorer la gestion des erreurs (#34)\najouter des informations d'optimisation simples\nRiemann somme de puissance d'entrée avec perte d'énergie pour le nouvel état `dailySolarYield \"\nessayer de recréer le « rendement aujourd'hui » du portail de fusion",
|
|
13
|
+
"it": "migliorare la gestione degli errori (#34)\naggiungere semplici informazioni di ottimizzazione\nRiemann somma di potenza di ingresso con perdita di energia per nuovo stato `dailySolarYield #\ncercare di ricreare il `yield oggi` dal portale fusion",
|
|
14
|
+
"es": "mejorar el manejo de errores (#34)\nañadir información de optimización simple\nRiemann suma de poder de entrada con pérdida de energía para el nuevo estado `dailySolarYield `\ntratar de recrear el `yield hoy' del portal de fusión",
|
|
15
|
+
"pl": "poprawić obsługę błędów (# 34)\ndodaj proste informacje o optymalizacji\nRiemann suma mocy wejściowej z utratą energii dla nowego stanu \"dailySolarYeld '\nspróbuj odtworzyć \"wydajność dzisiaj\" z portalu syntezy jądrowej",
|
|
16
|
+
"uk": "поліпшення обробки помилок (#34)\nдодати просту оптимізацію\nСума вхідної енергії з втратою енергії для нового стану `dailySolarYield й\nнамагатися відтворити `yield сьогодні` з порталу fusion",
|
|
17
|
+
"zh-cn": "改进错误处理(# 34)\n添加简单的优化信息\nRiemann 输入功率总和, `\n尝试从聚变门户重现“今天”"
|
|
18
|
+
},
|
|
19
|
+
"0.1.3": {
|
|
20
|
+
"en": "display the data from PV strings (#27)\noptimize the timing of interval loop\nimproved handling of read timeouts from more then 2 inverters",
|
|
21
|
+
"de": "die Daten von PV-Strings anzeigen (#27)\noptimieren sie das timing der intervallschleife\nverbesserte handhabung von lesezeitausgängen von mehr als 2 wechselrichtern",
|
|
22
|
+
"ru": "отобразить данные из строк PV (#27)\nоптимизировать время интервального цикла\nулучшенная обработка считываемых таймаутов от более чем 2 инверторов",
|
|
23
|
+
"pt": "exibir os dados de strings PV (#27)\notimizar o tempo de loop de intervalo\nmelhor manuseio de leitura timeouts de mais, em seguida, 2 inversores",
|
|
24
|
+
"nl": "weergave van de gegevens van PV strings (#27)\noptimaliseren van de timing van intervallus\nverbeterde behandeling van leestijd-outs van meer dan 2 inverters",
|
|
25
|
+
"fr": "afficher les données des chaînes PV (#27)\noptimiser le timing de la boucle d'intervalle\namélioration de la gestion des temps de lecture de plus de 2 onduleurs",
|
|
26
|
+
"it": "visualizzare i dati dalle stringhe PV (#27)\nottimizzare la tempistica del loop di intervallo\nmigliore gestione dei timeout di lettura da più di 2 inverter",
|
|
27
|
+
"es": "mostrar los datos de cadenas PV (#27)\noptimizar el tiempo del bucle de intervalo\nmejor manejo de tiempo de lectura de más de 2 inversores",
|
|
28
|
+
"pl": "wyświetla dane z łańcuchów fotowoltaicznych (# 27)\noptymalizacja czasu pętli interwałowej\nulepszona obsługa timeout odczytu z więcej niż 2 inwerterów",
|
|
29
|
+
"uk": "відображення даних з ПВ-рядок (#27)\nоптимізуйте частимізацію інтервалної петлі\nполіпшена обробка часу читання з більш ніж 2 інверторів",
|
|
30
|
+
"zh-cn": "显示 PV 字符串中的数据 (# 27)\n优化间隔循环的时间\n改进对超过2个反转器的断读处理"
|
|
31
|
+
},
|
|
6
32
|
"0.1.2": {
|
|
7
33
|
"en": "fix: no Data if interval less 20 sec (#24)\nprepare collected values more precisely\nexpand up to 5 inverters #18\nfix problems with multiple inverters",
|
|
8
34
|
"de": "fix: keine Daten, wenn Intervall weniger 20 sec (#24)\ndie gesammelten werte genauer\nbis zu 5 wechselrichtern #18 erweitern\nprobleme mit mehreren wechselrichtern beheben",
|
|
@@ -16,79 +42,36 @@
|
|
|
16
42
|
"uk": "виправити: немає даних, якщо інтервал менше 20 сек (#24)\nпідготовка зібраних значень точно\nрозширити до 5 інверторів #18\nвиправити проблеми з декількома інверторами",
|
|
17
43
|
"zh-cn": "固定: 如果间隔小于20秒(# 24) 则无数据\n更准确地编制收集的数值\n扩展至5个反转器 # 18\n解决多个反转器的问题"
|
|
18
44
|
},
|
|
19
|
-
"0.1.2-alpha.3": {
|
|
20
|
-
"en": "fix: wrong deploying date",
|
|
21
|
-
"de": "fix: falsches bereitstellungsdatum",
|
|
22
|
-
"ru": "исправить: неправильная дата развертывания",
|
|
23
|
-
"pt": "correção: data de implantação errada",
|
|
24
|
-
"nl": "fix: verkeerde datum voor ingebruikname",
|
|
25
|
-
"fr": "correction : mauvaise date de déploiement",
|
|
26
|
-
"it": "fix: la data di distribuzione sbagliata",
|
|
27
|
-
"es": "fijado: fecha de despliegue incorrecta",
|
|
28
|
-
"pl": "fix: nieprawidłowa data rozmieszczenia",
|
|
29
|
-
"uk": "виправити: неправильна дата розгортання",
|
|
30
|
-
"zh-cn": "固定: 部署日期错误"
|
|
31
|
-
},
|
|
32
|
-
"0.1.2-alpha.2": {
|
|
33
|
-
"en": "fix: no Data if interval less 20 sec (#24)",
|
|
34
|
-
"de": "fix: keine Daten, wenn Intervall weniger 20 sec (#24)",
|
|
35
|
-
"ru": "исправление: нет данных, если интервал менее 20 секунд (#24)",
|
|
36
|
-
"pt": "correção: não Dados se intervalo menos 20 segundos (#24)",
|
|
37
|
-
"nl": "fix: geen Gegevens indien interval minder 20 sec (#24)",
|
|
38
|
-
"fr": "correction : pas de données si l'intervalle est inférieur à 20 secondes (#24)",
|
|
39
|
-
"it": "correzione: no Dati se intervallo meno 20 sec (#24)",
|
|
40
|
-
"es": "fijado: no Datos si intervalo menos 20 segundos (#24)",
|
|
41
|
-
"pl": "fix: no Data if interval less 20 sec (# 24)",
|
|
42
|
-
"uk": "виправити: немає даних, якщо інтервал менше 20 сек (#24)",
|
|
43
|
-
"zh-cn": "固定: 如果间隔小于20秒(# 24) 则无数据"
|
|
44
|
-
},
|
|
45
|
-
"0.1.2-alpha.1": {
|
|
46
|
-
"en": "deploy npm package",
|
|
47
|
-
"de": "npm paket bereitstellen",
|
|
48
|
-
"ru": "развернуть пакет npm",
|
|
49
|
-
"pt": "implementar pacote npm",
|
|
50
|
-
"nl": "npm-pakket implementeren",
|
|
51
|
-
"fr": "déployer le paquet npm",
|
|
52
|
-
"it": "distribuire il pacchetto npm",
|
|
53
|
-
"es": "paquete npm",
|
|
54
|
-
"pl": "rozmieszczenie pakietu npm",
|
|
55
|
-
"uk": "пакет npm",
|
|
56
|
-
"zh-cn": "部署 npm 软件包"
|
|
57
|
-
},
|
|
58
|
-
"0.1.2-alpha.0": {
|
|
59
|
-
"en": "prepare collected values more precisely\nexpand up to 5 inverters #18\nfix problems with multiple inverters",
|
|
60
|
-
"de": "die gesammelten werte genauer\nbis zu 5 wechselrichtern #18 erweitern\nprobleme mit mehreren wechselrichtern beheben",
|
|
61
|
-
"ru": "готовить собранные значения точнее\n#18\nисправить проблемы с несколькими инверторами",
|
|
62
|
-
"pt": "preparar valores coletados mais precisamente\nexpandir até 5 inversores #18\ncorrigir problemas com vários inversores",
|
|
63
|
-
"nl": "verzamelde waarden nauwkeuriger voorbereiden\nuit te breiden tot 5 omvormers #18\nproblemen oplossen met meerdere inverters",
|
|
64
|
-
"fr": "préparer plus précisément les valeurs collectées\nétendre jusqu'à 5 onduleurs #18\nrésoudre les problèmes avec plusieurs onduleurs",
|
|
65
|
-
"it": "preparare i valori raccolti più precisamente\nespandere fino a 5 inverter #18\nrisolvere problemi con più inverter",
|
|
66
|
-
"es": "preparar los valores recogidos con mayor precisión\nampliar hasta 5 inversores #18\nsolucionar problemas con múltiples inversores",
|
|
67
|
-
"pl": "dokładniej przygotować zebrane wartości\nrozszerzyć do 5 inwerterów # 18\nrozwiązać problemy z wieloma inwerterami",
|
|
68
|
-
"uk": "підготовка зібраних значень точно\nрозширити до 5 інверторів #18\nвиправити проблеми з декількома інверторами",
|
|
69
|
-
"zh-cn": "更准确地编制收集的数值\n扩展至5个反转器 # 18\n解决多个反转器的问题"
|
|
70
|
-
},
|
|
71
45
|
"0.1.1": {
|
|
72
46
|
"en": "fix some collected values",
|
|
73
|
-
"de": "Korrektur einiger der gesammelten Werte (collected values)"
|
|
47
|
+
"de": "Korrektur einiger der gesammelten Werte (collected values)",
|
|
48
|
+
"ru": "исправить некоторые собранные значения",
|
|
49
|
+
"pt": "corrigir alguns valores coletados",
|
|
50
|
+
"nl": "sommige verzamelde waarden vast te stellen",
|
|
51
|
+
"fr": "fixer certaines valeurs collectées",
|
|
52
|
+
"it": "correggere alcuni valori raccolti",
|
|
53
|
+
"es": "fijar algunos valores recogidos",
|
|
54
|
+
"pl": "naprawić niektóre zebrane wartości",
|
|
55
|
+
"uk": "виправити деякі зібрані значення",
|
|
56
|
+
"zh-cn": "固定一些收集的值"
|
|
74
57
|
}
|
|
75
58
|
},
|
|
76
59
|
"titleLang": {
|
|
77
|
-
"en": "sun2000",
|
|
78
|
-
"de": "sun2000",
|
|
79
|
-
"ru": "
|
|
80
|
-
"pt": "
|
|
81
|
-
"nl": "
|
|
82
|
-
"fr": "
|
|
83
|
-
"it": "
|
|
84
|
-
"es": "
|
|
85
|
-
"pl": "
|
|
86
|
-
"uk": "sun2000",
|
|
87
|
-
"zh-cn": "
|
|
60
|
+
"en": "Huawei sun2000 inverters",
|
|
61
|
+
"de": "Huawei sun2000 Wechselrichtern",
|
|
62
|
+
"ru": "Huawei sun2000 inverters",
|
|
63
|
+
"pt": "Huawei sun2000 inverters",
|
|
64
|
+
"nl": "Huawei sun2000 inverters",
|
|
65
|
+
"fr": "Huawei sun2000 inverters",
|
|
66
|
+
"it": "Huawei sun2000 inverters",
|
|
67
|
+
"es": "Huawei sun2000 inverters",
|
|
68
|
+
"pl": "Huawei sun2000 inverters",
|
|
69
|
+
"uk": "Huawei sun2000 inverters",
|
|
70
|
+
"zh-cn": "Huawei sun2000 inverters"
|
|
88
71
|
},
|
|
89
72
|
"desc": {
|
|
90
|
-
"en": "Read data from Huawei SUN2000
|
|
91
|
-
"de": "
|
|
73
|
+
"en": "Read data from Huawei SUN2000 inverters and LUNA2000 battery using Modbus TCP\n",
|
|
74
|
+
"de": "Auslesen von Huawei SUN2000 Wechselrichtern und LUNA2000 Akkus über Modbus TCP\n",
|
|
92
75
|
"ru": "Прочитайте данные от Huawei SUN2000 inverter и LUNA2000 батареи с помощью Modbus TCP\n",
|
|
93
76
|
"pt": "Leia dados do inversor Huawei SUN2000 e da bateria LUNA2000 usando Modbus TCP\n",
|
|
94
77
|
"nl": "Lees gegevens van Huawei SUN2000 inverter en LUNA2000 batterij met Modbus TCP\n",
|
|
@@ -186,7 +169,7 @@
|
|
|
186
169
|
"common": {
|
|
187
170
|
"name": {
|
|
188
171
|
"en": "Inverter IP",
|
|
189
|
-
"de": "
|
|
172
|
+
"de": "Wechselrichter IP"
|
|
190
173
|
},
|
|
191
174
|
"type": "string",
|
|
192
175
|
"role": "indicator.ip",
|
|
@@ -215,8 +198,8 @@
|
|
|
215
198
|
"type": "state",
|
|
216
199
|
"common": {
|
|
217
200
|
"name": {
|
|
218
|
-
"en": "
|
|
219
|
-
"de": "Modbus
|
|
201
|
+
"en": "Modbus IDs of inverters",
|
|
202
|
+
"de": "Modbus IDs der Wechselrichters"
|
|
220
203
|
},
|
|
221
204
|
"type": "string",
|
|
222
205
|
"role": "indicator.id",
|
|
@@ -240,6 +223,22 @@
|
|
|
240
223
|
"desc": "modbus update interval",
|
|
241
224
|
"unit": "sec"
|
|
242
225
|
}
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"_id": "info.JSONhealth",
|
|
229
|
+
"type": "state",
|
|
230
|
+
"common": {
|
|
231
|
+
"name": {
|
|
232
|
+
"en": "health information",
|
|
233
|
+
"de": "Gesundheitsinformation"
|
|
234
|
+
},
|
|
235
|
+
"type": "string",
|
|
236
|
+
"role": "indicator.alarm.health",
|
|
237
|
+
"read": true,
|
|
238
|
+
"write": false,
|
|
239
|
+
"desc": "",
|
|
240
|
+
"unit": ""
|
|
241
|
+
}
|
|
243
242
|
}
|
|
244
243
|
]
|
|
245
244
|
}
|
package/lib/modbus_connect.js
CHANGED
|
@@ -22,7 +22,6 @@ class ModbusConnect extends DeviceInterface {
|
|
|
22
22
|
constructor(adapterInstance,ip,port) {
|
|
23
23
|
super(ip,port);
|
|
24
24
|
this.adapter = adapterInstance;
|
|
25
|
-
this.lastErrno = 0;
|
|
26
25
|
this._id = 0;
|
|
27
26
|
}
|
|
28
27
|
|
|
@@ -35,15 +34,11 @@ class ModbusConnect extends DeviceInterface {
|
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
close() {
|
|
38
|
-
return new Promise((resolve
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (err) reject(err);
|
|
42
|
-
resolve(data);
|
|
43
|
-
} );
|
|
44
|
-
} else {
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
this.client.close(() => {
|
|
39
|
+
//if (err) reject(err);
|
|
45
40
|
resolve({});
|
|
46
|
-
}
|
|
41
|
+
} );
|
|
47
42
|
});
|
|
48
43
|
}
|
|
49
44
|
|
|
@@ -70,21 +65,22 @@ class ModbusConnect extends DeviceInterface {
|
|
|
70
65
|
}
|
|
71
66
|
|
|
72
67
|
|
|
73
|
-
|
|
74
|
-
return new Promise((resolve
|
|
75
|
-
this.client.destroy((
|
|
76
|
-
if (err) reject(err);
|
|
77
|
-
resolve(
|
|
68
|
+
_destroy() {
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
this.client.destroy(() => {
|
|
71
|
+
//if (err) reject(err);
|
|
72
|
+
resolve({});
|
|
78
73
|
});
|
|
79
74
|
});
|
|
80
75
|
}
|
|
81
76
|
|
|
82
77
|
async _checkError(err) {
|
|
78
|
+
this.adapter.log.debug('modbus client error: '+JSON.stringify(err));
|
|
79
|
+
await this.close();
|
|
83
80
|
if (err.modbusCode == null) {
|
|
81
|
+
this.adapter.log.debug('modbus client destroyed');
|
|
84
82
|
//https://github.com/yaacov/node-modbus-serial/issues/96
|
|
85
|
-
//this.adapter.log.debug('Client destroy!');
|
|
86
83
|
//await this._destroy();
|
|
87
|
-
//this.adapter.log.debug('Client destroy!');
|
|
88
84
|
await this._create();
|
|
89
85
|
}
|
|
90
86
|
}
|
|
@@ -93,7 +89,7 @@ class ModbusConnect extends DeviceInterface {
|
|
|
93
89
|
async connect(repeatCounter = 0) {
|
|
94
90
|
try {
|
|
95
91
|
this.adapter.log.info('Open Connection...');
|
|
96
|
-
await this.close();
|
|
92
|
+
this.isOpen() && await this.close();
|
|
97
93
|
await this.client.setTimeout(5000);
|
|
98
94
|
await this.client.connectTcpRTUBuffered(this.ipAddress, { port: this.port} ); //hang
|
|
99
95
|
await this.delay(1000);
|
|
@@ -113,7 +109,6 @@ class ModbusConnect extends DeviceInterface {
|
|
|
113
109
|
async readHoldingRegisters(address, length) {
|
|
114
110
|
try {
|
|
115
111
|
await this.open();
|
|
116
|
-
//this.adapter.log.debug('client.setID: '+this._id);
|
|
117
112
|
await this.client.setID(this._id);
|
|
118
113
|
const data = await this.client.readHoldingRegisters(address, length);
|
|
119
114
|
return data.data;
|
package/lib/register.js
CHANGED
|
@@ -1,39 +1,14 @@
|
|
|
1
1
|
|
|
2
2
|
const {deviceType,batteryStatus,dataRefreshRate,dataType} = require(__dirname + '/types.js');
|
|
3
|
-
|
|
4
|
-
class StateMap {
|
|
5
|
-
constructor () {
|
|
6
|
-
this.stateMap = new Map();
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
get(id) {
|
|
10
|
-
return this.stateMap.get(id);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
set(id, value, options) {
|
|
14
|
-
if (options?.type == 'number' && isNaN(value)) return;
|
|
15
|
-
if (value !== null) {
|
|
16
|
-
if (options?.renew || this.get(id)?.value !== value) {
|
|
17
|
-
if (options?.stored ) {
|
|
18
|
-
this.stateMap.set(id, {id: id, value: value, stored: options.stored});
|
|
19
|
-
} else {
|
|
20
|
-
this.stateMap.set(id, {id: id, value: value});
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
values () {
|
|
27
|
-
return this.stateMap.values();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
}
|
|
3
|
+
const {StateMap,RiemannSum} = require(__dirname + '/tools.js');
|
|
31
4
|
|
|
32
5
|
class Registers {
|
|
33
6
|
constructor(adapterInstance) {
|
|
34
7
|
this.adapter = adapterInstance;
|
|
35
8
|
this.stateCache = new StateMap();
|
|
36
|
-
|
|
9
|
+
for (const inverter of this.adapter.inverters) {
|
|
10
|
+
inverter.solarSum = new RiemannSum();
|
|
11
|
+
}
|
|
37
12
|
this._loadStates();
|
|
38
13
|
|
|
39
14
|
//https://github.com/ioBroker/ioBroker.docs/blob/master/docs/en/dev/stateroles.md
|
|
@@ -75,6 +50,9 @@ class Registers {
|
|
|
75
50
|
},
|
|
76
51
|
{
|
|
77
52
|
state: {id: 'derived.inputPowerWithEfficiencyLoss', name: 'input power with efficiency loss', type: 'number', unit: 'kW', role: 'value.power', desc: ''}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
state: {id: 'derived.dailySolarYield', name: 'Solar Yield Today', type: 'number', unit: 'kWh', role: 'value.power.consumption', desc: 'Riemann sum of input power with efficiency loss'}
|
|
78
56
|
}
|
|
79
57
|
],
|
|
80
58
|
postHook: (path) => {
|
|
@@ -95,6 +73,9 @@ class Registers {
|
|
|
95
73
|
}
|
|
96
74
|
//inputPower_with_efficiency_loss
|
|
97
75
|
this.stateCache.set(path+'derived.inputPowerWithEfficiencyLoss', inPowerEff, {type: 'number'});
|
|
76
|
+
|
|
77
|
+
this.inverterInfo.solarSum.add(inPowerEff); //riemannSum
|
|
78
|
+
this.stateCache.set(path+'derived.dailySolarYield', this.inverterInfo.solarSum.sum, {type: 'number'});
|
|
98
79
|
}
|
|
99
80
|
},
|
|
100
81
|
{
|
|
@@ -151,6 +132,10 @@ class Registers {
|
|
|
151
132
|
state: {id: 'info.ratedPower', name: 'Rated power', type: 'number', unit: 'kW', role: 'value.power'},
|
|
152
133
|
register: {reg: 30073, type: dataType.int32, gain:1000}
|
|
153
134
|
},
|
|
135
|
+
{
|
|
136
|
+
state: {id: 'info.numberPVStrings', name: 'Number of PV Strings', type: 'number', unit: '', role: 'value'},
|
|
137
|
+
register: {reg: 30071, type: dataType.uint16}
|
|
138
|
+
},
|
|
154
139
|
{
|
|
155
140
|
state: {id: 'info.numberMPPTrackers', name: 'Number of MPP trackers', type: 'number', unit: '', role: 'value'},
|
|
156
141
|
register: {reg: 30072, type: dataType.uint16}
|
|
@@ -326,11 +311,49 @@ class Registers {
|
|
|
326
311
|
{
|
|
327
312
|
state: {id: 'dailyEnergyYield', name: 'Daily Energy Yield', type: 'number', unit: 'kWh', role: 'value.power.produced'},
|
|
328
313
|
register: {reg: 32114, type: dataType.uint32, gain: 100}
|
|
329
|
-
}
|
|
314
|
+
}
|
|
315
|
+
],
|
|
316
|
+
preHook: (path,reg) => {
|
|
317
|
+
const noPVString = this.stateCache.get(path+'info.numberPVStrings')?.value;
|
|
318
|
+
if (noPVString > 0) {
|
|
319
|
+
if (!stringFieldsTemplate.generated) stringFieldsTemplate.generated = 0;
|
|
320
|
+
if (stringFieldsTemplate.generated < noPVString) {
|
|
321
|
+
for (let i = stringFieldsTemplate.generated; i < noPVString; i++) {
|
|
322
|
+
//clonen
|
|
323
|
+
//const statePV = Object.assign({},stringFieldsTemplate.states[0]);
|
|
324
|
+
const statePV = JSON.parse(JSON.stringify(stringFieldsTemplate.states[0]));
|
|
325
|
+
const stateCu = JSON.parse(JSON.stringify(stringFieldsTemplate.states[1]));
|
|
326
|
+
const statePo = JSON.parse(JSON.stringify(stringFieldsTemplate.states[2]));
|
|
327
|
+
statePV.state.id = 'string.PV'+(i+1)+'Voltage';
|
|
328
|
+
statePV.register.reg = (stringFieldsTemplate.states[0].register?.reg ?? 0)+ (i*2);
|
|
329
|
+
statePV.register.type = stringFieldsTemplate.states[0].register?.type; //types are not copied?!
|
|
330
|
+
stateCu.state.id = 'string.PV'+(i+1)+'Current';
|
|
331
|
+
stateCu.register.reg = (stringFieldsTemplate.states[1].register?.reg ?? 0)+ (i*2);
|
|
332
|
+
stateCu.register.type = stringFieldsTemplate.states[1].register?.type;
|
|
333
|
+
statePo.state.id = 'string.PV'+(i+1)+'Power';
|
|
334
|
+
reg.states.push(statePV);
|
|
335
|
+
reg.states.push(stateCu);
|
|
336
|
+
reg.states.push(statePo);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
stringFieldsTemplate.generated = noPVString;
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
postHook: (path) => {
|
|
343
|
+
const noPVString = this.stateCache.get(path+'info.numberPVStrings')?.value;
|
|
344
|
+
if (noPVString > 0) {
|
|
345
|
+
for (let i = 1; i <= noPVString; i++) {
|
|
346
|
+
const voltage = this.stateCache.get(path+'string.PV'+i+'Voltage')?.value;
|
|
347
|
+
const current = this.stateCache.get(path+'string.PV'+i+'Current')?.value;
|
|
348
|
+
this.stateCache.set(path+'string.PV'+i+'Power',Math.round(voltage*current),{type: 'number'});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
330
353
|
},
|
|
331
354
|
{
|
|
332
355
|
address : 37100,
|
|
333
|
-
length :
|
|
356
|
+
length : 36,
|
|
334
357
|
info : 'meter info',
|
|
335
358
|
refresh : dataRefreshRate.low,
|
|
336
359
|
type : deviceType.meter,
|
|
@@ -409,6 +432,25 @@ class Registers {
|
|
|
409
432
|
{
|
|
410
433
|
state: {id: 'meter.activePowerL3', name: 'Active Power L3', type: 'number', unit: 'A', role: 'value.current'},
|
|
411
434
|
register: {reg: 37136, type: dataType.int32}
|
|
435
|
+
}
|
|
436
|
+
]
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
address : 37200,
|
|
440
|
+
length : 3,
|
|
441
|
+
info : 'optimizer info (static info)',
|
|
442
|
+
type : deviceType.inverter,
|
|
443
|
+
states: [{
|
|
444
|
+
state: {id: 'optimizer.optimizerTotalNumber', name: 'Optimizer Total Number', type: 'number', unit: '', role: 'value'},
|
|
445
|
+
register: {reg: 37200, type: dataType.int16}
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
state: {id: 'optimizer.optimizerOnlineNumber', name: 'Optimizer Online Number', type: 'number', unit: '', role: 'value'},
|
|
449
|
+
register: {reg: 37201, type: dataType.int16}
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
state: {id: 'optimizer.optimizerFeatureData', name: 'Optimizer Feature Data', type: 'number', unit: '', role: 'value'},
|
|
453
|
+
register: {reg: 37202, type: dataType.int16}
|
|
412
454
|
}]
|
|
413
455
|
},
|
|
414
456
|
{
|
|
@@ -457,19 +499,39 @@ class Registers {
|
|
|
457
499
|
]
|
|
458
500
|
}
|
|
459
501
|
];
|
|
502
|
+
//Vorlage für die StringsRegiter
|
|
503
|
+
const stringFieldsTemplate = {
|
|
504
|
+
states : [
|
|
505
|
+
{
|
|
506
|
+
state: {id: 'string.PV1Voltage', name: 'string voltage', type: 'number', unit: 'V', role: 'value.voltage'},
|
|
507
|
+
register: {reg: 32016, type: dataType.int16, length: 1, gain: 10}
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
state: {id: 'string.PV1Current', name: 'string current', type: 'number', unit: 'A', role: 'value.current'},
|
|
511
|
+
register: {reg: 32017, type: dataType.int16, length: 1, gain: 100}
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
state: {id: 'string.PV1Power', name: 'string power', type: 'number', unit: 'W', role: 'value.power'}
|
|
515
|
+
}
|
|
516
|
+
]
|
|
517
|
+
};
|
|
518
|
+
|
|
460
519
|
this.postUpdateHooks = [
|
|
461
520
|
{
|
|
462
521
|
refresh : dataRefreshRate.low,
|
|
463
|
-
state: {id: 'derived.dailyInputYield', name: '
|
|
522
|
+
state: {id: 'derived.dailyInputYield', name: 'Portal Yield Today', type: 'number', unit: 'kWh', role: 'value.power.consumption', desc: 'Try to recreate the yield from the portal'},
|
|
464
523
|
fn : (path) => {
|
|
465
524
|
const disCharge = this.stateCache.get(path+'battery.currentDayDischargeCapacity')?.value;
|
|
466
525
|
const charge = this.stateCache.get(path+'battery.currentDayChargeCapacity')?.value;
|
|
467
|
-
|
|
526
|
+
let inputYield = Math.round((this.stateCache.get(path+'dailyEnergyYield')?.value + charge - disCharge)*100)/100;
|
|
527
|
+
const energyLoss = new Date().getHours()*this.inverterInfo.energyLoss;//+disCharge*0.05;
|
|
528
|
+
inputYield -= energyLoss;
|
|
529
|
+
if (inputYield < 0) inputYield=0;
|
|
468
530
|
this.stateCache.set(path+'derived.dailyInputYield', inputYield, {type: 'number'});
|
|
469
531
|
}
|
|
470
532
|
}
|
|
471
533
|
];
|
|
472
|
-
this.
|
|
534
|
+
this.postProcessHooks = [
|
|
473
535
|
{
|
|
474
536
|
refresh : dataRefreshRate.high,
|
|
475
537
|
states : [
|
|
@@ -504,7 +566,8 @@ class Registers {
|
|
|
504
566
|
refresh : dataRefreshRate.low,
|
|
505
567
|
states : [
|
|
506
568
|
{id: 'collected.dailyEnergyYield', name: 'Daily Energy Yield', type: 'number', unit: 'kWh', role: 'value.power.consumption', desc: 'daily output yield of the inverters'},
|
|
507
|
-
{id: 'collected.dailyInputYield', name: '
|
|
569
|
+
{id: 'collected.dailyInputYield', name: 'Daily Portal Yield', type: 'number', unit: 'kWh', role: 'value.power.consumption', desc: 'Try to recreate the yield from the portal'},
|
|
570
|
+
{id: 'collected.dailySolarYield', name: 'Daily Solar Yield', type: 'number', unit: 'kWh', role: 'value.power.consumption', desc: 'Riemann sum of input power with efficiency loss'},
|
|
508
571
|
{id: 'collected.accumulatedEnergyYield', name: 'Accumulated Energy Yield', type: 'number', unit: 'kWh', role: 'value.power.consumption'},
|
|
509
572
|
{id: 'collected.consumptionSum', name: 'Consumption Sum', type: 'number', unit: 'kWh', role: 'value.power.consumption'},
|
|
510
573
|
{id: 'collected.gridExportStart', name: 'Grid Export Start Today', type: 'number', unit: 'kWh', role: 'value.power.consumption'},
|
|
@@ -521,7 +584,8 @@ class Registers {
|
|
|
521
584
|
{id: 'collected.ratedCapacity', name: 'Rated of battery Capacity', type: 'number', unit: 'Wh', role: 'value.capacity'}
|
|
522
585
|
],
|
|
523
586
|
fn : (inverters) => {
|
|
524
|
-
let inYield = 0;
|
|
587
|
+
let inYield = 0; //deprecated
|
|
588
|
+
let solarYield = 0;
|
|
525
589
|
let outYield = 0;
|
|
526
590
|
let enYield = 0;
|
|
527
591
|
let charge = 0;
|
|
@@ -532,7 +596,8 @@ class Registers {
|
|
|
532
596
|
let load = 0;
|
|
533
597
|
for (const inverter of inverters) {
|
|
534
598
|
outYield += this.stateCache.get(inverter.path+'.dailyEnergyYield')?.value;
|
|
535
|
-
inYield += this.stateCache.get(inverter.path+'.derived.dailyInputYield')?.value;
|
|
599
|
+
inYield += this.stateCache.get(inverter.path+'.derived.dailyInputYield')?.value; //deprecated
|
|
600
|
+
solarYield += this.stateCache.get(inverter.path+'.derived.dailySolarYield')?.value;
|
|
536
601
|
enYield += this.stateCache.get(inverter.path+'.accumulatedEnergyYield')?.value;
|
|
537
602
|
charge += this.stateCache.get(inverter.path+'.battery.currentDayChargeCapacity')?.value;
|
|
538
603
|
disCharge += this.stateCache.get(inverter.path+'.battery.currentDayDischargeCapacity')?.value;
|
|
@@ -544,7 +609,8 @@ class Registers {
|
|
|
544
609
|
}
|
|
545
610
|
}
|
|
546
611
|
this.stateCache.set('collected.dailyEnergyYield',outYield, {type: 'number'});
|
|
547
|
-
this.stateCache.set('collected.dailyInputYield',inYield, {type: 'number'});
|
|
612
|
+
this.stateCache.set('collected.dailyInputYield',inYield, {type: 'number'}); //deprecated
|
|
613
|
+
this.stateCache.set('collected.dailySolarYield',solarYield, {type: 'number'});
|
|
548
614
|
this.stateCache.set('collected.accumulatedEnergyYield',enYield, {type: 'number'});
|
|
549
615
|
const conSum = enYield + this.stateCache.get('meter.reverseActiveEnergy')?.value - this.stateCache.get('meter.positiveActiveEnergy')?.value;
|
|
550
616
|
this.stateCache.set('collected.consumptionSum',conSum, {type: 'number'});
|
|
@@ -623,6 +689,7 @@ class Registers {
|
|
|
623
689
|
|
|
624
690
|
async processRegister(reg,data) {
|
|
625
691
|
const path = this.getStatePath(reg.type);
|
|
692
|
+
if (reg.preHook) reg.preHook(path,reg);
|
|
626
693
|
if (reg.states) {
|
|
627
694
|
for(const field of reg.states) {
|
|
628
695
|
const state = field.state;
|
|
@@ -658,7 +725,7 @@ class Registers {
|
|
|
658
725
|
let readRegisters = 0;
|
|
659
726
|
for (const reg of this.registerFields) {
|
|
660
727
|
if (duration) {
|
|
661
|
-
if (new Date().getTime() - start > (duration -
|
|
728
|
+
if (new Date().getTime() - start > (duration - 1000)) {
|
|
662
729
|
this.adapter.log.debug('Duration: '+Math.round(duration/1000)+' used time: '+ (new Date().getTime() - start)/1000);
|
|
663
730
|
break;
|
|
664
731
|
}
|
|
@@ -671,8 +738,8 @@ class Registers {
|
|
|
671
738
|
if ( refreshRate !== dataRefreshRate.high) {
|
|
672
739
|
if (lastread) {
|
|
673
740
|
if (!reg.refresh) continue;
|
|
674
|
-
if ((start - lastread) <
|
|
675
|
-
this.adapter.log.debug('Last
|
|
741
|
+
if ((start - lastread) < this.adapter.settings.lowIntervall) {
|
|
742
|
+
this.adapter.log.debug('Last read :'+(start - lastread));
|
|
676
743
|
continue;
|
|
677
744
|
}
|
|
678
745
|
}
|
|
@@ -706,12 +773,6 @@ class Registers {
|
|
|
706
773
|
if (dataRefreshRate.compare(refreshRate,hook.refresh)) {
|
|
707
774
|
const state = hook.state;
|
|
708
775
|
if (!hook['initState'+this.inverterInfo.index]) {
|
|
709
|
-
/*
|
|
710
|
-
if (state.id == 'derived.dailyInputYield' ) {
|
|
711
|
-
this.adapter.log.debug('*** [runPostUpdateHooks modbusID '+JSON.stringify(this.inverterInfo));
|
|
712
|
-
this.adapter.log.debug('*** [runPostUpdateHooks] path '+path+state.id+' modbusID ');
|
|
713
|
-
}
|
|
714
|
-
*/
|
|
715
776
|
await this._initState(path,state);
|
|
716
777
|
}
|
|
717
778
|
hook.fn(path);
|
|
@@ -720,9 +781,8 @@ class Registers {
|
|
|
720
781
|
}
|
|
721
782
|
}
|
|
722
783
|
|
|
723
|
-
|
|
724
784
|
async runProcessHooks(refreshRate) {
|
|
725
|
-
for (const hook of this.
|
|
785
|
+
for (const hook of this.postProcessHooks) {
|
|
726
786
|
if (dataRefreshRate.compare(refreshRate,hook.refresh)) {
|
|
727
787
|
for (const state of hook.states) {
|
|
728
788
|
if (!hook['initState'+this.inverterInfo.index]) {
|
|
@@ -737,12 +797,54 @@ class Registers {
|
|
|
737
797
|
}
|
|
738
798
|
|
|
739
799
|
async _loadStates() {
|
|
740
|
-
let
|
|
741
|
-
this.stateCache.set('collected.gridExportStart',
|
|
742
|
-
|
|
743
|
-
this.stateCache.set('collected.gridImportStart',
|
|
744
|
-
|
|
745
|
-
this.stateCache.set('collected.consumptionStart',
|
|
800
|
+
let state = await this.adapter.getStateAsync('collected.gridExportStart');
|
|
801
|
+
this.stateCache.set('collected.gridExportStart',state?.val, {type : 'number', stored : true });
|
|
802
|
+
state = await this.adapter.getStateAsync('collected.gridImportStart');
|
|
803
|
+
this.stateCache.set('collected.gridImportStart',state?.val, {type : 'number', stored : true });
|
|
804
|
+
state = await this.adapter.getStateAsync('collected.consumptionStart');
|
|
805
|
+
this.stateCache.set('collected.consumptionStart',state?.val, {type : 'number', stored : true });
|
|
806
|
+
for (const inverter of this.adapter.inverters) {
|
|
807
|
+
state = await this.adapter.getStateAsync(inverter.path+'.derived.dailySolarYield');
|
|
808
|
+
state?.val && inverter.solarSum.setStart(state.val,state.ts);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
ChechReadError(timeShift) {
|
|
813
|
+
const now = new Date();
|
|
814
|
+
for (const inverter of this.adapter.inverters) {
|
|
815
|
+
for (const [i, reg] of this.registerFields.entries()) {
|
|
816
|
+
if (reg.type == deviceType.meter && inverter.meter == false) continue; //not meter
|
|
817
|
+
if (reg.states && reg.refresh) {
|
|
818
|
+
const lastread = reg['lastread'+inverter.index];
|
|
819
|
+
const ret = {
|
|
820
|
+
errno : 0,
|
|
821
|
+
address : reg.address,
|
|
822
|
+
info : reg.info,
|
|
823
|
+
inverter : inverter.index,
|
|
824
|
+
modbusID : inverter.modbusId,
|
|
825
|
+
tc : now.getTime
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
if (lastread) {
|
|
829
|
+
ret.lastread = lastread;
|
|
830
|
+
} else {
|
|
831
|
+
ret.lastread = 0;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (now.getTime()-ret.lastread > timeShift) {
|
|
835
|
+
if (reg.lastread == 0 && i == 0) {
|
|
836
|
+
ret.errno = 101;
|
|
837
|
+
ret.message = 'Can\'t read data from inverter! Please check the configuration.';
|
|
838
|
+
} else {
|
|
839
|
+
ret.errno = 102;
|
|
840
|
+
ret.message = 'Not all data can be read! Please inspect the sun2000 logs.';
|
|
841
|
+
}
|
|
842
|
+
return ret ;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return {message: 'No problems detected'};
|
|
746
848
|
}
|
|
747
849
|
|
|
748
850
|
// one minute before midnight - perform housekeeping actions
|
package/lib/tools.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
class StateMap {
|
|
4
|
+
constructor () {
|
|
5
|
+
this.stateMap = new Map();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
get(id) {
|
|
9
|
+
return this.stateMap.get(id);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
set(id, value, options) {
|
|
13
|
+
if (options?.type == 'number' && isNaN(value)) return;
|
|
14
|
+
if (value !== null) {
|
|
15
|
+
if (options?.renew || this.get(id)?.value !== value) {
|
|
16
|
+
if (options?.type == 'number') {
|
|
17
|
+
value = Math.round((value + Number.EPSILON) * 1000) / 1000; //3rd behind
|
|
18
|
+
}
|
|
19
|
+
if (options?.stored ) {
|
|
20
|
+
this.stateMap.set(id, {id: id, value: value, stored: options.stored});
|
|
21
|
+
} else {
|
|
22
|
+
this.stateMap.set(id, {id: id, value: value});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
values () {
|
|
29
|
+
return this.stateMap.values();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class RiemannSum {
|
|
35
|
+
constructor (autoResetAtMitnight = true) {
|
|
36
|
+
this._resetAtMitnight = autoResetAtMitnight;
|
|
37
|
+
this.reset();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
add (newValue) {
|
|
41
|
+
//Obersumme bilden
|
|
42
|
+
const now = new Date();
|
|
43
|
+
if (this._resetAtMitnight) {
|
|
44
|
+
const night = new Date(
|
|
45
|
+
now.getFullYear(),
|
|
46
|
+
now.getMonth(),
|
|
47
|
+
now.getDate(), // today, ...
|
|
48
|
+
0, 0, 0 // ...at 00:00:00 hours
|
|
49
|
+
);
|
|
50
|
+
// @ts-ignore
|
|
51
|
+
if (this._lastDate.getTime() <= night.getTime()) {
|
|
52
|
+
this.reset();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// @ts-ignore
|
|
56
|
+
this._sum += newValue * (now.getTime() - this._lastDate.getTime())/3600/1000; //Hour/Sekunden
|
|
57
|
+
this._lastDate = now;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
reset() {
|
|
61
|
+
this._sum = 0;
|
|
62
|
+
this._lastDate = new Date();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get sum() {
|
|
66
|
+
return this._sum;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
setStart(sum, ts) {
|
|
70
|
+
this._sum = sum;
|
|
71
|
+
this._lastDate = new Date(ts);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
StateMap,
|
|
78
|
+
RiemannSum
|
|
79
|
+
};
|
package/main.js
CHANGED
|
@@ -25,14 +25,16 @@ class Sun2000 extends utils.Adapter {
|
|
|
25
25
|
name: 'sun2000',
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
this.lastTimeUpdated =
|
|
29
|
-
this.
|
|
28
|
+
this.lastTimeUpdated = new Date().getTime();
|
|
29
|
+
this.lastStateUpdatedHigh = 0;
|
|
30
|
+
this.lastStateUpdatedLow = 0;
|
|
30
31
|
this.isConnected = false;
|
|
31
32
|
this.inverters = [];
|
|
32
33
|
this.settings = {
|
|
33
|
-
|
|
34
|
+
highIntervall : 20000,
|
|
35
|
+
lowIntervall : 60000,
|
|
34
36
|
address : '',
|
|
35
|
-
port : 520
|
|
37
|
+
port : 520
|
|
36
38
|
};
|
|
37
39
|
|
|
38
40
|
this.on('ready', this.onReady.bind(this));
|
|
@@ -42,19 +44,11 @@ class Sun2000 extends utils.Adapter {
|
|
|
42
44
|
this.on('unload', this.onUnload.bind(this));
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
getInverterInfo(id) {
|
|
46
|
-
/*
|
|
47
|
-
const inverter = this.inverters.find((item) => item.modbusId == id);
|
|
48
|
-
return inverter;
|
|
49
|
-
*/
|
|
50
|
-
return this.inverters[id];
|
|
51
|
-
}
|
|
52
|
-
|
|
53
47
|
async initPath() {
|
|
54
48
|
await this.extendObjectAsync('info', {
|
|
55
49
|
type: 'channel',
|
|
56
50
|
common: {
|
|
57
|
-
name: 'info',
|
|
51
|
+
name: 'channel info',
|
|
58
52
|
role: 'info'
|
|
59
53
|
},
|
|
60
54
|
native: {}
|
|
@@ -76,16 +70,14 @@ class Sun2000 extends utils.Adapter {
|
|
|
76
70
|
await this.extendObjectAsync('meter', {
|
|
77
71
|
type: 'device',
|
|
78
72
|
common: {
|
|
79
|
-
name: 'meter'
|
|
80
|
-
role: 'info'
|
|
73
|
+
name: 'device meter'
|
|
81
74
|
},
|
|
82
75
|
native: {}
|
|
83
76
|
});
|
|
84
77
|
await this.extendObjectAsync('collected', {
|
|
85
78
|
type: 'channel',
|
|
86
79
|
common: {
|
|
87
|
-
name: 'collected'
|
|
88
|
-
role: 'info'
|
|
80
|
+
name: 'channel collected'
|
|
89
81
|
},
|
|
90
82
|
native: {}
|
|
91
83
|
});
|
|
@@ -93,21 +85,19 @@ class Sun2000 extends utils.Adapter {
|
|
|
93
85
|
await this.extendObjectAsync('inverter', {
|
|
94
86
|
type: 'device',
|
|
95
87
|
common: {
|
|
96
|
-
name: '
|
|
97
|
-
role: 'info'
|
|
88
|
+
name: 'device inverter'
|
|
98
89
|
},
|
|
99
90
|
native: {}
|
|
100
91
|
});
|
|
101
92
|
|
|
102
|
-
//ES6 use a for (const [index, item] of array.entries())
|
|
103
|
-
//for (const [i, item] of this.conf.entries()) {
|
|
93
|
+
//ES6 use a for (const [index, item] of array.entries()) of loop
|
|
104
94
|
for (const [i, item] of this.inverters.entries()) {
|
|
105
95
|
const path = 'inverter.'+String(i);
|
|
106
96
|
item.path = path;
|
|
107
97
|
await this.extendObjectAsync(path, {
|
|
108
98
|
type: 'channel',
|
|
109
99
|
common: {
|
|
110
|
-
name: 'modbus'+i,
|
|
100
|
+
name: 'channel modbus'+i,
|
|
111
101
|
role: 'indicator'
|
|
112
102
|
},
|
|
113
103
|
native: {}
|
|
@@ -116,7 +106,15 @@ class Sun2000 extends utils.Adapter {
|
|
|
116
106
|
await this.extendObjectAsync(path+'.grid', {
|
|
117
107
|
type: 'channel',
|
|
118
108
|
common: {
|
|
119
|
-
name: 'grid'
|
|
109
|
+
name: 'channel grid'
|
|
110
|
+
},
|
|
111
|
+
native: {}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await this.extendObjectAsync(path+'.info', {
|
|
115
|
+
type: 'channel',
|
|
116
|
+
common: {
|
|
117
|
+
name: 'channel info',
|
|
120
118
|
role: 'info'
|
|
121
119
|
},
|
|
122
120
|
native: {}
|
|
@@ -125,8 +123,15 @@ class Sun2000 extends utils.Adapter {
|
|
|
125
123
|
await this.extendObjectAsync(path+'.battery', {
|
|
126
124
|
type: 'channel',
|
|
127
125
|
common: {
|
|
128
|
-
name: 'battery'
|
|
129
|
-
|
|
126
|
+
name: 'channel battery'
|
|
127
|
+
},
|
|
128
|
+
native: {}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await this.extendObjectAsync(path+'.string', {
|
|
132
|
+
type: 'channel',
|
|
133
|
+
common: {
|
|
134
|
+
name: 'channel string'
|
|
130
135
|
},
|
|
131
136
|
native: {}
|
|
132
137
|
});
|
|
@@ -134,8 +139,15 @@ class Sun2000 extends utils.Adapter {
|
|
|
134
139
|
await this.extendObjectAsync(path+'.derived', {
|
|
135
140
|
type: 'channel',
|
|
136
141
|
common: {
|
|
137
|
-
name: 'derived'
|
|
138
|
-
|
|
142
|
+
name: 'channel derived'
|
|
143
|
+
},
|
|
144
|
+
native: {}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await this.extendObjectAsync(path+'.optimizer', {
|
|
148
|
+
type: 'channel',
|
|
149
|
+
common: {
|
|
150
|
+
name: 'channel optimizer'
|
|
139
151
|
},
|
|
140
152
|
native: {}
|
|
141
153
|
});
|
|
@@ -144,8 +156,6 @@ class Sun2000 extends utils.Adapter {
|
|
|
144
156
|
}
|
|
145
157
|
|
|
146
158
|
async InitProcess() {
|
|
147
|
-
this.state = new Registers(this);
|
|
148
|
-
this.modbusClient = new ModbusConnect(this,this.settings.address,this.settings.port);
|
|
149
159
|
try {
|
|
150
160
|
await this.initPath();
|
|
151
161
|
/*
|
|
@@ -154,6 +164,8 @@ class Sun2000 extends utils.Adapter {
|
|
|
154
164
|
} catch (err) {
|
|
155
165
|
this.log.warn(err);
|
|
156
166
|
}
|
|
167
|
+
this.modbusClient = new ModbusConnect(this,this.settings.address,this.settings.port);
|
|
168
|
+
this.state = new Registers(this);
|
|
157
169
|
this.dataPolling();
|
|
158
170
|
this.runWatchDog();
|
|
159
171
|
this.atMidnight();
|
|
@@ -210,21 +222,33 @@ class Sun2000 extends utils.Adapter {
|
|
|
210
222
|
runWatchDog() {
|
|
211
223
|
this.watchDogHandle && this.clearInterval(this.watchDogHandle);
|
|
212
224
|
this.watchDogHandle = this.setInterval( () => {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
this.
|
|
220
|
-
|
|
221
|
-
if (this.
|
|
222
|
-
|
|
223
|
-
this.log.warn(
|
|
224
|
-
this.
|
|
225
|
+
const sinceLastUpdate = new Date().getTime() - this.lastTimeUpdated; //ms
|
|
226
|
+
this.log.debug('Watchdog: time of last update '+sinceLastUpdate/1000+' sec');
|
|
227
|
+
const lastIsConnected = this.isConnected;
|
|
228
|
+
this.isConnected = this.lastStateUpdatedHigh > 0 && sinceLastUpdate < this.settings.highIntervall*3;
|
|
229
|
+
if (this.isConnected !== lastIsConnected ) this.setState('info.connection', this.isConnected, true);
|
|
230
|
+
if (!this.isConnected) {
|
|
231
|
+
this.setStateAsync('info.JSONhealth', {val: '{errno:1, message: "Can\'t connect to inverter"}', ack: true});
|
|
232
|
+
} else {
|
|
233
|
+
if (this.alreadyRunWatchDog) {
|
|
234
|
+
const ret = this.state.ChechReadError(this.settings.lowIntervall*2);
|
|
235
|
+
if (ret.errno) this.log.warn(ret.message);
|
|
236
|
+
this.setStateAsync('info.JSONhealth', {val: JSON.stringify(ret), ack: true});
|
|
237
|
+
} else {
|
|
238
|
+
this.alreadyRunWatchDog = true;
|
|
225
239
|
}
|
|
226
240
|
}
|
|
227
|
-
|
|
241
|
+
|
|
242
|
+
this.lastStateUpdatedLow = 0;
|
|
243
|
+
this.lastStateUpdatedHigh = 0;
|
|
244
|
+
|
|
245
|
+
if (sinceLastUpdate > this.settings.highIntervall*10) {
|
|
246
|
+
this.setStateAsync('info.JSONhealth', {val: '{errno:2, message: "Internal loop error"}', ack: true});
|
|
247
|
+
this.log.warn('watchdog: restart Adapter...');
|
|
248
|
+
this.restart();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
},this.settings.lowIntervall);
|
|
228
252
|
}
|
|
229
253
|
|
|
230
254
|
|
|
@@ -237,21 +261,25 @@ class Sun2000 extends utils.Adapter {
|
|
|
237
261
|
await this.setStateAsync('info.ip', {val: this.config.address, ack: true});
|
|
238
262
|
await this.setStateAsync('info.port', {val: this.config.port, ack: true});
|
|
239
263
|
await this.setStateAsync('info.modbusIds', {val: this.config.modbusIds, ack: true});
|
|
264
|
+
await this.setStateAsync('info.JSONhealth', {val: '{message : "Information is collected"}', ack: true});
|
|
240
265
|
|
|
241
266
|
// Load user settings
|
|
242
267
|
if (this.config.address !== '' || this.config.port > 0 || this.config.updateInterval > 0 ) {
|
|
243
|
-
this.settings.
|
|
268
|
+
this.settings.highIntervall = this.config.updateInterval*1000; //ms
|
|
244
269
|
this.settings.address = this.config.address;
|
|
245
270
|
this.settings.port = this.config.port;
|
|
246
271
|
this.settings.modbusIds = this.config.modbusIds.split(',').map((n) => {return Number(n);});
|
|
247
272
|
|
|
248
273
|
if (this.settings.modbusIds.length > 0 && this.settings.modbusIds.length < 6) {
|
|
249
|
-
if (this.settings.
|
|
250
|
-
this.settings.
|
|
274
|
+
if (this.settings.highIntervall < 5000*this.settings.modbusIds.length ) {
|
|
275
|
+
this.settings.highIntervall = 5000*this.settings.modbusIds.length;
|
|
251
276
|
}
|
|
252
|
-
|
|
277
|
+
if (this.settings.highIntervall >= this.settings.lowIntervall) {
|
|
278
|
+
this.settings.lowIntervall = this.settings.highIntervall;
|
|
279
|
+
}
|
|
280
|
+
await this.setStateAsync('info.modbusUpdateInterval', {val: this.settings.highIntervall/1000, ack: true});
|
|
253
281
|
for (const [i,id] of this.settings.modbusIds.entries()) {
|
|
254
|
-
this.inverters.push({index: i, modbusId: id, meter: (i==0)});
|
|
282
|
+
this.inverters.push({index: i, modbusId: id, energyLoss: 0.008, meter: (i==0)}); //own energy consumption of inverter 8 W
|
|
255
283
|
}
|
|
256
284
|
await this.InitProcess();
|
|
257
285
|
} else {
|
|
@@ -265,52 +293,43 @@ class Sun2000 extends utils.Adapter {
|
|
|
265
293
|
}
|
|
266
294
|
|
|
267
295
|
async dataPolling() {
|
|
268
|
-
|
|
269
|
-
|
|
296
|
+
|
|
297
|
+
function timeLeft(target,factor =1) {
|
|
298
|
+
const left = Math.round((target - new Date().getTime())*factor);
|
|
270
299
|
if (left < 0) return 0;
|
|
271
300
|
return left;
|
|
272
301
|
}
|
|
273
302
|
|
|
274
303
|
const start = new Date().getTime();
|
|
275
|
-
|
|
276
|
-
this.
|
|
277
|
-
|
|
278
|
-
this.log.warn('time intervall '+(start-this.lastTimeUpdated)/1000+' sec');
|
|
304
|
+
this.log.debug('### DataPolling START '+ Math.round((start-this.lastTimeUpdated)/1000)+' sec ###');
|
|
305
|
+
if (this.lastTimeUpdated > 0 && (start-this.lastTimeUpdated)/1000 > this.settings.highIntervall/1000 + 1) {
|
|
306
|
+
this.log.info('time intervall '+(start-this.lastTimeUpdated)/1000+' sec');
|
|
279
307
|
}
|
|
280
308
|
this.lastTimeUpdated = start;
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const nextLoop = this.settings.intervall - start % (this.settings.intervall) + start;
|
|
309
|
+
const nextLoop = this.settings.highIntervall - start % (this.settings.highIntervall) + start;
|
|
284
310
|
|
|
285
311
|
//High Loop
|
|
286
312
|
for (const item of this.inverters) {
|
|
287
313
|
this.modbusClient.setID(item.modbusId);
|
|
288
|
-
|
|
289
|
-
stateUpdated += await this.state.updateStates(item,this.modbusClient,dataRefreshRate.high,timeLeft(nextLoop));
|
|
314
|
+
this.lastStateUpdatedHigh += await this.state.updateStates(item,this.modbusClient,dataRefreshRate.high,timeLeft(nextLoop));
|
|
290
315
|
}
|
|
316
|
+
await this.state.runProcessHooks(dataRefreshRate.high);
|
|
291
317
|
|
|
292
|
-
if (timeLeft(nextLoop) >
|
|
293
|
-
await this.state.runProcessHooks(dataRefreshRate.high);
|
|
294
|
-
|
|
318
|
+
if (timeLeft(nextLoop) > 500) {
|
|
295
319
|
//Low Loop
|
|
296
|
-
for (const item of this.inverters) {
|
|
320
|
+
for (const [i,item] of this.inverters.entries()) {
|
|
297
321
|
this.modbusClient.setID(item.modbusId);
|
|
298
|
-
//this.log.
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
if (timeLeft(nextLoop) > 1000) {
|
|
302
|
-
await this.state.runProcessHooks(dataRefreshRate.low);
|
|
322
|
+
//this.log.debug('+++++ Loop: '+i+' Left Time: '+timeLeft(nextLoop,(i+1)/this.inverters.length)+' Faktor '+((i+1)/this.inverters.length));
|
|
323
|
+
this.lastStateUpdatedLow += await this.state.updateStates(item,this.modbusClient,dataRefreshRate.low,timeLeft(nextLoop,(i+1)/this.inverters.length));
|
|
303
324
|
}
|
|
304
325
|
}
|
|
305
|
-
|
|
306
|
-
this.lastStateUpdated = stateUpdated;
|
|
326
|
+
await this.state.runProcessHooks(dataRefreshRate.low);
|
|
307
327
|
|
|
308
328
|
if (this.pollingTimer) this.clearTimeout(this.pollingTimer);
|
|
309
329
|
this.pollingTimer = this.setTimeout(() => {
|
|
310
330
|
this.dataPolling(); //recursiv
|
|
311
331
|
}, timeLeft(nextLoop));
|
|
312
332
|
this.log.debug('### DataPolling STOP ###');
|
|
313
|
-
//this.state.mitnightProcess();
|
|
314
333
|
}
|
|
315
334
|
|
|
316
335
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.sun2000",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "sun2000",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "bolliy",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@iobroker/adapter-core": "^3.0.4",
|
|
26
|
-
"modbus-serial": "^8.0.
|
|
26
|
+
"modbus-serial": "^8.0.16"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@alcalzone/release-script": "^3.6.0",
|