iobroker.sun2000 0.1.2-alpha.3 → 0.1.3

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
@@ -26,6 +26,8 @@ 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).
@@ -33,7 +35,6 @@ The development of this adapter was inspired by discussions from the forum threa
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
 
36
-
37
38
  ## Configure inverters
38
39
 
39
40
  In order to use the Modbus connection, all Huawei devices must use the latest firmware
@@ -45,8 +46,9 @@ To log into the app as an `installer` you need usually the password:`00000a` or
45
46
  You may also need a password to connect to the inverters own WLAN: `Changeme`
46
47
 
47
48
  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
- If you use two inverters, then connect to the second inverter and read the communication address there too. A maximum of 2 inverters can be connected via Modbus.
49
+ If you use two inverters, then connect to the second inverter and read the communication address there too.
49
50
 
51
+ [How activate 'Modbus TCP' - from huawei forum](https://forum.huawei.com/enterprise/en/modbus-tcp-guide/thread/789585-100027)
50
52
 
51
53
  ## Settings
52
54
 
@@ -61,19 +63,16 @@ If you use two inverters, then connect to the second inverter and read the commu
61
63
  Placeholder for the next version (at the beginning of the line):
62
64
  ### **WORK IN PROGRESS**
63
65
  -->
64
- ### 0.1.2-alpha.3 (2024-01-12)
65
- * fix: wrong deploying date
66
+ ### 0.1.3 (2024-01-17)
67
+ * display the data from PV strings (#27)
68
+ * optimize the timing of interval loop
69
+ * improved handling of read timeouts from more then 2 inverters
66
70
 
67
- ### 0.1.2-alpha.2 (2024-01-12)
71
+ ### 0.1.2 (2024-01-12)
68
72
  * fix: no Data if interval less 20 sec (#24)
69
-
70
- ### 0.1.2-alpha.1 (2024-01-11)
71
- * deploy npm package
72
-
73
- ### 0.1.2-alpha.0 (2024-01-11)
74
73
  * prepare collected values more precisely
75
74
  * expand up to 5 inverters #18
76
- * fix problems with multiple inverters
75
+ * fix: problems with multiple inverters
77
76
 
78
77
  ### 0.1.1 (2024-01-07)
79
78
  * fix some collected values
package/io-package.json CHANGED
@@ -1,8 +1,34 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "sun2000",
4
- "version": "0.1.2-alpha.3",
4
+ "version": "0.1.3",
5
5
  "news": {
6
+ "0.1.3": {
7
+ "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
+ "de": "die Daten von PV-Strings anzeigen (#27)\noptimieren sie das timing der intervallschleife\nverbesserte handhabung von lesezeitausgängen von mehr als 2 wechselrichtern",
9
+ "ru": "отобразить данные из строк PV (#27)\nоптимизировать время интервального цикла\nулучшенная обработка считываемых таймаутов от более чем 2 инверторов",
10
+ "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",
11
+ "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",
12
+ "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",
13
+ "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",
14
+ "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",
15
+ "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",
16
+ "uk": "відображення даних з ПВ-рядок (#27)\nоптимізуйте частимізацію інтервалної петлі\nполіпшена обробка часу читання з більш ніж 2 інверторів",
17
+ "zh-cn": "显示 PV 字符串中的数据 (# 27)\n优化间隔循环的时间\n改进对超过2个反转器的断读处理"
18
+ },
19
+ "0.1.2": {
20
+ "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",
21
+ "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",
22
+ "ru": "исправление: нет данных, если интервал менее 20 секунд (#24)\nготовить собранные значения точнее\n#18\nисправить проблемы с несколькими инверторами",
23
+ "pt": "correção: não Dados se intervalo menos 20 segundos (#24)\npreparar valores coletados mais precisamente\nexpandir até 5 inversores #18\ncorrigir problemas com vários inversores",
24
+ "nl": "fix: geen Gegevens indien interval minder 20 sec (#24)\nverzamelde waarden nauwkeuriger voorbereiden\nuit te breiden tot 5 omvormers #18\nproblemen oplossen met meerdere inverters",
25
+ "fr": "correction : pas de données si l'intervalle est inférieur à 20 secondes (#24)\npréparer plus précisément les valeurs collectées\nétendre jusqu'à 5 onduleurs #18\nrésoudre les problèmes avec plusieurs onduleurs",
26
+ "it": "correzione: no Dati se intervallo meno 20 sec (#24)\npreparare i valori raccolti più precisamente\nespandere fino a 5 inverter #18\nrisolvere problemi con più inverter",
27
+ "es": "fijado: no Datos si intervalo menos 20 segundos (#24)\npreparar los valores recogidos con mayor precisión\nampliar hasta 5 inversores #18\nsolucionar problemas con múltiples inversores",
28
+ "pl": "fix: no Data if interval less 20 sec (# 24)\ndokładniej przygotować zebrane wartości\nrozszerzyć do 5 inwerterów # 18\nrozwiązać problemy z wieloma inwerterami",
29
+ "uk": "виправити: немає даних, якщо інтервал менше 20 сек (#24)\nпідготовка зібраних значень точно\nрозширити до 5 інверторів #18\nвиправити проблеми з декількома інверторами",
30
+ "zh-cn": "固定: 如果间隔小于20秒(# 24) 则无数据\n更准确地编制收集的数值\n扩展至5个反转器 # 18\n解决多个反转器的问题"
31
+ },
6
32
  "0.1.2-alpha.3": {
7
33
  "en": "fix: wrong deploying date",
8
34
  "de": "fix: falsches bereitstellungsdatum",
@@ -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
 
@@ -81,8 +80,9 @@ class ModbusConnect extends DeviceInterface {
81
80
 
82
81
  async _checkError(err) {
83
82
  if (err.modbusCode == null) {
83
+ this.adapter.log.debug('modbusCode == 0!');
84
+ await this.close();
84
85
  //https://github.com/yaacov/node-modbus-serial/issues/96
85
- //this.adapter.log.debug('Client destroy!');
86
86
  //await this._destroy();
87
87
  //this.adapter.log.debug('Client destroy!');
88
88
  await this._create();
package/lib/register.js CHANGED
@@ -151,6 +151,10 @@ class Registers {
151
151
  state: {id: 'info.ratedPower', name: 'Rated power', type: 'number', unit: 'kW', role: 'value.power'},
152
152
  register: {reg: 30073, type: dataType.int32, gain:1000}
153
153
  },
154
+ {
155
+ state: {id: 'info.numberPVStrings', name: 'Number of PV Strings', type: 'number', unit: '', role: 'value'},
156
+ register: {reg: 30071, type: dataType.uint16}
157
+ },
154
158
  {
155
159
  state: {id: 'info.numberMPPTrackers', name: 'Number of MPP trackers', type: 'number', unit: '', role: 'value'},
156
160
  register: {reg: 30072, type: dataType.uint16}
@@ -326,7 +330,45 @@ class Registers {
326
330
  {
327
331
  state: {id: 'dailyEnergyYield', name: 'Daily Energy Yield', type: 'number', unit: 'kWh', role: 'value.power.produced'},
328
332
  register: {reg: 32114, type: dataType.uint32, gain: 100}
329
- }]
333
+ }
334
+ ],
335
+ preHook: (path,reg) => {
336
+ const noPVString = this.stateCache.get(path+'info.numberPVStrings')?.value;
337
+ if (noPVString > 0) {
338
+ if (!stringFieldsTemplate.generated) stringFieldsTemplate.generated = 0;
339
+ if (stringFieldsTemplate.generated < noPVString) {
340
+ for (let i = stringFieldsTemplate.generated; i < noPVString; i++) {
341
+ //clonen
342
+ //const statePV = Object.assign({},stringFieldsTemplate.states[0]);
343
+ const statePV = JSON.parse(JSON.stringify(stringFieldsTemplate.states[0]));
344
+ const stateCu = JSON.parse(JSON.stringify(stringFieldsTemplate.states[1]));
345
+ const statePo = JSON.parse(JSON.stringify(stringFieldsTemplate.states[2]));
346
+ statePV.state.id = 'string.PV'+(i+1)+'Voltage';
347
+ statePV.register.reg = (stringFieldsTemplate.states[0].register?.reg ?? 0)+ (i*2);
348
+ statePV.register.type = stringFieldsTemplate.states[0].register?.type; //types are not copied?!
349
+ stateCu.state.id = 'string.PV'+(i+1)+'Current';
350
+ stateCu.register.reg = (stringFieldsTemplate.states[1].register?.reg ?? 0)+ (i*2);
351
+ stateCu.register.type = stringFieldsTemplate.states[1].register?.type;
352
+ statePo.state.id = 'string.PV'+(i+1)+'Power';
353
+ reg.states.push(statePV);
354
+ reg.states.push(stateCu);
355
+ reg.states.push(statePo);
356
+ }
357
+ }
358
+ stringFieldsTemplate.generated = noPVString;
359
+ }
360
+ },
361
+ postHook: (path) => {
362
+ const noPVString = this.stateCache.get(path+'info.numberPVStrings')?.value;
363
+ if (noPVString > 0) {
364
+ for (let i = 1; i <= noPVString; i++) {
365
+ const voltage = this.stateCache.get(path+'string.PV'+i+'Voltage')?.value;
366
+ const current = this.stateCache.get(path+'string.PV'+i+'Current')?.value;
367
+ this.stateCache.set(path+'string.PV'+i+'Power',Math.round(voltage*current),{type: 'number'});
368
+ }
369
+ }
370
+ }
371
+
330
372
  },
331
373
  {
332
374
  address : 37100,
@@ -457,6 +499,23 @@ 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,
@@ -469,7 +528,7 @@ class Registers {
469
528
  }
470
529
  }
471
530
  ];
472
- this.processHooks = [
531
+ this.postProcessHooks = [
473
532
  {
474
533
  refresh : dataRefreshRate.high,
475
534
  states : [
@@ -623,6 +682,7 @@ class Registers {
623
682
 
624
683
  async processRegister(reg,data) {
625
684
  const path = this.getStatePath(reg.type);
685
+ if (reg.preHook) reg.preHook(path,reg);
626
686
  if (reg.states) {
627
687
  for(const field of reg.states) {
628
688
  const state = field.state;
@@ -658,7 +718,7 @@ class Registers {
658
718
  let readRegisters = 0;
659
719
  for (const reg of this.registerFields) {
660
720
  if (duration) {
661
- if (new Date().getTime() - start > (duration - 3000)) {
721
+ if (new Date().getTime() - start > (duration - 1000)) {
662
722
  this.adapter.log.debug('Duration: '+Math.round(duration/1000)+' used time: '+ (new Date().getTime() - start)/1000);
663
723
  break;
664
724
  }
@@ -722,7 +782,7 @@ class Registers {
722
782
 
723
783
 
724
784
  async runProcessHooks(refreshRate) {
725
- for (const hook of this.processHooks) {
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]) {
@@ -736,6 +796,21 @@ class Registers {
736
796
  this.storeStates(); //fire and forget
737
797
  }
738
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
+ } );
811
+ }
812
+ */
813
+
739
814
  async _loadStates() {
740
815
  let value = await this.adapter.getStateAsync('collected.gridExportStart');
741
816
  this.stateCache.set('collected.gridExportStart',value?.val, {type : 'number', stored : true });
package/main.js CHANGED
@@ -26,7 +26,8 @@ class Sun2000 extends utils.Adapter {
26
26
  });
27
27
 
28
28
  this.lastTimeUpdated = 0;
29
- this.lastStateUpdated = 0;
29
+ this.lastStateUpdatedHigh = 0;
30
+ this.lastStateUpdatedLow = 0;
30
31
  this.isConnected = false;
31
32
  this.inverters = [];
32
33
  this.settings = {
@@ -54,7 +55,7 @@ class Sun2000 extends utils.Adapter {
54
55
  await this.extendObjectAsync('info', {
55
56
  type: 'channel',
56
57
  common: {
57
- name: 'info',
58
+ name: 'channel info',
58
59
  role: 'info'
59
60
  },
60
61
  native: {}
@@ -76,16 +77,14 @@ class Sun2000 extends utils.Adapter {
76
77
  await this.extendObjectAsync('meter', {
77
78
  type: 'device',
78
79
  common: {
79
- name: 'meter',
80
- role: 'info'
80
+ name: 'device meter'
81
81
  },
82
82
  native: {}
83
83
  });
84
84
  await this.extendObjectAsync('collected', {
85
85
  type: 'channel',
86
86
  common: {
87
- name: 'collected',
88
- role: 'info'
87
+ name: 'channel collected'
89
88
  },
90
89
  native: {}
91
90
  });
@@ -93,21 +92,19 @@ class Sun2000 extends utils.Adapter {
93
92
  await this.extendObjectAsync('inverter', {
94
93
  type: 'device',
95
94
  common: {
96
- name: 'meter',
97
- role: 'info'
95
+ name: 'device inverter'
98
96
  },
99
97
  native: {}
100
98
  });
101
99
 
102
- //ES6 use a for (const [index, item] of array.entries()) of loop
103
- //for (const [i, item] of this.conf.entries()) {
100
+ //ES6 use a for (const [index, item] of array.entries()) of loop
104
101
  for (const [i, item] of this.inverters.entries()) {
105
102
  const path = 'inverter.'+String(i);
106
103
  item.path = path;
107
104
  await this.extendObjectAsync(path, {
108
105
  type: 'channel',
109
106
  common: {
110
- name: 'modbus'+i,
107
+ name: 'channel modbus'+i,
111
108
  role: 'indicator'
112
109
  },
113
110
  native: {}
@@ -116,7 +113,15 @@ class Sun2000 extends utils.Adapter {
116
113
  await this.extendObjectAsync(path+'.grid', {
117
114
  type: 'channel',
118
115
  common: {
119
- name: 'grid',
116
+ name: 'channel grid'
117
+ },
118
+ native: {}
119
+ });
120
+
121
+ await this.extendObjectAsync(path+'.info', {
122
+ type: 'channel',
123
+ common: {
124
+ name: 'channel info',
120
125
  role: 'info'
121
126
  },
122
127
  native: {}
@@ -125,8 +130,15 @@ class Sun2000 extends utils.Adapter {
125
130
  await this.extendObjectAsync(path+'.battery', {
126
131
  type: 'channel',
127
132
  common: {
128
- name: 'battery',
129
- role: 'info'
133
+ name: 'channel battery'
134
+ },
135
+ native: {}
136
+ });
137
+
138
+ await this.extendObjectAsync(path+'.string', {
139
+ type: 'channel',
140
+ common: {
141
+ name: 'channel string'
130
142
  },
131
143
  native: {}
132
144
  });
@@ -134,8 +146,7 @@ class Sun2000 extends utils.Adapter {
134
146
  await this.extendObjectAsync(path+'.derived', {
135
147
  type: 'channel',
136
148
  common: {
137
- name: 'derived',
138
- role: 'indicator'
149
+ name: 'channel derived'
139
150
  },
140
151
  native: {}
141
152
  });
@@ -213,18 +224,26 @@ class Sun2000 extends utils.Adapter {
213
224
  if (!this.lastTimeUpdated) this.lastUpdated = 0;
214
225
  if (this.lastTimeUpdated > 0) {
215
226
  const sinceLastUpdate = new Date().getTime() - this.lastTimeUpdated; //ms
216
- this.log.debug('Watchdog: time to last update '+sinceLastUpdate/1000+' sec');
227
+ this.log.debug('Watchdog: time of last update '+sinceLastUpdate/1000+' sec');
217
228
  const lastIsConnected = this.isConnected;
218
- this.isConnected = this.lastStateUpdated > 0 && sinceLastUpdate < this.settings.intervall*2;
219
- this.log.debug('lastIsConncted '+lastIsConnected+' isConnectetd '+this.isConnected+' lastStateupdated '+this.lastStateUpdated);
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
+ }
236
+ }
237
+ if (this.isConnected !== lastIsConnected ) this.setState('info.connection', this.isConnected, true);
238
+ this.lastStateUpdatedLow = 0;
239
+ this.lastStateUpdatedHigh = 0;
220
240
 
221
- if (this.isConnected !== lastIsConnected ) this.setState('info.connection', this.isConnected, true);
222
241
  if (sinceLastUpdate > this.settings.intervall*10) {
223
242
  this.log.warn('watchdog: restart Adapter...');
224
243
  this.restart();
225
244
  }
226
245
  }
227
- },30000);
246
+ },60000);
228
247
  }
229
248
 
230
249
 
@@ -265,46 +284,38 @@ class Sun2000 extends utils.Adapter {
265
284
  }
266
285
 
267
286
  async dataPolling() {
268
- function timeLeft(target) {
269
- const left = target - new Date().getTime();
287
+
288
+ function timeLeft(target,factor =1) {
289
+ const left = Math.round((target - new Date().getTime())*factor);
270
290
  if (left < 0) return 0;
271
291
  return left;
272
292
  }
273
293
 
274
294
  const start = new Date().getTime();
275
-
276
- this.log.debug('### DataPolling START <> '+ Math.round((start-this.lastTimeUpdated)/1000)+' sec ###');
295
+ this.log.debug('### DataPolling START '+ Math.round((start-this.lastTimeUpdated)/1000)+' sec ###');
277
296
  if (this.lastTimeUpdated > 0 && (start-this.lastTimeUpdated)/1000 > this.settings.intervall/1000 + 1) {
278
297
  this.log.warn('time intervall '+(start-this.lastTimeUpdated)/1000+' sec');
279
298
  }
280
299
  this.lastTimeUpdated = start;
281
- let stateUpdated = 0;
282
-
283
300
  const nextLoop = this.settings.intervall - start % (this.settings.intervall) + start;
284
301
 
285
302
  //High Loop
286
303
  for (const item of this.inverters) {
287
304
  this.modbusClient.setID(item.modbusId);
288
- //this.log.info('### Left Time '+timeLeft/1000);
289
- stateUpdated += await this.state.updateStates(item,this.modbusClient,dataRefreshRate.high,timeLeft(nextLoop));
305
+ this.lastStateUpdatedHigh += await this.state.updateStates(item,this.modbusClient,dataRefreshRate.high,timeLeft(nextLoop));
290
306
  }
291
307
 
292
- if (timeLeft(nextLoop) > 2000) {
308
+ if (timeLeft(nextLoop) > 500) {
293
309
  await this.state.runProcessHooks(dataRefreshRate.high);
294
-
295
310
  //Low Loop
296
- for (const item of this.inverters) {
311
+ for (const [i,item] of this.inverters.entries()) {
297
312
  this.modbusClient.setID(item.modbusId);
298
- //this.log.info('### Left Time '+timeLeft/1000);
299
- stateUpdated += await this.state.updateStates(item,this.modbusClient,dataRefreshRate.low,timeLeft(nextLoop));
300
- }
301
- if (timeLeft(nextLoop) > 1000) {
302
- await this.state.runProcessHooks(dataRefreshRate.low);
313
+ //this.log.debug('+++++ Loop: '+i+' Left Time: '+timeLeft(nextLoop,(i+1)/this.inverters.length)+' Faktor '+((i+1)/this.inverters.length));
314
+ this.lastStateUpdatedLow += await this.state.updateStates(item,this.modbusClient,dataRefreshRate.low,timeLeft(nextLoop,(i+1)/this.inverters.length));
303
315
  }
316
+ await this.state.runProcessHooks(dataRefreshRate.low);
304
317
  }
305
318
 
306
- this.lastStateUpdated = stateUpdated;
307
-
308
319
  if (this.pollingTimer) this.clearTimeout(this.pollingTimer);
309
320
  this.pollingTimer = this.setTimeout(() => {
310
321
  this.dataPolling(); //recursiv
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.sun2000",
3
- "version": "0.1.2-alpha.3",
3
+ "version": "0.1.3",
4
4
  "description": "sun2000",
5
5
  "author": {
6
6
  "name": "bolliy",