node.js based data collection for SMA Sunny WebBox
Costa Tsaousis (ktsaou) committed
Jan 6, 2016 at 19:28 UTC
aae85f5841691b58d64f33e82f2a1e8a16d2288e
1 file changed
+350
plugins.d/sma_webbox.plugin
new
+350
@@ -0,0 +1,350 @@
1
+#!/usr/bin/env node
2
+
3
+// This program will connect to one or more SMA Sunny Webboxes
4
+// to get the Solar Power Generated (current, today, total).
5
+
6
+// It needs a configuration file named sma_webbox.conf
7
+// The configuration is a JSON array, like this:
8
+
9
+/* --- BEGIN EXAMPLE CONFIGURATION ---
10
+
11
+[
12
+ {
13
+ "name": "label_of_solar_installation1",
14
+ "hostname": "10.11.13.2"
15
+ },
16
+ {
17
+ "name": "label_of_solar_installation2",
18
+ "hostname": "10.11.13.3"
19
+ }
20
+]
21
+
22
+ --- END EXAMPLE CONFIGURATION --- */
23
+
24
+// you can add an unlimited number of webboxes
25
+
26
+var http = require('http');
27
+var fs = require('fs');
28
+
29
+// --------------------------------------------------------------------------------------------------------------------
30
+// get NETDATA environment variables
31
+
32
+var NETDATA_PLUGINS_DIR = process.env.NETDATA_PLUGINS_DIR || __dirname;
33
+var NETDATA_CONFIG_DIR = process.env.NETDATA_CONFIG_DIR || '/etc/netdata';
34
+var NETDATA_UPDATE_EVERY = process.env.NETDATA_UPDATE_EVERY || 1;
35
+NETDATA_UPDATE_EVERY = NETDATA_UPDATE_EVERY * 1000;
36
+
37
+var filename = NETDATA_CONFIG_DIR + '/sma_webbox.conf';
38
+
39
+// --------------------------------------------------------------------------------------------------------------------
40
+// get command line arguments
41
+
42
+var DEBUG = false;
43
+var UPDATE_EVERY = NETDATA_UPDATE_EVERY;
44
+var EXIT_AFTER_MS = 3600 * 1000;
45
+
46
+function debug(msg) {
47
+ if(DEBUG)
48
+ console.error(__filename + ': DEBUG: ' + ((typeof(msg) === 'object')?JSON.stringify(msg):msg).toString());
49
+}
50
+
51
+function error(msg) {
52
+ console.error(__filename + ': ERROR: ' + ((typeof(msg) === 'object')?JSON.stringify(msg):msg).toString());
53
+}
54
+
55
+function netdata(msg) {
56
+ console.log(msg.toString());
57
+}
58
+
59
+var found_myself = false;
60
+process.argv.forEach(function (val, index, array) {
61
+ debug('PARAM: ' + val);
62
+
63
+ if(!found_myself) {
64
+ if(val === __filename)
65
+ found_myself = true;
66
+ }
67
+ else {
68
+ switch(val) {
69
+ case 'debug':
70
+ DEBUG = true;
71
+ debug('DEBUG enabled');
72
+ break;
73
+
74
+ default:
75
+ try {
76
+ var x = parseInt(val);
77
+ if(x > 0) {
78
+ UPDATE_EVERY = x * 1000;
79
+ if(UPDATE_EVERY < NETDATA_UPDATE_EVERY) {
80
+ UPDATE_EVERY = NETDATA_UPDATE_EVERY;
81
+ debug('Update frequency ' + x + 's is too low');
82
+ }
83
+
84
+ debug('Update frequency set to ' + UPDATE_EVERY + ' ms');
85
+ }
86
+ else error('Ignoring parameter: ' + val);
87
+ }
88
+ catch(e) {
89
+ error('Cannot get value of parameter: ' + val);
90
+ }
91
+ }
92
+ }
93
+});
94
+
95
+// there is no meaning to update the values sooner than 5 seconds
96
+// the SMA webbox collects data every 5 seconds
97
+if(UPDATE_EVERY < 5000) {
98
+ debug('Adjusting update frequency to 5 seconds');
99
+ UPDATE_EVERY = 5000;
100
+}
101
+
102
+// --------------------------------------------------------------------------------------------------------------------
103
+// parse configuration
104
+
105
+var Servers = new Array();
106
+try {
107
+ Servers = JSON.parse(fs.readFileSync(filename, 'utf8'));
108
+}
109
+catch(e) {
110
+ error('Cannot read configuration file ' + filename + ': ' + e.message);
111
+ process.exit(1);
112
+}
113
+
114
+
115
+// --------------------------------------------------------------------------------------------------------------------
116
+// library functions
117
+
118
+// function to get data from server
119
+function getSMAData(server, callback) {
120
+ // make sure there is not a request in progress for this webbox
121
+ if(typeof(server.running) === 'boolean' && server.running) {
122
+ debug(server.name + ' is already running');
123
+ return false;
124
+ }
125
+ server.running = true;
126
+
127
+ var now = new Date().getTime();
128
+
129
+ // align the time to collect the values
130
+ if(typeof(server.next_run) === 'undefined')
131
+ server.next_run = now - (now % UPDATE_EVERY) + UPDATE_EVERY;
132
+
133
+ // if it is too soon, we will collect data later
134
+ if(now < server.next_run) {
135
+ debug(server.name + ' not yet');
136
+ server.running = false;
137
+ return false;
138
+ }
139
+
140
+ // find the next refresh for this server
141
+ while(server.next_run < now)
142
+ server.next_run += UPDATE_EVERY;
143
+
144
+ if(typeof(server.last_updated) === 'undefined')
145
+ server.last_updated = 0;
146
+
147
+ if(typeof(server.agent) === 'undefined') {
148
+ server.agent = new http.Agent({
149
+ keepAlive: true,
150
+ keepAliveMsecs: UPDATE_EVERY * 5,
151
+ maxSockets: 2, // it needs 2, otherwise it ignores keepAlive
152
+ maxFreeSockets: 1
153
+ });
154
+ }
155
+
156
+ if(typeof(server.created) === 'undefined') {
157
+ netdata('CHART sma_webbox_' + server.name + '.current "" "Solar Power Production of ' + server.name + '" "Watts" sma_webbox_' + server.name + ' "" area 15000 ' + Math.round(UPDATE_EVERY / 1000));
158
+ netdata('DIMENSION GriPwr power absolute 1 1');
159
+ netdata('CHART sma_webbox_' + server.name + '.today "" "Today\'s Solar Power Production of ' + server.name + '" "kWatts" sma_webbox_' + server.name + ' "" area 15001 ' + Math.round(UPDATE_EVERY / 1000));
160
+ netdata('DIMENSION GriEgyTdy today absolute 1 1000');
161
+ netdata('CHART sma_webbox_' + server.name + '.total "" "Total Solar Power Production of ' + server.name + '" "kWatts" sma_webbox_' + server.name + ' "" area 15001 ' + Math.round(UPDATE_EVERY / 1000));
162
+ netdata('DIMENSION GriEgyTot total absolute 1 1000');
163
+ server.created = true;
164
+ }
165
+
166
+ server.error = 500;
167
+ server.success = false;
168
+ server.data = {
169
+ 'GriPwr': {
170
+ unit: null,
171
+ value: null
172
+ },
173
+ 'GriEgyTdy': {
174
+ unit: null,
175
+ value: null
176
+ },
177
+ 'GriEgyTot': {
178
+ unit: null,
179
+ value: null
180
+ }
181
+ };
182
+
183
+ var postData = 'RPC={"proc":"GetPlantOverview","format":"JSON","version":"1.0","id":"1"}'
184
+
185
+ var options = {
186
+ protocol: server.protocol || 'http:',
187
+ hostname: server.hostname,
188
+ path: server.path || '/rpc',
189
+ family: server.family || 4,
190
+ port: server.port || 80,
191
+ method: 'POST',
192
+ headers: {
193
+ 'Content-Type': 'application/x-www-form-urlencoded',
194
+ 'Connection': 'keep-alive',
195
+ 'Content-Length': postData.length
196
+ },
197
+ agent: server.agent
198
+ };
199
+
200
+ server.response = '';
201
+ server.request = http.request(options, function(res) {
202
+ res.setEncoding('utf8');
203
+ server.error = 0;
204
+
205
+ server.response_code = res.statusCode;
206
+
207
+ // check if we got HTTP/200
208
+ if(server.response_code !== 200) {
209
+ debug('Server responded with ' + server.response_code + ', failed to get data.');
210
+ server.error = 503;
211
+ server.running = false;
212
+ server.success = false;
213
+ if(typeof(callback) === 'function')
214
+ callback(server);
215
+ }
216
+
217
+ res.on('error', function() {
218
+ debug('Received HTTP read error, failed to get data.');
219
+ server.error = 504;
220
+ server.success = false;
221
+ server.running = false;
222
+ if(typeof(callback) === 'function')
223
+ callback(server);
224
+ });
225
+
226
+ res.on('data', function(chunk) {
227
+ server.response += chunk;
228
+ });
229
+
230
+ res.on('end', function() {
231
+ if(server.error !== 0)
232
+ return;
233
+
234
+ var t = new Date().getTime();
235
+
236
+ server.dt = 0;
237
+ if(server.last_updated !== 0)
238
+ server.dt = t - server.last_updated;
239
+
240
+ debug('RESPONSE: ' + server.response);
241
+ try {
242
+ server.response = JSON.parse(server.response);
243
+
244
+ var len = server.response.result.overview.length;
245
+ while(len--) {
246
+ var e = server.response.result.overview[len];
247
+ debug(e);
248
+ if(typeof(server.data[e.meta]) !== 'undefined') {
249
+ server.data[e.meta].value = e.value;
250
+ server.data[e.meta].unit = e.unit;
251
+ }
252
+ }
253
+ server.error = 0;
254
+ server.last_updated = t;
255
+
256
+ if(server.data['GriPwr'].value !== null) {
257
+ netdata('BEGIN sma_webbox_' + server.name + '.current ' + ((server.dt)?server.dt*1000:'').toString());
258
+ netdata('SET GriPwr = ' + server.data['GriPwr'].value);
259
+ netdata('END');
260
+ }
261
+
262
+ if(server.data['GriEgyTdy'].value !== null) {
263
+ netdata('BEGIN sma_webbox_' + server.name + '.today ' + ((server.dt)?server.dt*1000:'').toString());
264
+ netdata('SET GriEgyTdy = ' + Math.round(server.data['GriEgyTdy'].value * 1000));
265
+ netdata('END');
266
+ }
267
+
268
+ if(server.data['GriEgyTot'].value !== null) {
269
+ netdata('BEGIN sma_webbox_' + server.name + '.total ' + ((server.dt)?server.dt*1000:'').toString());
270
+ netdata('SET GriEgyTot = ' + Math.round(server.data['GriEgyTot'].value * 1000));
271
+ netdata('END');
272
+ }
273
+ }
274
+ catch(e) {
275
+ server.error = 501;
276
+ server.failure_message = e.message;
277
+ error('FAILED TO PARSE DATA: ' + e.message);
278
+ }
279
+
280
+ server.running = false;
281
+ if(typeof(callback) === 'function') {
282
+ server.success = true;
283
+ callback(server);
284
+ }
285
+ });
286
+ });
287
+
288
+ server.request.on('error', function(e) {
289
+ server.running = false;
290
+ server.error = 502;
291
+ server.failure_message = e.message;
292
+ error('problem with request to ' + server.hostname + ': ' + e.message);
293
+ });
294
+
295
+ // write data to request body
296
+ server.request.write(postData);
297
+ server.request.end();
298
+
299
+ return true;
300
+}
301
+
302
+// get a message for the current error
303
+function getSMAError(server) {
304
+ if(server.success) return 'OK';
305
+
306
+ if(typeof(server.error) === 'undefined')
307
+ return 'Not initialized';
308
+
309
+ switch(server.error) {
310
+ case 500: return 'Pre-fetch Error';
311
+ case 501: return 'Failed to parse server response';
312
+ case 502: return 'HTTP error while making request';
313
+ case 504: return 'Failed to read response data';
314
+ default: return 'Undefined Error';
315
+ }
316
+}
317
+
318
+// --------------------------------------------------------------------------------------------------------------------
319
+
320
+// if we don't have any servers to run
321
+// inform netdata we don't need to run on this machine
322
+if(!Servers.length) {
323
+ error('No SMA webbox servers defined.');
324
+ process.exit(1);
325
+}
326
+
327
+var started = new Date().getTime();
328
+function runServers() {
329
+ var now = new Date().getTime();
330
+ if(now - started > EXIT_AFTER_MS) {
331
+ debug('We have to exit now. Netdata will restart us.');
332
+ // FIXME
333
+ // We should wait for any currently running requests to complete
334
+ process.exit(0);
335
+ }
336
+
337
+ var len = Servers.length;
338
+ while(len--) {
339
+ getSMAData(Servers[len], function(server) {
340
+ if(server.success)
341
+ debug(' OK ' + server.name + ' time since last run: ' + server.dt + ' ms');
342
+ else
343
+ error(getSMAError(server) + server.name);
344
+ });
345
+ }
346
+
347
+ setTimeout(runServers, 100);
348
+}
349
+
350
+runServers();