2 javascript display of raw SMA webbox data
3 Copyright Andrew Tridgell 2010
4 Released under GNU GPL v3 or later
9 setup midnight as a date
11 midnight = new Date();
13 midnight.setMinutes(0);
14 midnight.setSeconds(0);
15 midnight.setMilliseconds(0);
17 pvdate = new Date(midnight);
22 function heading(level, h) {
24 document.write("<h" + level + ">" + h + "</h" + level + ">\n");
29 create a div for a graph
31 function graph_div(divname) {
33 document.write('<div id="' + divname + '" style="width:700px; height:350px;"></div>');
40 function hide_div(divname, hidden) {
41 var div = document.getElementById(divname);
43 writeDebug("hiding " + divname);
44 div.style.display = "none";
46 writeDebug("unhiding " + divname);
47 div.style.display = "block";
51 /* unhide the loading div when busy */
54 function loading(busy) {
57 if (loading_counter == 1) {
58 hide_div("loading", false);
62 if (loading_counter == 0) {
63 hide_div("loading", true);
69 /* a global call queue */
70 global_queue = new Array();
73 /* IE is _very_ slow at digraphs, we need bigger pauses to stop
83 function run_queue() {
84 var qe = global_queue[0];
87 if (global_queue.length > 0) {
88 setTimeout(run_queue, job_delay);
93 queue a call. This is used to serialise long async operations in the
94 browser, so that you get less timeouts. It is especially needed on
95 IE, where the canvas widget is terribly slow.
97 function queue_call(callback, arg) {
98 global_queue.push( { callback: callback, arg : arg });
99 if (global_queue.length == 1) {
100 setTimeout(run_queue, job_delay);
106 date parser. Not completely general, but good enough
108 function parse_date(s) {
109 if (s.length == 5 && s[2] == ':') {
110 /* its a time since midnight */
111 var h = (+s.substring(0, 2));
112 var m = (+s.substring(3));
113 var d = new Date(midnight);
118 if (s.search("-") != -1) {
119 s = s.replace("-", "/", "g");
124 /* keep a cache of loaded CSV files */
125 CSV_Cache = new Array();
129 load a CSV file, returing column names and data via a callback
131 function load_CSV(filename, callback) {
133 /* maybe its in the global cache? */
134 if (CSV_Cache[filename] !== undefined) {
136 if (CSV_Cache[filename].pending) {
137 /* its pending load by someone else. Add ourselves to the notify
138 queue so we are told when it is done */
139 CSV_Cache[filename].queue.push({filename:filename, callback:callback});
143 /* its ready in the cache - return it via a delayed callback */
144 var d = { filename: CSV_Cache[filename].filename,
145 labels: CSV_Cache[filename].labels.slice(0),
146 data: CSV_Cache[filename].data.slice(0) };
147 queue_call(callback, d);
151 /* mark this one pending */
152 CSV_Cache[filename] = { filename:filename, pending: true, queue: new Array()};
155 async callback when the CSV is loaded
157 function load_CSV_callback(caller) {
159 var csv = caller.r.responseText.split(/\n/g);
161 /* assume first line is column labels */
162 var labels = csv[0].split(/,/g);
163 for (var i=0; i<labels.length; i++) {
164 labels[i] = labels[i].replace(" ", " ", "g");
167 /* the rest is data, we assume comma separation */
168 var data = new Array();
169 for (var i=1; i<csv.length; i++) {
170 var row = csv[i].split(/,/g);
171 if (row.length <= 1) {
174 data[i-1] = new Array();
175 data[i-1][0] = parse_date(row[0]);
176 for (var j=1; j<row.length; j++) {
177 data[i-1][j] = parseFloat(row[j]);
181 /* save into the global cache */
182 CSV_Cache[caller.filename].labels = labels;
183 CSV_Cache[caller.filename].data = data;
185 /* give the caller a copy of the data (via slice()), as they may
187 var d = { filename: CSV_Cache[filename].filename,
188 labels: CSV_Cache[filename].labels.slice(0),
189 data: CSV_Cache[filename].data.slice(0) };
190 queue_call(caller.callback, d);
192 /* fire off any pending callbacks */
193 while (CSV_Cache[caller.filename].queue.length > 0) {
194 var qe = CSV_Cache[caller.filename].queue.shift();
195 var d = { filename: filename,
196 labels: CSV_Cache[filename].labels.slice(0),
197 data: CSV_Cache[filename].data.slice(0) };
198 queue_call(qe.callback, d);
200 CSV_Cache[caller.filename].pending = false;
201 CSV_Cache[caller.filename].queue = null;
204 /* make the async request for the file */
205 var caller = new Object();
206 caller.r = new XMLHttpRequest();
207 caller.callback = callback;
208 caller.filename = filename;
210 /* check the status when that returns */
211 caller.r.onreadystatechange = function() {
212 if (caller.r.readyState == 4) {
213 if (caller.r.status == 200) {
214 load_CSV_callback(caller);
216 /* the load failed */
217 queue_call(caller.callback, { filename: filename, data: null, labels: null });
218 while (CSV_Cache[caller.filename].queue.length > 0) {
219 var qe = CSV_Cache[caller.filename].queue.shift();
220 var d = { filename: CSV_Cache[filename].filename,
223 queue_call(qe.callback, d);
225 CSV_Cache[caller.filename].pending = false;
226 CSV_Cache[caller.filename].queue = null;
230 caller.r.open("GET",filename,true);
235 format an integer with N digits by adding leading zeros
236 javascript is so lame ...
238 function intLength(v, N) {
240 while (r.length < N) {
248 return the list of CSV files for the inverters for date pvdate
250 function days_csv_files() {
251 var list = new Array();
253 for (var i=0; i<serialnums.length; i++) {
254 list[i] = CSV_directory +
255 pvdate.getFullYear() + "-" +
256 intLength(pvdate.getMonth()+1,2) + "-" +
257 intLength(pvdate.getDate(),2) + "-WR5KA-08:" +
258 serialnums[i] + ".csv";
265 return the position of v in an array or -1
267 function pos_in_array(a, v) {
268 for (var i=0; i<a.length; i++) {
277 see if v exists in array a
279 function in_array(a, v) {
280 return pos_in_array(a, v) != -1;
285 return a set of columns from a CSV file
287 function get_csv_data(filenames, columns, callback) {
288 var caller = new Object();
289 caller.d = new Array();
290 caller.columns = columns.slice(0);
291 caller.filenames = filenames.slice(0);
292 caller.callback = callback;
294 /* initially blank data - we can tell a load has completed when it
296 for (var i=0; i<caller.filenames.length; i++) {
297 caller.d[i] = { filename: caller.filenames[i], labels: null, data: null};
300 /* process one loaded CSV, mapping the data for
301 the requested columns */
302 function process_one_csv(d) {
303 var labels = new Array();
305 if (d.data == null) {
306 queue_call(caller.callback, d);
310 /* form the labels */
312 for (var i=0; i<caller.columns.length; i++) {
313 labels[i+1] = caller.columns[i];
316 /* get the column numbers */
317 var cnums = new Array();
319 for (var i=0; i<caller.columns.length; i++) {
320 cnums[i+1] = pos_in_array(d.labels, caller.columns[i]);
324 var data = new Array();
325 for (var i=0; i<d.data.length; i++) {
326 data[i] = new Array();
327 for (var j=0; j<cnums.length; j++) {
328 data[i][j] = d.data[i][cnums[j]];
334 for (var f=0; f<caller.filenames.length; f++) {
335 if (d.filename == caller.d[f].filename) {
336 caller.d[f].labels = labels;
337 caller.d[f].data = data;
341 /* see if all the files are now loaded */
342 for (var f=0; f<caller.filenames.length; f++) {
343 if (caller.d[f].data == null) {
348 /* they are all loaded - make the callback */
349 queue_call(caller.callback, caller.d);
352 /* start the loading */
353 for (var i=0; i<caller.filenames.length; i++) {
354 load_CSV(caller.filenames[i], process_one_csv);
360 apply a function to a set of data, giving it a new label
362 function apply_function(d, func, label) {
366 for (var i=0; i<d.data.length; i++) {
368 d.data[i] = r.slice(0,1);
369 d.data[i][1] = func(r.slice(1))
371 d.labels = d.labels.slice(0,1);
376 /* currently displayed graphs, indexed by divname */
377 global_graphs = new Array();
379 /* default dygraph attributes */
389 graph results from a set of CSV files:
390 - apply func1 to the name columns within each file
391 - apply func2 between the files
393 function graph_csv_files_func(divname, filenames, columns, func1, func2, attrs) {
394 /* load the csv files */
395 var caller = new Object();
396 caller.divname = divname;
397 caller.filenames = filenames.slice(0);
398 caller.columns = columns.slice(0);
399 caller.func1 = func1;
400 caller.func2 = func2;
401 caller.attrs = attrs;
403 if (columns.length == 1) {
404 caller.colname = columns[0]
406 caller.colname = divname;
409 /* called when all the data is loaded and we're ready to apply the
410 functions and graph */
411 function loaded_callback(d) {
413 if (d[0] == undefined) {
414 writeDebug("unable to load file");
415 /* hide_div("nodata", false); */
420 for (var i=0; i<caller.filenames.length; i++) {
421 apply_function(d[i], caller.func1, caller.colname);
424 /* work out the y offsets to align the times */
425 var yoffsets = new Array();
427 for (var i=1; i<caller.filenames.length; i++) {
429 if (d[i].data[0][0] < d[0].data[0][0]) {
430 while (d[i].data[yoffsets[i]][0] < d[0].data[0][0]) {
433 } else if (d[i].data[0][0] > d[0].data[0][0]) {
434 while (d[i].data[0][0] > d[0].data[-yoffsets[i]][0]) {
440 if (caller.attrs.missingValue !== undefined) {
441 missingValue = attrs.missingValue;
447 var data = d[0].data;
448 for (var j=0; j<data.length; j++) {
449 if (data[j][1] == missingValue) {
452 for (var i=1; i<caller.filenames.length; i++) {
453 var y = j + yoffsets[i];
454 if (y < 0 || y >= d[i].data.length || d[i].data[y][1] == missingValue) {
457 data[j][i+1] = d[i].data[y][1];
462 labels = new Array();
463 labels[0] = d[0].labels[0];
464 for (var i=0; i<caller.filenames.length; i++) {
465 labels[i+1] = caller.colname + (i+1);
468 var d2 = { labels: labels, data: data };
469 apply_function(d2, caller.func2, caller.colname);
471 /* add the labels to the given graph attributes */
472 caller.attrs.labels = d2.labels;
474 for (a in defaultAttrs) {
475 if (caller.attrs[a] == undefined) {
476 caller.attrs[a] = defaultAttrs[a];
480 if (global_graphs[divname] !== undefined) {
481 /* we already have the graph, just update the data */
482 var g = global_graphs[divname];
483 for (var i=0; i<d2.data.length; i++) {
484 /* the rawData_ attribute doesn't take dates */
485 d2.data[i][0] = d2.data[i][0].valueOf();
487 g.rawData_ = d2.data;
488 g.drawGraph_(g.rawData_);
490 /* create a new dygraph */
491 global_graphs[divname] = new Dygraph(document.getElementById(divname), d2.data, caller.attrs);
497 /* fire off a request to load the data */
500 get_csv_data(caller.filenames, caller.columns, loaded_callback);
504 function product(v) {
506 for (var i=1; i<v.length; i++) {
514 for (var i=1; i<v.length; i++) {
523 graph one column from a set of CSV files
525 function graph_csv_files(divname, filenames, column, attrs) {
526 return graph_csv_files_func(divname, filenames, [column], null, null, attrs);
530 graph one column from a set of CSV files as a sum over multiple files
532 function graph_sum_csv_files(divname, filenames, column, attrs) {
533 return graph_csv_files_func(divname, filenames, [column], null, sum, attrs);
536 /* marker for whether we are in a redraw with new data */
540 show all the live data graphs
542 function show_graphs() {
544 hide_div("nodata", true);
546 heading(3, "Total AC Power (W)");
548 graph_sum_csv_files("Total AC Power",
551 { includeZero: true });
554 heading(3, "AC Power from each inverter (W) [Pac]");
556 graph_csv_files("AC Power",
559 { includeZero: true });
561 heading(3, "DC Voltage for each inverter (V) [UpvSoll]");
563 graph_csv_files("DC Voltage",
566 { includeZero: false,
567 missingValue: 666 });
569 heading(3, "Target DC Voltage for each inverter (V) [UpvIst]");
571 graph_csv_files("Target DC Voltage",
574 { includeZero: false,
575 missingValue: 666 });
577 heading(3, "Total DC current (A)");
579 graph_sum_csv_files("Total Current",
582 { includeZero: true });
584 heading(3, "DC Current for each inverter (A) [Ipv]");
586 graph_csv_files("DC Current [Ipv]",
589 { includeZero: false });
591 heading(3, "DC Power for each inverter (W) [Ipv*UpvSoll]");
594 graph_csv_files_func("DC Power",
596 [ "Ipv", "Upv-Soll" ],
598 { includeZero: true });
600 heading(3, "Total DC Power (W)");
602 graph_csv_files_func("Total DC Power",
604 [ "Ipv", "Upv-Soll" ],
606 { includeZero: true });
609 heading(3, "Inverter efficiencies (%) [(Ipv*UpvSoll)/Pac]");
611 function efficiency(v) {
612 var dc_pow = v[1] * v[2];
616 return 100.0*(v[0] / dc_pow);
619 graph_csv_files_func("Inverter Efficiency",
621 [ "Pac", "Ipv", "Upv-Soll" ],
623 { includeZero: false,
624 valueRange: [0, 100]});
626 heading(3, "AC Voltage for each inverter (V) [Uac]");
628 graph_csv_files("AC Voltage",
631 { includeZero: false });
633 heading(3, "Lifetime Power for each inverter (kWh) [E-total]");
635 graph_csv_files("Lifetime Power",
638 { includeZero: false });
640 heading(3, "Total Lifetime Power (kWh)");
642 graph_sum_csv_files("Total Lifetime Power",
645 { includeZero: false });
647 heading(3, "Fan voltage for each inverter (V) [UFan]");
649 graph_csv_files("Fan Voltage",
654 valueRange: [0, 12] });
660 called when the user selects a date
662 function set_date(e) {
663 var dp = datePickerController.getDatePicker("pvdate");
665 writeDebug("redrawing for: " + pvdate);
670 setup the datepicker widget
672 function setup_datepicker() {
673 document.getElementById("pvdate").value =
674 intLength(pvdate.getDate(),2) + "/" + intLength(pvdate.getMonth()+1, 2) + "/" + pvdate.getFullYear();
675 datePickerController.addEvent(document.getElementById("pvdate"), "change", set_date);
680 called to reload every few minutes
682 function reload_timer() {
683 /* flush the old CSV cache */
684 CSV_Cache = new Array();
685 writeDebug("reloading on timer");
686 if (loading_counter == 0) {
689 setup_reload_timer();
693 setup for automatic reloads
695 function setup_reload_timer() {
696 setTimeout(reload_timer, 300000);