iobroker.sun2000 0.1.2-alpha.1

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/lib/types.js ADDED
@@ -0,0 +1,119 @@
1
+ const modbusErrorMessages =
2
+ [ 'Unknown error',
3
+ 'Illegal function (device does not support this read/write function)',
4
+ 'Illegal data address (register not supported by device)',
5
+ 'Illegal data value (value cannot be written to this register)',
6
+ 'Slave device failure (device reports internal error)',
7
+ 'Acknowledge (requested data will be available later)',
8
+ 'Slave device busy (retry request again later)'
9
+ ];
10
+
11
+ const batteryStatus = [
12
+ 'OFFLINE',
13
+ 'STANDBY',
14
+ 'RUNNING',
15
+ 'FAULT',
16
+ 'SLEEP_MODE'
17
+ ];
18
+
19
+ const dataRefreshRate = {
20
+ low : 'low',
21
+ high : 'high',
22
+ compare (refresh,fieldRefresh) {
23
+ if (!fieldRefresh && refresh !== this.high ) return true;
24
+ return (fieldRefresh === refresh );
25
+ }
26
+ };
27
+
28
+ const deviceType = {
29
+ inverter : Symbol('inverter'),
30
+ meter : Symbol('meter'),
31
+ battery : Symbol('battery')
32
+ };
33
+
34
+ //export enum ModbusDatatype {
35
+ const dataType = {
36
+ //enums using Sympls
37
+ int16: Symbol('int16'),
38
+ int32: Symbol('int32'),
39
+ string: Symbol('string'),
40
+ uint16: Symbol('uint16'),
41
+ uint32: Symbol('uint32'),
42
+
43
+ size(type) {
44
+ switch (type) {
45
+ case this.int16:
46
+ return 1;
47
+ case this.int32:
48
+ return 2;
49
+ case this.uint16:
50
+ return 1;
51
+ case this.uint32:
52
+ return 2;
53
+ case this.string:
54
+ return undefined;
55
+ }
56
+ },
57
+
58
+ convert(array,type) {
59
+ switch (type) {
60
+ case this.int16:
61
+ return this.readSignedInt16(array);
62
+ case this.int32:
63
+ return this.readSignedInt32(array);
64
+ case this.uint16:
65
+ return this.readUnsignedInt16(array);
66
+ case this.uint32:
67
+ return this.readUnsignedInt32(array);
68
+ case this.string:
69
+ return this.readStr(array,array.length);
70
+ }
71
+ },
72
+ // some helper functions
73
+ readUnsignedInt16(array)
74
+ {
75
+ return array[0];
76
+ },
77
+
78
+
79
+ readUnsignedInt32(array)
80
+ {
81
+ return array[0] * 256 * 256 + array[1];
82
+ },
83
+
84
+ readSignedInt16(array)
85
+ {
86
+ let value = 0;
87
+ if (array[0] > 32767) value = array[0] - 65535;
88
+ else value = array[0];
89
+ return value;
90
+ },
91
+
92
+ readSignedInt32(array)
93
+ { let value = 0;
94
+ for (let i = 0; i < 2; i++) { value = (value << 16) | array[i]; }
95
+ return value;
96
+ },
97
+ readStr(array, length) {
98
+ const bytearray = [];
99
+ console.log(array);
100
+ for(let i = 0; i < length; i++)
101
+ {
102
+ if (array[i] > 0 ) {
103
+ bytearray.push(array[i] >> 8); //right shift
104
+ bytearray.push(array[i] & 0xff);
105
+ }
106
+ }
107
+ //const value = String.fromCharCode.apply(null, bytearray);
108
+ const value = String.fromCharCode(...bytearray);
109
+ return new String(value).trim();
110
+ }
111
+ };
112
+
113
+ module.exports = {
114
+ modbusErrorMessages,
115
+ batteryStatus,
116
+ dataRefreshRate,
117
+ deviceType,
118
+ dataType
119
+ };
package/main.js ADDED
@@ -0,0 +1,405 @@
1
+ 'use strict';
2
+
3
+ /*
4
+ * Created with @iobroker/create-adapter v2.5.0
5
+ */
6
+
7
+ // The adapter-core module gives you access to the core ioBroker functions
8
+ // you need to create an adapter
9
+ const utils = require('@iobroker/adapter-core');
10
+
11
+ const Registers = require(__dirname + '/lib/register.js');
12
+ const ModbusConnect = require(__dirname + '/lib/modbus_connect.js');
13
+ const {dataRefreshRate} = require(__dirname + '/lib/types.js');
14
+
15
+ // Load your modules here, e.g.:
16
+
17
+ class Sun2000 extends utils.Adapter {
18
+
19
+ /**
20
+ * @param {Partial<utils.AdapterOptions>} [options={}]
21
+ */
22
+ constructor(options) {
23
+ super({
24
+ ...options,
25
+ name: 'sun2000',
26
+ });
27
+
28
+ this.lastTimeUpdated = 0;
29
+ this.lastStateUpdated = 0;
30
+ this.isConnected = false;
31
+ this.inverters = [];
32
+ this.settings = {
33
+ intervall : 30000,
34
+ address : '',
35
+ port : 520,
36
+ };
37
+
38
+ this.on('ready', this.onReady.bind(this));
39
+ this.on('stateChange', this.onStateChange.bind(this));
40
+ // this.on('objectChange', this.onObjectChange.bind(this));
41
+ // this.on('message', this.onMessage.bind(this));
42
+ this.on('unload', this.onUnload.bind(this));
43
+ }
44
+
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
+ async initPath() {
54
+ this.log.debug('InitPath START....');
55
+ await this.extendObjectAsync('info', {
56
+ type: 'channel',
57
+ common: {
58
+ name: 'info',
59
+ role: 'info'
60
+ },
61
+ native: {}
62
+ });
63
+
64
+ await this.extendObjectAsync('info.connection', {
65
+ type: 'state',
66
+ common: {
67
+ name: 'Inverter connected',
68
+ type: 'boolean',
69
+ role: 'indicator.connected',
70
+ read: true,
71
+ write: false,
72
+ desc: 'Is the inverter connected?'
73
+ },
74
+ native: {},
75
+ });
76
+
77
+ await this.extendObjectAsync('meter', {
78
+ type: 'device',
79
+ common: {
80
+ name: 'meter',
81
+ role: 'info'
82
+ },
83
+ native: {}
84
+ });
85
+ await this.extendObjectAsync('collected', {
86
+ type: 'channel',
87
+ common: {
88
+ name: 'collected',
89
+ role: 'info'
90
+ },
91
+ native: {}
92
+ });
93
+
94
+ await this.extendObjectAsync('inverter', {
95
+ type: 'device',
96
+ common: {
97
+ name: 'meter',
98
+ role: 'info'
99
+ },
100
+ native: {}
101
+ });
102
+
103
+ //ES6 use a for (const [index, item] of array.entries()) of loop
104
+ //for (const [i, item] of this.conf.entries()) {
105
+ for (const [i, item] of this.inverters.entries()) {
106
+ const path = 'inverter.'+String(i);
107
+ item.path = path;
108
+ await this.extendObjectAsync(path, {
109
+ type: 'channel',
110
+ common: {
111
+ name: 'modbus'+i,
112
+ role: 'indicator'
113
+ },
114
+ native: {}
115
+ });
116
+
117
+ await this.extendObjectAsync(path+'.grid', {
118
+ type: 'channel',
119
+ common: {
120
+ name: 'grid',
121
+ role: 'info'
122
+ },
123
+ native: {}
124
+ });
125
+
126
+ await this.extendObjectAsync(path+'.battery', {
127
+ type: 'channel',
128
+ common: {
129
+ name: 'battery',
130
+ role: 'info'
131
+ },
132
+ native: {}
133
+ });
134
+
135
+ await this.extendObjectAsync(path+'.derived', {
136
+ type: 'channel',
137
+ common: {
138
+ name: 'derived',
139
+ role: 'indicator'
140
+ },
141
+ native: {}
142
+ });
143
+
144
+ }
145
+ this.log.debug('InitPath STOP');
146
+
147
+ }
148
+
149
+ async InitProcess() {
150
+ this.log.debug('InitProzess START');
151
+ this.state = new Registers(this);
152
+ this.modbusClient = new ModbusConnect(this,this.config.address,this.config.port);
153
+ try {
154
+ await this.initPath();
155
+ /*
156
+ await this.checkAndPrepare();
157
+ await processBatterie();
158
+ */
159
+ } catch (err) {
160
+ this.log.warn(err);
161
+ }
162
+ this.dataPolling();
163
+ this.runWatchDog();
164
+ this.atMidnight();
165
+ this.log.debug('InitProzess STOP');
166
+ }
167
+
168
+ atMidnight() {
169
+ const now = new Date();
170
+ const night = new Date(
171
+ now.getFullYear(),
172
+ now.getMonth(),
173
+ now.getDate() + 1, // the next day, ...
174
+ 0, 0, 0 // ...at 00:00:00 hours
175
+ );
176
+ const msToMidnight = night.getTime() - now.getTime();
177
+
178
+ if (this.mitnightTimer) this.clearTimeout(this.mitnightTimer);
179
+ this.mitnightTimer = this.setTimeout(async () => {
180
+ await this.state.mitnightProcess(); // the function being called at midnight.
181
+ this.atMidnight(); // reset again next midnight.
182
+ }, msToMidnight);
183
+ }
184
+
185
+ async checkAndPrepare() {
186
+ // Time of Using charging and discharging periodes (siehe Table 5-6)
187
+ // tCDP[3]= 127
188
+ const tCDP = [1,0,1440,383,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; //nicht aus dem Netz laden
189
+ this.modbusClient.setID(this.inverters[0].modbusId); //Master Inverter
190
+ const data = await this.modbusClient.readHoldingRegisters(47086,4); //
191
+ /*
192
+ 127 - Working mode settings
193
+ 2 : Maximise self consumptions (default)
194
+ 5 : Time Of Use(Luna) - hilfreich bei dynamischem Stromtarif (z.B Tibber)
195
+ */
196
+ const workingMode = data[0]; // Working mode settings 2:Maximise self consumptio5=)
197
+ const chargeFromGrid = data[1]; // Voraussetzung für Netzbezug in den Speicher (Luna)
198
+ const gridChargeCutOff = data[2]/10; // Ab welcher Schwelle wird der Netzbezug beendet (default 50 %)
199
+ const storageModel = data[3]; // Modell/Herrsteller des Speichers, 2 : HUAWEI-LUNA2000
200
+
201
+ if (storageModel == 2) { //wurde nur mit Luna getestet!
202
+ if (workingMode != 5 || chargeFromGrid != 1 ) {
203
+ this.log.debug('Row '+data+' Workingmode '+workingMode+ ' Charge from Grid '+chargeFromGrid+ ' Grid Cut Off '+gridChargeCutOff+'%');
204
+ await this.modbusClient.writeRegisters(47086,[5,1,500]);
205
+ //await writeRegistersAsync(1,47086,[5,1,500]); //[TOU,chargeFromGrid,50%]
206
+ await this.modbusClient.writeRegisters(47255,tCDP);
207
+ //await writeRegistersAsync(1,47255,tCDP); //Plan:1,StartZeit:00:00,EndZeit: 24:00,Endladen/täglich
208
+ /* ggf. sinnvoll
209
+ await writeRegistersAsync(1,47075,[0,5000]); //max. charging power
210
+ await writeRegistersAsync(1,47077,[0,5000]); //max. discharging power
211
+ */
212
+ }
213
+ }
214
+ }
215
+
216
+ runWatchDog() {
217
+ this.watchDogHandle && this.clearInterval(this.watchDogHandle);
218
+ this.watchDogHandle = this.setInterval( () => {
219
+ if (!this.lastTimeUpdated) this.lastUpdated = 0;
220
+ if (this.lastTimeUpdated > 0) {
221
+ const sinceLastUpdate = (new Date().getTime() - this.lastTimeUpdated)/1000;
222
+ this.log.debug('Watchdog: time to last update '+sinceLastUpdate+' sec');
223
+ const lastIsConnected = this.isConnected;
224
+ this.isConnected = this.lastStateUpdated > 0 && sinceLastUpdate < this.config.updateInterval*2;
225
+ this.log.debug('lastIsConncted '+lastIsConnected+' isConnectetd '+this.isConnected+' lastStateupdated '+this.lastStateUpdated);
226
+
227
+ if (this.isConnected !== lastIsConnected ) this.setState('info.connection', this.isConnected, true);
228
+ if (sinceLastUpdate > this.config.updateInterval*10) {
229
+ this.log.warn('watchdog: restart Adapter...');
230
+ this.restart();
231
+ }
232
+ }
233
+ },30000);
234
+ }
235
+
236
+
237
+ /**
238
+ * Is called when databases are connected and adapter received configuration.
239
+ */
240
+ async onReady() {
241
+
242
+ // Initialize your adapter here
243
+ await this.setStateAsync('info.ip', {val: this.config.address, ack: true});
244
+ await this.setStateAsync('info.port', {val: this.config.port, ack: true});
245
+ //await this.setStateAsync('info.modbusUpdateInterval', {val: this.config.updateInterval, ack: true});
246
+ await this.setStateAsync('info.modbusIds', {val: this.config.modbusIds, ack: true});
247
+
248
+ // Load user settings
249
+ if (this.config.address !== '' || this.config.port > 0 || this.config.updateInterval > 0 ) {
250
+ this.settings.intervall = this.config.updateInterval*1000;
251
+ this.settings.address = this.config.address;
252
+ this.settings.port = this.config.port;
253
+ this.settings.modbusIds = this.config.modbusIds.split(',').map((n) => {return Number(n);});
254
+
255
+ if (this.settings.modbusIds.length > 0 && this.settings.modbusIds.length < 6) {
256
+ if (this.settings.intervall < 10000*this.settings.modbusIds.length ) {
257
+ this.config.updateInterval = 10000*this.settings.modbusIds.length;
258
+ }
259
+ await this.setStateAsync('info.modbusUpdateInterval', {val: this.settings.intervall/1000, ack: true});
260
+ for (const [i,id] of this.settings.modbusIds.entries()) {
261
+ this.inverters.push({index: i, modbusId: id, meter: (i==0)});
262
+ }
263
+ await this.InitProcess();
264
+ } else {
265
+ this.log.error('*** Adapter deactivated, can\'t parse modbusIds ! ***');
266
+ this.setForeignState('system.' + this.namespace + '.alive', false);
267
+ }
268
+ } else {
269
+ this.log.error('*** Adapter deactivated, credentials missing in Adapter Settings ! ***');
270
+ this.setForeignState('system.' + this.namespace + '.alive', false);
271
+ }
272
+ }
273
+
274
+ async dataPolling() {
275
+
276
+ function timeLeft(target) {
277
+ const left = target - new Date().getTime();
278
+ if (left < 0) return 0;
279
+ return left;
280
+ }
281
+ const start = new Date().getTime();
282
+
283
+ this.log.debug('### DataPolling START <- '+ Math.round((start-this.lastTimeUpdated)/1000)+' sec ###');
284
+ if (this.lastTimeUpdated > 0 && (start-this.lastTimeUpdated)/1000 > this.config.updateInterval + 1) {
285
+ this.log.warn('time intervall '+(start-this.lastTimeUpdated)/1000+' sec');
286
+ }
287
+ this.lastTimeUpdated = start;
288
+ let stateUpdated = 0;
289
+
290
+ //ZielZeit
291
+ //nextTick = this.config.updateInterval*1000 - now % (this.config.updateInterval*1000);
292
+
293
+ const target = this.config.updateInterval*1000 - start % (this.config.updateInterval*1000) + start;
294
+
295
+ //High Loop
296
+ for (const item of this.inverters) {
297
+ this.modbusClient.setID(item.modbusId);
298
+ //this.log.info('### Left Time '+timeLeft/1000);
299
+ stateUpdated += await this.state.updateStates(item,this.modbusClient,dataRefreshRate.high,timeLeft(target));
300
+ }
301
+
302
+ if (timeLeft(target) > 5000) {
303
+ await this.state.runProcessHooks(dataRefreshRate.high);
304
+
305
+ //Low Loop
306
+ for (const item of this.inverters) {
307
+ this.modbusClient.setID(item.modbusId);
308
+ //this.log.info('### Left Time '+timeLeft/1000);
309
+ stateUpdated += await this.state.updateStates(item,this.modbusClient,dataRefreshRate.low,timeLeft(target));
310
+ }
311
+ if (timeLeft(target) > 2000) {
312
+ await this.state.runProcessHooks(dataRefreshRate.low);
313
+ }
314
+ }
315
+
316
+ this.lastStateUpdated = stateUpdated;
317
+
318
+ if (this.timer) this.clearTimeout(this.timer);
319
+ this.timer = this.setTimeout(() => {
320
+ this.dataPolling(); //recursiv
321
+ }, timeLeft(target));
322
+ this.log.debug('### DataPolling STOP ###');
323
+ //this.state.mitnightProcess();
324
+ }
325
+
326
+ /**
327
+ * Is called when adapter shuts down - callback has to be called under any circumstances!
328
+ * @param {() => void} callback
329
+ */
330
+ onUnload(callback) {
331
+ try {
332
+ this.timer && this.clearTimeout(this.timer);
333
+ this.mitnightTimer && this.clearTimeout(this.mitnightTimer);
334
+ this.watchDogHandle && this.clearInterval(this.watchDogHandle);
335
+ this.modbusClient && this.modbusClient.close();
336
+ this.setState('info.connection', false, true);
337
+ this.log.info('cleaned everything up...');
338
+ callback();
339
+ } catch (e) {
340
+ callback();
341
+ }
342
+ }
343
+
344
+ // If you need to react to object changes, uncomment the following block and the corresponding line in the constructor.
345
+ // You also need to subscribe to the objects with `this.subscribeObjects`, similar to `this.subscribeStates`.
346
+ // /**
347
+ // * Is called if a subscribed object changes
348
+ // * @param {string} id
349
+ // * @param {ioBroker.Object | null | undefined} obj
350
+ // */
351
+ // onObjectChange(id, obj) {
352
+ // if (obj) {
353
+ // // The object was changed
354
+ // this.log.info(`object ${id} changed: ${JSON.stringify(obj)}`);
355
+ // } else {
356
+ // // The object was deleted
357
+ // this.log.info(`object ${id} deleted`);
358
+ // }
359
+ // }
360
+
361
+ /**
362
+ * Is called if a subscribed state changes
363
+ * @param {string} id
364
+ * @param {ioBroker.State | null | undefined} state
365
+ */
366
+ onStateChange(id, state) {
367
+ if (state) {
368
+ // The state was changed
369
+ this.log.info(`state ${id} changed: ${state.val} (ack = ${state.ack})`);
370
+ } else {
371
+ // The state was deleted
372
+ this.log.info(`state ${id} deleted`);
373
+ }
374
+ }
375
+
376
+ // If you need to accept messages in your adapter, uncomment the following block and the corresponding line in the constructor.
377
+ // /**
378
+ // * Some message was sent to this instance over message box. Used by email, pushover, text2speech, ...
379
+ // * Using this method requires "common.messagebox" property to be set to true in io-package.json
380
+ // * @param {ioBroker.Message} obj
381
+ // */
382
+ // onMessage(obj) {
383
+ // if (typeof obj === 'object' && obj.message) {
384
+ // if (obj.command === 'send') {
385
+ // // e.g. send email or pushover or whatever
386
+ // this.log.info('send command');
387
+
388
+ // // Send response in callback if required
389
+ // if (obj.callback) this.sendTo(obj.from, obj.command, 'Message received', obj.callback);
390
+ // }
391
+ // }
392
+ // }
393
+
394
+ }
395
+
396
+ if (require.main !== module) {
397
+ // Export the constructor in compact mode
398
+ /**
399
+ * @param {Partial<utils.AdapterOptions>} [options={}]
400
+ */
401
+ module.exports = (options) => new Sun2000(options);
402
+ } else {
403
+ // otherwise start the instance directly
404
+ new Sun2000();
405
+ }
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "iobroker.sun2000",
3
+ "version": "0.1.2-alpha.1",
4
+ "description": "sun2000",
5
+ "author": {
6
+ "name": "bolliy",
7
+ "email": "stephan@mante.info"
8
+ },
9
+ "homepage": "https://github.com/bolliy/ioBroker.sun2000",
10
+ "license": "MIT",
11
+ "keywords": [
12
+ "ioBroker",
13
+ "template",
14
+ "Smart Home",
15
+ "home automation"
16
+ ],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/bolliy/ioBroker.sun2000.git"
20
+ },
21
+ "engines": {
22
+ "node": ">= 16"
23
+ },
24
+ "dependencies": {
25
+ "@iobroker/adapter-core": "^3.0.4",
26
+ "modbus-serial": "^8.0.13"
27
+ },
28
+ "devDependencies": {
29
+ "@alcalzone/release-script": "^3.6.0",
30
+ "@alcalzone/release-script-plugin-iobroker": "^3.6.0",
31
+ "@alcalzone/release-script-plugin-license": "^3.5.9",
32
+ "@alcalzone/release-script-plugin-manual-review": "^3.5.9",
33
+ "@iobroker/adapter-dev": "^1.2.0",
34
+ "@iobroker/testing": "^4.1.0",
35
+ "@tsconfig/node16": "^16.1.1",
36
+ "@types/chai": "^4.3.10",
37
+ "@types/chai-as-promised": "^7.1.8",
38
+ "@types/mocha": "^10.0.4",
39
+ "@types/node": "^20.10.6",
40
+ "@types/proxyquire": "^1.3.31",
41
+ "@types/sinon": "^17.0.1",
42
+ "@types/sinon-chai": "^3.2.12",
43
+ "chai": "^4.3.10",
44
+ "chai-as-promised": "^7.1.1",
45
+ "eslint": "^8.56.0",
46
+ "mocha": "^10.2.0",
47
+ "proxyquire": "^2.1.3",
48
+ "sinon": "^17.0.1",
49
+ "sinon-chai": "^3.7.0",
50
+ "typescript": "~5.3.3"
51
+ },
52
+ "main": "main.js",
53
+ "files": [
54
+ "admin{,/!(src)/**}/!(tsconfig|tsconfig.*|.eslintrc).{json,json5}",
55
+ "admin{,/!(src)/**}/*.{html,css,png,svg,jpg,js}",
56
+ "lib/",
57
+ "www/",
58
+ "io-package.json",
59
+ "LICENSE",
60
+ "main.js"
61
+ ],
62
+ "scripts": {
63
+ "test:js": "mocha --config test/mocharc.custom.json \"{!(node_modules|test)/**/*.test.js,*.test.js,test/**/test!(PackageFiles|Startup).js}\"",
64
+ "test:package": "mocha test/package --exit",
65
+ "test:integration": "mocha test/integration --exit",
66
+ "test": "npm run test:js && npm run test:package",
67
+ "check": "tsc --noEmit -p tsconfig.check.json",
68
+ "lint": "eslint .",
69
+ "translate": "translate-adapter",
70
+ "release": "release-script",
71
+ "release-minor": "release-script minor --yes",
72
+ "release-major": "release-script major --yes"
73
+ },
74
+ "bugs": {
75
+ "url": "https://github.com/bolliy/ioBroker.sun2000/issues"
76
+ },
77
+ "readmeFilename": "README.md"
78
+ }