2 javascript display of raw SMA webbox data
3 Copyright Andrew Tridgell 2010
4 Released under GNU GPL v3 or later
9 return the variables set after a '#' as a hash
11 function parse_hashvariables() {
13 var url = window.location.hash.slice(1);
14 var vars = url.split(';');
15 for (var i=0; i<vars.length; i++) {
16 var x = vars[i].split('=');
24 hashvars = parse_hashvariables();
27 rewrite the URL hash so you can bookmark particular dates
29 function rewrite_hashvars(vars) {
32 hash += '' + x + '=' + vars[x] + ';';
34 hash = hash.slice(0,hash.length-1);
35 window.location.hash = hash;
39 round a date back to midnight
41 function date_round(d) {
46 d2.setMilliseconds(0);
53 function canberraDate() {
55 return date_round(new Date(d.getTime() + (tz_difference*60*60*1000)));
62 tz_difference = 11 + (pvdate.getTimezoneOffset()/60);
66 /* marker for whether we are in a redraw with new data */
74 document.write("<h3><a STYLE='text-decoration:none' href=\"javascript:toggle_div('"+h+"')\"><img src='icons/icon_unhide_16.png' border='0' id='img-"+h+"'></a> "+h+"</h3>");
79 create a div for a graph
81 function graph_div(divname) {
85 '<td valign="top"><div id="' + divname + '" style="width:700px; height:350px;"></div></td>' +
86 '<td valign="top"> </td>' +
87 '<td valign="top"><div id="' + divname + ':labels"></div></td>' +
96 function hide_div(divname, hidden) {
97 var div = document.getElementById(divname);
99 div.style.display = "none";
101 div.style.display = "block";
105 /* unhide the loading div when busy */
108 function loading(busy) {
111 if (loading_counter == 1) {
112 started_loading=new Date();
113 hide_div("loading", false);
116 if (loading_counter > 0) {
118 if (loading_counter == 0) {
119 hide_div("loading", true);
121 var load_time = d.getTime() - started_loading.getTime();
122 writeDebug("Loading took: " + (load_time/1000));
129 /* a global call queue */
130 global_queue = new Array();
133 /* IE is _very_ slow at digraphs, we need bigger pauses to stop
143 function run_queue() {
144 var qe = global_queue[0];
146 global_queue.shift();
147 if (global_queue.length > 0) {
148 setTimeout(run_queue, job_delay);
153 queue a call. This is used to serialise long async operations in the
154 browser, so that you get less timeouts. It is especially needed on
155 IE, where the canvas widget is terribly slow.
157 function queue_call(callback, arg) {
158 global_queue.push( { callback: callback, arg : arg });
159 if (global_queue.length == 1) {
160 setTimeout(run_queue, job_delay);
166 date parser. Not completely general, but good enough
168 function parse_date(s) {
169 if (s.length == 5 && s[2] == ':') {
170 /* its a time since midnight */
171 var h = (+s.substring(0, 2));
172 var m = (+s.substring(3));
173 var d = new Date(pvdate);
178 if (s.search("-") != -1) {
179 s = s.replace("-", "/", "g");
182 var x = s.split('/');
187 return date_round(d);
189 if (s.search("/") != -1) {
192 /* assume time in milliseconds since 1970 */
198 return a YYYY-MM-DD date string
200 function date_YMD(d) {
201 return '' + d.getFullYear() + '-' + (d.getMonth()+1) + '-' + d.getDate();
207 function parse_value(s) {
208 if (s.substring(0,1) == '"') {
209 s = unescape(s.substring(1,s.length-1));
212 var n = new Number(s);
219 /* keep a cache of loaded CSV files */
220 CSV_Cache = new Array();
224 load a CSV file, returing column names and data via a callback
226 function load_CSV(filename, callback) {
228 /* maybe its in the global cache? */
229 if (CSV_Cache[filename] !== undefined) {
231 if (CSV_Cache[filename].pending) {
232 /* its pending load by someone else. Add ourselves to the notify
233 queue so we are told when it is done */
234 CSV_Cache[filename].queue.push({filename:filename, callback:callback});
238 /* its ready in the cache - return it via a delayed callback */
239 if (CSV_Cache[filename].data == null) {
240 var d = { filename: CSV_Cache[filename].filename,
243 hide_div("nodata", false);
244 queue_call(callback, d);
246 var d = { filename: CSV_Cache[filename].filename,
247 labels: CSV_Cache[filename].labels.slice(0),
248 data: CSV_Cache[filename].data.slice(0) };
249 hide_div("nodata", true);
250 queue_call(callback, d);
255 /* mark this one pending */
256 CSV_Cache[filename] = { filename:filename, pending: true, queue: new Array()};
259 async callback when the CSV is loaded
261 function load_CSV_callback(caller) {
263 var csv = caller.r.responseText.split(/\n/g);
265 /* assume first line is column labels */
266 var labels = csv[0].split(/,/g);
267 for (var i=0; i<labels.length; i++) {
268 labels[i] = labels[i].replace(" ", " ", "g");
271 /* the rest is data, we assume comma separation */
272 var data = new Array();
273 for (var i=1; i<csv.length; i++) {
274 var row = csv[i].split(/,/g);
275 if (row.length <= 1) {
278 data[i-1] = new Array();
279 data[i-1][0] = parse_date(row[0]);
280 for (var j=1; j<row.length; j++) {
281 data[i-1][j] = parse_value(row[j]);
285 /* save into the global cache */
286 CSV_Cache[caller.filename].labels = labels;
287 CSV_Cache[caller.filename].data = data;
289 /* give the caller a copy of the data (via slice()), as they may
291 var d = { filename: CSV_Cache[filename].filename,
292 labels: CSV_Cache[filename].labels.slice(0),
293 data: CSV_Cache[filename].data.slice(0) };
294 queue_call(caller.callback, d);
296 /* fire off any pending callbacks */
297 while (CSV_Cache[caller.filename].queue.length > 0) {
298 var qe = CSV_Cache[caller.filename].queue.shift();
299 var d = { filename: filename,
300 labels: CSV_Cache[filename].labels.slice(0),
301 data: CSV_Cache[filename].data.slice(0) };
302 queue_call(qe.callback, d);
304 CSV_Cache[caller.filename].pending = false;
305 CSV_Cache[caller.filename].queue = null;
308 /* make the async request for the file */
309 var caller = new Object();
310 caller.r = new XMLHttpRequest();
311 caller.callback = callback;
312 caller.filename = filename;
314 /* check the status when that returns */
315 caller.r.onreadystatechange = function() {
316 if (caller.r.readyState == 4) {
317 if (caller.r.status == 200) {
318 load_CSV_callback(caller);
319 hide_div("nodata", true);
321 /* the load failed */
322 hide_div("nodata", false);
323 queue_call(caller.callback, { filename: filename, data: null, labels: null });
324 while (CSV_Cache[caller.filename].queue.length > 0) {
325 var qe = CSV_Cache[caller.filename].queue.shift();
326 var d = { filename: CSV_Cache[filename].filename,
329 queue_call(qe.callback, d);
331 CSV_Cache[caller.filename].pending = false;
332 CSV_Cache[caller.filename].queue = null;
333 CSV_Cache[caller.filename].data = null;
334 CSV_Cache[caller.filename].labels = null;
338 caller.r.open("GET",filename,true);
343 format an integer with N digits by adding leading zeros
344 javascript is so lame ...
346 function intLength(v, N) {
348 while (r.length < N) {
356 return the list of CSV files for the inverters for date pvdate
358 function days_csv_files() {
359 var list = new Array();
360 for (var i=0; i<serialnums.length; i++) {
361 list[i] = CSV_directory + date_YMD(pvdate) + "-WR5KA-08:" +
362 serialnums[i] + ".csv";
369 return the position of v in an array or -1
371 function pos_in_array(a, v) {
372 for (var i=0; i<a.length; i++) {
381 see if v exists in array a
383 function in_array(a, v) {
384 return pos_in_array(a, v) != -1;
389 return a set of columns from a CSV file
391 function get_csv_data(filenames, columns, callback) {
392 var caller = new Object();
393 caller.d = new Array();
394 caller.columns = columns.slice(0);
395 caller.filenames = filenames.slice(0);
396 caller.callback = callback;
398 /* initially blank data - we can tell a load has completed when it
400 for (var i=0; i<caller.filenames.length; i++) {
401 caller.d[i] = { filename: caller.filenames[i], labels: null, data: null};
404 /* process one loaded CSV, mapping the data for
405 the requested columns */
406 function process_one_csv(d) {
407 var labels = new Array();
409 if (d.data == null) {
410 queue_call(caller.callback, d);
414 /* form the labels */
416 for (var i=0; i<caller.columns.length; i++) {
417 labels[i+1] = caller.columns[i];
420 /* get the column numbers */
421 var cnums = new Array();
423 for (var i=0; i<caller.columns.length; i++) {
424 cnums[i+1] = pos_in_array(d.labels, caller.columns[i]);
428 var data = new Array();
429 for (var i=0; i<d.data.length; i++) {
430 data[i] = new Array();
431 for (var j=0; j<cnums.length; j++) {
432 data[i][j] = d.data[i][cnums[j]];
438 for (var f=0; f<caller.filenames.length; f++) {
439 if (d.filename == caller.d[f].filename) {
440 caller.d[f].labels = labels;
441 caller.d[f].data = data;
445 /* see if all the files are now loaded */
446 for (var f=0; f<caller.filenames.length; f++) {
447 if (caller.d[f].data == null) {
452 /* they are all loaded - make the callback */
453 queue_call(caller.callback, caller.d);
456 /* start the loading */
457 for (var i=0; i<caller.filenames.length; i++) {
458 load_CSV(caller.filenames[i], process_one_csv);
464 apply a function to a set of data, giving it a new label
466 function apply_function(d, func, label) {
470 for (var i=0; i<d.data.length; i++) {
472 d.data[i] = r.slice(0,1);
473 d.data[i][1] = func(r.slice(1))
475 d.labels = d.labels.slice(0,1);
480 /* currently displayed graphs, indexed by divname */
481 global_graphs = new Array();
484 find a graph by divname
486 function graph_find(divname) {
487 for (var i=0; i<global_graphs.length; i++) {
488 var g = global_graphs[i];
489 if (g.divname == divname) {
496 function nameAnnotation(ann) {
497 return "(" + ann.series + ", " + ann.xval + ")";
503 try to save an annotation via annotation.cgi
505 function save_annotation(ann) {
506 var r = new XMLHttpRequest();
508 "cgi/annotation.cgi?series="+escape(ann.series)+"&xval="+ann.xval+"&text="+escape(ann.text), true);
513 load annotations from annotations.csv
515 function load_annotations(g) {
516 function callback(d) {
517 var anns_by_name = new Array();
519 for (var i=0; i<d.data.length; i++) {
520 var xval = d.data[i][0] + (tz_difference*60*60*1000);
521 if (xval < pvdate.valueOf() || xval >= (pvdate.valueOf() + (24*60*60*1000))) {
526 series: d.data[i][1],
528 text: decodeURIComponent(d.data[i][2])
530 var a = anns_by_name[nameAnnotation(ann)];
531 if (a == undefined) {
532 anns_by_name[nameAnnotation(ann)] = annotations.length;
533 annotations.push(ann);
535 annotations[a] = ann;
538 for (var i=0; i<global_graphs.length; i++) {
539 var g = global_graphs[i];
540 g.setAnnotations(annotations);
544 load_CSV("../CSV/annotations.csv", callback);
547 function annotation_highlight(ann, point, dg, event) {
548 saveBg = ann.div.style.backgroundColor;
549 ann.div.style.backgroundColor = '#ddd';
552 function annotation_unhighlight(ann, point, dg, event) {
553 ann.div.style.backgroundColor = saveBg;
557 handle annotation updates
559 function annotation_click(ann, point, dg, event) {
560 ann.text = prompt("Enter annotation", ann.text);
561 for (var i=0; i<annotations.length; i++) {
562 if (annotations[i].xval == ann.xval && annotations[i].series == ann.series) {
563 annotations[i].text = ann.text;
564 if (ann.text == '') {
565 annotations.splice(i,1);
570 for (var i=0; i<global_graphs.length; i++) {
571 var g = global_graphs[i];
572 if (g.series_names.indexOf(ann.series) != -1) {
573 g.setAnnotations(annotations);
576 save_annotation(ann);
580 add a new annotation to one graph
582 function annotation_add_graph(g, p, ann) {
583 var anns = g.annotations();
586 if (ann.text == '') {
587 var idx = anns.indexOf(p);
592 p.annotation.text = ann.text;
597 g.setAnnotations(anns);
603 function annotation_add(event, p) {
608 text: prompt("Enter annotation", ""),
610 for (var i=0; i<global_graphs.length; i++) {
611 var g = global_graphs[i];
612 if (g.series_names.indexOf(p.name) != -1) {
613 annotation_add_graph(g, p, ann);
617 save_annotation(ann);
621 /* default dygraph attributes */
628 annotationMouseOverHandler: annotation_highlight,
629 annotationMouseOutHandler: annotation_unhighlight,
630 annotationClickHandler: annotation_click,
631 pointClickCallback: annotation_add
636 graph results from a set of CSV files:
637 - apply func1 to the name columns within each file
638 - apply func2 between the files
640 function graph_csv_files_func(divname, filenames, columns, func1, func2, attrs) {
641 /* load the csv files */
642 var caller = new Object();
643 caller.divname = divname;
644 caller.filenames = filenames.slice(0);
645 caller.columns = columns.slice(0);
646 caller.func1 = func1;
647 caller.func2 = func2;
648 caller.attrs = attrs;
650 if (attrs.series_base != undefined) {
651 caller.colname = attrs.series_base;
652 } else if (columns.length == 1) {
653 caller.colname = columns[0]
655 caller.colname = divname;
658 /* called when all the data is loaded and we're ready to apply the
659 functions and graph */
660 function loaded_callback(d) {
662 if (d[0] == undefined) {
667 for (var i=0; i<caller.filenames.length; i++) {
668 apply_function(d[i], caller.func1, caller.colname);
671 /* work out the y offsets to align the times */
672 var yoffsets = new Array();
674 for (var i=1; i<caller.filenames.length; i++) {
676 if (d[i].data[0][0] < d[0].data[0][0]) {
677 while (d[i].data[yoffsets[i]][0] < d[0].data[0][0]) {
680 } else if (d[i].data[0][0] > d[0].data[0][0]) {
681 while (d[i].data[0][0] > d[0].data[-yoffsets[i]][0]) {
687 if (caller.attrs.missingValue !== undefined) {
688 missingValue = attrs.missingValue;
694 var data = d[0].data;
695 for (var j=0; j<data.length; j++) {
696 if (data[j][1] == missingValue) {
699 for (var i=1; i<caller.filenames.length; i++) {
700 var y = j + yoffsets[i];
701 if (y < 0 || y >= d[i].data.length || d[i].data[y][1] == missingValue) {
704 data[j][i+1] = d[i].data[y][1];
709 labels = new Array();
710 labels[0] = d[0].labels[0];
711 for (var i=0; i<caller.filenames.length; i++) {
712 labels[i+1] = caller.colname + (i+1);
715 var d2 = { labels: labels, data: data };
716 apply_function(d2, caller.func2, caller.colname);
718 /* add the labels to the given graph attributes */
719 caller.attrs.labels = d2.labels;
721 for (a in defaultAttrs) {
722 if (caller.attrs[a] == undefined) {
723 caller.attrs[a] = defaultAttrs[a];
727 caller.attrs['labelsDiv'] = divname + ":labels";
729 /* we need to create a new one, as otherwise we can't remove
731 for (var i=0; i<global_graphs.length; i++) {
732 var g = global_graphs[i];
733 if (g.divname == divname) {
734 global_graphs.splice(i, 1);
740 /* create a new dygraph */
741 g = new Dygraph(document.getElementById(divname), d2.data, caller.attrs);
742 g.series_names = caller.attrs.labels;
744 g.setAnnotations(annotations);
745 global_graphs.push(g);
750 /* fire off a request to load the data */
754 get_csv_data(caller.filenames, caller.columns, loaded_callback);
758 function product(v) {
760 for (var i=1; i<v.length; i++) {
768 for (var i=1; i<v.length; i++) {
777 graph one column from a set of CSV files
779 function graph_csv_files(divname, filenames, column, attrs) {
780 return graph_csv_files_func(divname, filenames, [column], null, null, attrs);
784 graph one column from a set of CSV files as a sum over multiple files
786 function graph_sum_csv_files(divname, filenames, column, attrs) {
787 return graph_csv_files_func(divname, filenames, [column], null, sum, attrs);
791 show all the live data graphs
793 function show_graphs() {
794 graph_sum_csv_files("Total AC Power (W)",
797 { includeZero: true });
800 graph_csv_files("AC Power from each inverter (W) [Pac]",
803 { includeZero: true });
805 graph_csv_files("DC Voltage for each inverter (V) [UpvIst]",
808 { includeZero: false,
809 missingValue: 666 });
811 graph_csv_files("Target DC Voltage for each inverter (V) [UpvSoll]",
814 { includeZero: false,
815 missingValue: 666 });
818 graph_sum_csv_files("Total DC current (A)",
821 { includeZero: true });
823 graph_csv_files("DC Current for each inverter (A) [Ipv]",
826 { includeZero: false });
828 graph_csv_files_func("DC Power for each inverter (W) [Ipv*UpvIst]",
830 [ "Ipv", "Upv-Ist" ],
833 series_base: 'Pdc' });
835 graph_csv_files_func("Total DC Power (W)",
837 [ "Ipv", "Upv-Ist" ],
839 { includeZero: true });
842 function efficiency(v) {
843 var dc_pow = v[1] * v[2];
847 return 100.0*(v[0] / dc_pow);
850 graph_csv_files_func("Inverter efficiencies (%) [(Ipv*UpvIst)/Pac]",
852 [ "Pac", "Ipv", "Upv-Ist" ],
854 { includeZero: false,
855 series_base: 'Eff'});
857 graph_csv_files("AC Voltage for each inverter (V) [Uac]",
860 { includeZero: false });
862 graph_csv_files("Lifetime Power for each inverter (kWh) [E-total]",
865 { includeZero: false });
867 graph_sum_csv_files("Total Lifetime Power (kWh)",
870 { includeZero: false });
872 graph_csv_files("Fan voltage for each inverter (V) [UFan]",
877 valueRange: [0, 12] });
885 called when the user selects a date
887 function set_date(e) {
888 var dp = datePickerController.getDatePicker("pvdate");
889 pvdate = date_round(dp.date);
890 hashvars['date'] = date_YMD(pvdate);
891 rewrite_hashvars(hashvars);
892 writeDebug("redrawing for: " + pvdate);
893 annotations = new Array();
898 setup the datepicker widget
900 function setup_datepicker() {
901 document.getElementById("pvdate").value =
902 intLength(pvdate.getDate(),2) + "/" + intLength(pvdate.getMonth()+1, 2) + "/" + pvdate.getFullYear();
903 datePickerController.addEvent(document.getElementById("pvdate"), "change", set_date);
908 called to reload every few minutes
910 function reload_timer() {
911 /* flush the old CSV cache */
912 CSV_Cache = new Array();
913 writeDebug("reloading on timer");
914 if (loading_counter == 0) {
917 setup_reload_timer();
921 setup for automatic reloads
923 function setup_reload_timer() {
924 setTimeout(reload_timer, 300000);
929 toggle display of a div
931 function toggle_div(divname)
933 div = document.getElementById(divname);
934 img = document.getElementById("img-" + divname);
935 current_display = div.style.display;
936 old_src = img.getAttribute("src");
937 if (current_display != "none") {
938 div.style.display = "none";
939 img.setAttribute("src", old_src.replace("_unhide", "_hide"));
941 div.style.display = "block";
942 img.setAttribute("src", old_src.replace("_hide", "_unhide"));