@cryptotaxi247 / netdata-1 / commits / aae85f584

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();