iobroker.sun2000 0.1.3 → 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 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
@@ -31,10 +31,17 @@ The development of this adapter was inspired by discussions from the forum threa
31
31
  ## Feature list
32
32
 
33
33
  * Maximum 5 inverters (master/slave) can be processed, each with a battery module (max. 30kWh).
34
- * Live data such as input power, output power, charging/discharging power and the grid consumption are read out at a fixed interval.
34
+ * Real-time values such as input power, output power, charging/discharging power and the grid consumption are read out at a fixed interval.
35
35
  * States are only written for changed data from the inverter. This relieves the burden on the iobroker instance.
36
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.
37
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)
44
+
38
45
  ## Configure inverters
39
46
 
40
47
  In order to use the Modbus connection, all Huawei devices must use the latest firmware
@@ -50,12 +57,10 @@ If you use two inverters, then connect to the second inverter and read the commu
50
57
 
51
58
  [How activate 'Modbus TCP' - from huawei forum](https://forum.huawei.com/enterprise/en/modbus-tcp-guide/thread/789585-100027)
52
59
 
53
- ## Settings
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.
54
63
 
55
- * `address`: Inverter IP address
56
- * `port`: Inverter modbus port (default: 502)
57
- * `modbusIds`: inverter IDs, separated with "," (default: 1, max. 5 inverters)
58
- * `updateInterval`: Fast update interval (default: 20 sec, smallest 5 seconds per inverter)
59
64
 
60
65
  ## Changelog
61
66
 
@@ -63,6 +68,13 @@ If you use two inverters, then connect to the second inverter and read the commu
63
68
  Placeholder for the next version (at the beginning of the line):
64
69
  ### **WORK IN PROGRESS**
65
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
+
66
78
  ### 0.1.3 (2024-01-17)
67
79
  * display the data from PV strings (#27)
68
80
  * optimize the timing of interval loop
package/io-package.json CHANGED
@@ -1,8 +1,21 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "sun2000",
4
- "version": "0.1.3",
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
+ },
6
19
  "0.1.3": {
7
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",
8
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",
@@ -29,79 +42,36 @@
29
42
  "uk": "виправити: немає даних, якщо інтервал менше 20 сек (#24)\nпідготовка зібраних значень точно\nрозширити до 5 інверторів #18\nвиправити проблеми з декількома інверторами",
30
43
  "zh-cn": "固定: 如果间隔小于20秒(# 24) 则无数据\n更准确地编制收集的数值\n扩展至5个反转器 # 18\n解决多个反转器的问题"
31
44
  },
32
- "0.1.2-alpha.3": {
33
- "en": "fix: wrong deploying date",
34
- "de": "fix: falsches bereitstellungsdatum",
35
- "ru": "исправить: неправильная дата развертывания",
36
- "pt": "correção: data de implantação errada",
37
- "nl": "fix: verkeerde datum voor ingebruikname",
38
- "fr": "correction : mauvaise date de déploiement",
39
- "it": "fix: la data di distribuzione sbagliata",
40
- "es": "fijado: fecha de despliegue incorrecta",
41
- "pl": "fix: nieprawidłowa data rozmieszczenia",
42
- "uk": "виправити: неправильна дата розгортання",
43
- "zh-cn": "固定: 部署日期错误"
44
- },
45
- "0.1.2-alpha.2": {
46
- "en": "fix: no Data if interval less 20 sec (#24)",
47
- "de": "fix: keine Daten, wenn Intervall weniger 20 sec (#24)",
48
- "ru": "исправление: нет данных, если интервал менее 20 секунд (#24)",
49
- "pt": "correção: não Dados se intervalo menos 20 segundos (#24)",
50
- "nl": "fix: geen Gegevens indien interval minder 20 sec (#24)",
51
- "fr": "correction : pas de données si l'intervalle est inférieur à 20 secondes (#24)",
52
- "it": "correzione: no Dati se intervallo meno 20 sec (#24)",
53
- "es": "fijado: no Datos si intervalo menos 20 segundos (#24)",
54
- "pl": "fix: no Data if interval less 20 sec (# 24)",
55
- "uk": "виправити: немає даних, якщо інтервал менше 20 сек (#24)",
56
- "zh-cn": "固定: 如果间隔小于20秒(# 24) 则无数据"
57
- },
58
- "0.1.2-alpha.1": {
59
- "en": "deploy npm package",
60
- "de": "npm paket bereitstellen",
61
- "ru": "развернуть пакет npm",
62
- "pt": "implementar pacote npm",
63
- "nl": "npm-pakket implementeren",
64
- "fr": "déployer le paquet npm",
65
- "it": "distribuire il pacchetto npm",
66
- "es": "paquete npm",
67
- "pl": "rozmieszczenie pakietu npm",
68
- "uk": "пакет npm",
69
- "zh-cn": "部署 npm 软件包"
70
- },
71
- "0.1.2-alpha.0": {
72
- "en": "prepare collected values more precisely\nexpand up to 5 inverters #18\nfix problems with multiple inverters",
73
- "de": "die gesammelten werte genauer\nbis zu 5 wechselrichtern #18 erweitern\nprobleme mit mehreren wechselrichtern beheben",
74
- "ru": "готовить собранные значения точнее\n#18\nисправить проблемы с несколькими инверторами",
75
- "pt": "preparar valores coletados mais precisamente\nexpandir até 5 inversores #18\ncorrigir problemas com vários inversores",
76
- "nl": "verzamelde waarden nauwkeuriger voorbereiden\nuit te breiden tot 5 omvormers #18\nproblemen oplossen met meerdere inverters",
77
- "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",
78
- "it": "preparare i valori raccolti più precisamente\nespandere fino a 5 inverter #18\nrisolvere problemi con più inverter",
79
- "es": "preparar los valores recogidos con mayor precisión\nampliar hasta 5 inversores #18\nsolucionar problemas con múltiples inversores",
80
- "pl": "dokładniej przygotować zebrane wartości\nrozszerzyć do 5 inwerterów # 18\nrozwiązać problemy z wieloma inwerterami",
81
- "uk": "підготовка зібраних значень точно\nрозширити до 5 інверторів #18\nвиправити проблеми з декількома інверторами",
82
- "zh-cn": "更准确地编制收集的数值\n扩展至5个反转器 # 18\n解决多个反转器的问题"
83
- },
84
45
  "0.1.1": {
85
46
  "en": "fix some collected values",
86
- "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": "固定一些收集的值"
87
57
  }
88
58
  },
89
59
  "titleLang": {
90
- "en": "sun2000",
91
- "de": "sun2000",
92
- "ru": "солнце2000",
93
- "pt": "sol2000",
94
- "nl": "zon2000",
95
- "fr": "soleil2000",
96
- "it": "sole2000",
97
- "es": "sol2000",
98
- "pl": "słońce2000",
99
- "uk": "sun2000",
100
- "zh-cn": "太阳2000"
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"
101
71
  },
102
72
  "desc": {
103
- "en": "Read data from Huawei SUN2000 inverter and LUNA2000 battery using Modbus TCP\n",
104
- "de": "Lesen Sie die Daten von Huawei SUN2000 Wechselrichter und LUNA2000 Akku mit Modbus TCP\n",
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",
105
75
  "ru": "Прочитайте данные от Huawei SUN2000 inverter и LUNA2000 батареи с помощью Modbus TCP\n",
106
76
  "pt": "Leia dados do inversor Huawei SUN2000 e da bateria LUNA2000 usando Modbus TCP\n",
107
77
  "nl": "Lees gegevens van Huawei SUN2000 inverter en LUNA2000 batterij met Modbus TCP\n",
@@ -199,7 +169,7 @@
199
169
  "common": {
200
170
  "name": {
201
171
  "en": "Inverter IP",
202
- "de": "Inverter IP"
172
+ "de": "Wechselrichter IP"
203
173
  },
204
174
  "type": "string",
205
175
  "role": "indicator.ip",
@@ -228,8 +198,8 @@
228
198
  "type": "state",
229
199
  "common": {
230
200
  "name": {
231
- "en": "Secondary Modbus inverter ID",
232
- "de": "Modbus ID des 2ten Wechselrichters"
201
+ "en": "Modbus IDs of inverters",
202
+ "de": "Modbus IDs der Wechselrichters"
233
203
  },
234
204
  "type": "string",
235
205
  "role": "indicator.id",
@@ -253,6 +223,22 @@
253
223
  "desc": "modbus update interval",
254
224
  "unit": "sec"
255
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
+ }
256
242
  }
257
243
  ]
258
244
  }
@@ -34,15 +34,11 @@ class ModbusConnect extends DeviceInterface {
34
34
  }
35
35
 
36
36
  close() {
37
- return new Promise((resolve,reject) => {
38
- if (this.isOpen()) {
39
- this.client.close((err,data) => {
40
- if (err) reject(err);
41
- resolve(data);
42
- } );
43
- } else {
37
+ return new Promise((resolve) => {
38
+ this.client.close(() => {
39
+ //if (err) reject(err);
44
40
  resolve({});
45
- }
41
+ } );
46
42
  });
47
43
  }
48
44
 
@@ -69,22 +65,22 @@ class ModbusConnect extends DeviceInterface {
69
65
  }
70
66
 
71
67
 
72
- async _destroy() {
73
- return new Promise((resolve,reject) => {
74
- this.client.destroy((err,data) => {
75
- if (err) reject(err);
76
- resolve(data);
68
+ _destroy() {
69
+ return new Promise((resolve) => {
70
+ this.client.destroy(() => {
71
+ //if (err) reject(err);
72
+ resolve({});
77
73
  });
78
74
  });
79
75
  }
80
76
 
81
77
  async _checkError(err) {
78
+ this.adapter.log.debug('modbus client error: '+JSON.stringify(err));
79
+ await this.close();
82
80
  if (err.modbusCode == null) {
83
- this.adapter.log.debug('modbusCode == 0!');
84
- await this.close();
81
+ this.adapter.log.debug('modbus client destroyed');
85
82
  //https://github.com/yaacov/node-modbus-serial/issues/96
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
  {
@@ -372,7 +353,7 @@ class Registers {
372
353
  },
373
354
  {
374
355
  address : 37100,
375
- length : 114,
356
+ length : 36,
376
357
  info : 'meter info',
377
358
  refresh : dataRefreshRate.low,
378
359
  type : deviceType.meter,
@@ -451,6 +432,25 @@ class Registers {
451
432
  {
452
433
  state: {id: 'meter.activePowerL3', name: 'Active Power L3', type: 'number', unit: 'A', role: 'value.current'},
453
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}
454
454
  }]
455
455
  },
456
456
  {
@@ -519,11 +519,14 @@ class Registers {
519
519
  this.postUpdateHooks = [
520
520
  {
521
521
  refresh : dataRefreshRate.low,
522
- state: {id: 'derived.dailyInputYield', name: 'Input Yield Today', type: 'number', unit: 'kWh', role: 'value.power.consumption', desc: 'daily yield displayed on the portal'},
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'},
523
523
  fn : (path) => {
524
524
  const disCharge = this.stateCache.get(path+'battery.currentDayDischargeCapacity')?.value;
525
525
  const charge = this.stateCache.get(path+'battery.currentDayChargeCapacity')?.value;
526
- const inputYield = Math.round((this.stateCache.get(path+'dailyEnergyYield')?.value + charge - disCharge)*100)/100;
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;
527
530
  this.stateCache.set(path+'derived.dailyInputYield', inputYield, {type: 'number'});
528
531
  }
529
532
  }
@@ -563,7 +566,8 @@ class Registers {
563
566
  refresh : dataRefreshRate.low,
564
567
  states : [
565
568
  {id: 'collected.dailyEnergyYield', name: 'Daily Energy Yield', type: 'number', unit: 'kWh', role: 'value.power.consumption', desc: 'daily output yield of the inverters'},
566
- {id: 'collected.dailyInputYield', name: 'Input Yield Today', type: 'number', unit: 'kWh', role: 'value.power.consumption', desc: 'daily yield displayed on the portal'},
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'},
567
571
  {id: 'collected.accumulatedEnergyYield', name: 'Accumulated Energy Yield', type: 'number', unit: 'kWh', role: 'value.power.consumption'},
568
572
  {id: 'collected.consumptionSum', name: 'Consumption Sum', type: 'number', unit: 'kWh', role: 'value.power.consumption'},
569
573
  {id: 'collected.gridExportStart', name: 'Grid Export Start Today', type: 'number', unit: 'kWh', role: 'value.power.consumption'},
@@ -580,7 +584,8 @@ class Registers {
580
584
  {id: 'collected.ratedCapacity', name: 'Rated of battery Capacity', type: 'number', unit: 'Wh', role: 'value.capacity'}
581
585
  ],
582
586
  fn : (inverters) => {
583
- let inYield = 0;
587
+ let inYield = 0; //deprecated
588
+ let solarYield = 0;
584
589
  let outYield = 0;
585
590
  let enYield = 0;
586
591
  let charge = 0;
@@ -591,7 +596,8 @@ class Registers {
591
596
  let load = 0;
592
597
  for (const inverter of inverters) {
593
598
  outYield += this.stateCache.get(inverter.path+'.dailyEnergyYield')?.value;
594
- 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;
595
601
  enYield += this.stateCache.get(inverter.path+'.accumulatedEnergyYield')?.value;
596
602
  charge += this.stateCache.get(inverter.path+'.battery.currentDayChargeCapacity')?.value;
597
603
  disCharge += this.stateCache.get(inverter.path+'.battery.currentDayDischargeCapacity')?.value;
@@ -603,7 +609,8 @@ class Registers {
603
609
  }
604
610
  }
605
611
  this.stateCache.set('collected.dailyEnergyYield',outYield, {type: 'number'});
606
- 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'});
607
614
  this.stateCache.set('collected.accumulatedEnergyYield',enYield, {type: 'number'});
608
615
  const conSum = enYield + this.stateCache.get('meter.reverseActiveEnergy')?.value - this.stateCache.get('meter.positiveActiveEnergy')?.value;
609
616
  this.stateCache.set('collected.consumptionSum',conSum, {type: 'number'});
@@ -731,8 +738,8 @@ class Registers {
731
738
  if ( refreshRate !== dataRefreshRate.high) {
732
739
  if (lastread) {
733
740
  if (!reg.refresh) continue;
734
- if ((start - lastread) < 60000) {
735
- this.adapter.log.debug('Last Update :'+(start - lastread));
741
+ if ((start - lastread) < this.adapter.settings.lowIntervall) {
742
+ this.adapter.log.debug('Last read :'+(start - lastread));
736
743
  continue;
737
744
  }
738
745
  }
@@ -766,12 +773,6 @@ class Registers {
766
773
  if (dataRefreshRate.compare(refreshRate,hook.refresh)) {
767
774
  const state = hook.state;
768
775
  if (!hook['initState'+this.inverterInfo.index]) {
769
- /*
770
- if (state.id == 'derived.dailyInputYield' ) {
771
- this.adapter.log.debug('*** [runPostUpdateHooks modbusID '+JSON.stringify(this.inverterInfo));
772
- this.adapter.log.debug('*** [runPostUpdateHooks] path '+path+state.id+' modbusID ');
773
- }
774
- */
775
776
  await this._initState(path,state);
776
777
  }
777
778
  hook.fn(path);
@@ -780,7 +781,6 @@ class Registers {
780
781
  }
781
782
  }
782
783
 
783
-
784
784
  async runProcessHooks(refreshRate) {
785
785
  for (const hook of this.postProcessHooks) {
786
786
  if (dataRefreshRate.compare(refreshRate,hook.refresh)) {
@@ -796,28 +796,55 @@ class Registers {
796
796
  this.storeStates(); //fire and forget
797
797
  }
798
798
 
799
- /*
800
- setStateOfStrings() {
801
- jsonPricesTomorrow = jsonPricesToday.map( x =>
802
- {
803
- //console.log(x);
804
- const json = Object.assign({}, x); //Object clonen, flaches Clonen!
805
- const date = new Date(json.startsAt);
806
- // add a day
807
- date.setDate(date.getDate() + 1);
808
- json.startsAt = date.toISOString();
809
- return json;
810
- } );
799
+ async _loadStates() {
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
+ }
811
810
  }
812
- */
813
811
 
814
- async _loadStates() {
815
- let value = await this.adapter.getStateAsync('collected.gridExportStart');
816
- this.stateCache.set('collected.gridExportStart',value?.val, {type : 'number', stored : true });
817
- value = await this.adapter.getStateAsync('collected.gridImportStart');
818
- this.stateCache.set('collected.gridImportStart',value?.val, {type : 'number', stored : true });
819
- value = await this.adapter.getStateAsync('collected.consumptionStart');
820
- this.stateCache.set('collected.consumptionStart',value?.val, {type : 'number', stored : true });
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'};
821
848
  }
822
849
 
823
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,15 +25,16 @@ class Sun2000 extends utils.Adapter {
25
25
  name: 'sun2000',
26
26
  });
27
27
 
28
- this.lastTimeUpdated = 0;
28
+ this.lastTimeUpdated = new Date().getTime();
29
29
  this.lastStateUpdatedHigh = 0;
30
30
  this.lastStateUpdatedLow = 0;
31
31
  this.isConnected = false;
32
32
  this.inverters = [];
33
33
  this.settings = {
34
- intervall : 20000,
34
+ highIntervall : 20000,
35
+ lowIntervall : 60000,
35
36
  address : '',
36
- port : 520,
37
+ port : 520
37
38
  };
38
39
 
39
40
  this.on('ready', this.onReady.bind(this));
@@ -43,14 +44,6 @@ class Sun2000 extends utils.Adapter {
43
44
  this.on('unload', this.onUnload.bind(this));
44
45
  }
45
46
 
46
- getInverterInfo(id) {
47
- /*
48
- const inverter = this.inverters.find((item) => item.modbusId == id);
49
- return inverter;
50
- */
51
- return this.inverters[id];
52
- }
53
-
54
47
  async initPath() {
55
48
  await this.extendObjectAsync('info', {
56
49
  type: 'channel',
@@ -151,12 +144,18 @@ class Sun2000 extends utils.Adapter {
151
144
  native: {}
152
145
  });
153
146
 
147
+ await this.extendObjectAsync(path+'.optimizer', {
148
+ type: 'channel',
149
+ common: {
150
+ name: 'channel optimizer'
151
+ },
152
+ native: {}
153
+ });
154
+
154
155
  }
155
156
  }
156
157
 
157
158
  async InitProcess() {
158
- this.state = new Registers(this);
159
- this.modbusClient = new ModbusConnect(this,this.settings.address,this.settings.port);
160
159
  try {
161
160
  await this.initPath();
162
161
  /*
@@ -165,6 +164,8 @@ class Sun2000 extends utils.Adapter {
165
164
  } catch (err) {
166
165
  this.log.warn(err);
167
166
  }
167
+ this.modbusClient = new ModbusConnect(this,this.settings.address,this.settings.port);
168
+ this.state = new Registers(this);
168
169
  this.dataPolling();
169
170
  this.runWatchDog();
170
171
  this.atMidnight();
@@ -221,29 +222,33 @@ class Sun2000 extends utils.Adapter {
221
222
  runWatchDog() {
222
223
  this.watchDogHandle && this.clearInterval(this.watchDogHandle);
223
224
  this.watchDogHandle = this.setInterval( () => {
224
- if (!this.lastTimeUpdated) this.lastUpdated = 0;
225
- if (this.lastTimeUpdated > 0) {
226
- const sinceLastUpdate = new Date().getTime() - this.lastTimeUpdated; //ms
227
- this.log.debug('Watchdog: time of last update '+sinceLastUpdate/1000+' sec');
228
- const lastIsConnected = this.isConnected;
229
- this.isConnected = this.lastStateUpdatedHigh > 0 && sinceLastUpdate < this.settings.intervall*3;
230
- if (this.lastStateUpdatedLow == 0) {
231
- if (this.lastStateUpdatedHigh == 0) {
232
- this.log.warn('Not data can be read! Please check your settings.');
233
- } else {
234
- this.log.warn('Not all data can be read! Please reduce the intervall value.');
235
- }
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;
236
239
  }
237
- if (this.isConnected !== lastIsConnected ) this.setState('info.connection', this.isConnected, true);
238
- this.lastStateUpdatedLow = 0;
239
- this.lastStateUpdatedHigh = 0;
240
+ }
240
241
 
241
- if (sinceLastUpdate > this.settings.intervall*10) {
242
- this.log.warn('watchdog: restart Adapter...');
243
- this.restart();
244
- }
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();
245
249
  }
246
- },60000);
250
+
251
+ },this.settings.lowIntervall);
247
252
  }
248
253
 
249
254
 
@@ -256,21 +261,25 @@ class Sun2000 extends utils.Adapter {
256
261
  await this.setStateAsync('info.ip', {val: this.config.address, ack: true});
257
262
  await this.setStateAsync('info.port', {val: this.config.port, ack: true});
258
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});
259
265
 
260
266
  // Load user settings
261
267
  if (this.config.address !== '' || this.config.port > 0 || this.config.updateInterval > 0 ) {
262
- this.settings.intervall = this.config.updateInterval*1000; //ms
268
+ this.settings.highIntervall = this.config.updateInterval*1000; //ms
263
269
  this.settings.address = this.config.address;
264
270
  this.settings.port = this.config.port;
265
271
  this.settings.modbusIds = this.config.modbusIds.split(',').map((n) => {return Number(n);});
266
272
 
267
273
  if (this.settings.modbusIds.length > 0 && this.settings.modbusIds.length < 6) {
268
- if (this.settings.intervall < 5000*this.settings.modbusIds.length ) {
269
- this.settings.intervall = 5000*this.settings.modbusIds.length;
274
+ if (this.settings.highIntervall < 5000*this.settings.modbusIds.length ) {
275
+ this.settings.highIntervall = 5000*this.settings.modbusIds.length;
276
+ }
277
+ if (this.settings.highIntervall >= this.settings.lowIntervall) {
278
+ this.settings.lowIntervall = this.settings.highIntervall;
270
279
  }
271
- await this.setStateAsync('info.modbusUpdateInterval', {val: this.settings.intervall/1000, ack: true});
280
+ await this.setStateAsync('info.modbusUpdateInterval', {val: this.settings.highIntervall/1000, ack: true});
272
281
  for (const [i,id] of this.settings.modbusIds.entries()) {
273
- 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
274
283
  }
275
284
  await this.InitProcess();
276
285
  } else {
@@ -293,35 +302,34 @@ class Sun2000 extends utils.Adapter {
293
302
 
294
303
  const start = new Date().getTime();
295
304
  this.log.debug('### DataPolling START '+ Math.round((start-this.lastTimeUpdated)/1000)+' sec ###');
296
- if (this.lastTimeUpdated > 0 && (start-this.lastTimeUpdated)/1000 > this.settings.intervall/1000 + 1) {
297
- this.log.warn('time intervall '+(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');
298
307
  }
299
308
  this.lastTimeUpdated = start;
300
- const nextLoop = this.settings.intervall - start % (this.settings.intervall) + start;
309
+ const nextLoop = this.settings.highIntervall - start % (this.settings.highIntervall) + start;
301
310
 
302
311
  //High Loop
303
312
  for (const item of this.inverters) {
304
313
  this.modbusClient.setID(item.modbusId);
305
314
  this.lastStateUpdatedHigh += await this.state.updateStates(item,this.modbusClient,dataRefreshRate.high,timeLeft(nextLoop));
306
315
  }
316
+ await this.state.runProcessHooks(dataRefreshRate.high);
307
317
 
308
318
  if (timeLeft(nextLoop) > 500) {
309
- await this.state.runProcessHooks(dataRefreshRate.high);
310
319
  //Low Loop
311
320
  for (const [i,item] of this.inverters.entries()) {
312
321
  this.modbusClient.setID(item.modbusId);
313
322
  //this.log.debug('+++++ Loop: '+i+' Left Time: '+timeLeft(nextLoop,(i+1)/this.inverters.length)+' Faktor '+((i+1)/this.inverters.length));
314
323
  this.lastStateUpdatedLow += await this.state.updateStates(item,this.modbusClient,dataRefreshRate.low,timeLeft(nextLoop,(i+1)/this.inverters.length));
315
324
  }
316
- await this.state.runProcessHooks(dataRefreshRate.low);
317
325
  }
326
+ await this.state.runProcessHooks(dataRefreshRate.low);
318
327
 
319
328
  if (this.pollingTimer) this.clearTimeout(this.pollingTimer);
320
329
  this.pollingTimer = this.setTimeout(() => {
321
330
  this.dataPolling(); //recursiv
322
331
  }, timeLeft(nextLoop));
323
332
  this.log.debug('### DataPolling STOP ###');
324
- //this.state.mitnightProcess();
325
333
  }
326
334
 
327
335
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.sun2000",
3
- "version": "0.1.3",
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.13"
26
+ "modbus-serial": "^8.0.16"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@alcalzone/release-script": "^3.6.0",