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)));
61 pvdate = date_round(new Date());
64 tz_difference = 11 + (pvdate.getTimezoneOffset()/60);
68 /* marker for whether we are in a redraw with new data */
76 document.write("<h3><a STYLE='text-decoration:none' href=\"javascript:toggle_div('"+h+"')\"><img src='icons/icon_unhide_16.png' width='16' height='16' border='0' id='img-"+h+"'></a> "+h+"</h3>");
81 create a div for a graph
83 function graph_div(divname) {
87 '<td valign="top"><div id="' + divname + '" style="width:700px; height:350px;"></div></td>' +
88 '<td valign="top"> </td>' +
89 '<td valign="top"><div id="' + divname + ':labels"></div></td>' +
98 function hide_div(divname, hidden) {
99 var div = document.getElementById(divname);
101 div.style.display = "none";
103 div.style.display = "block";
107 /* unhide the loading div when busy */
110 function loading(busy) {
113 if (loading_counter == 1) {
114 started_loading=new Date();
115 hide_div("loading", false);
118 if (loading_counter > 0) {
120 if (loading_counter == 0) {
121 hide_div("loading", true);
123 var load_time = d.getTime() - started_loading.getTime();
124 writeDebug("Loading took: " + (load_time/1000));
131 /* a global call queue */
132 global_queue = new Array();
135 /* IE is _very_ slow at digraphs, we need bigger pauses to stop
145 function run_queue() {
146 var qe = global_queue[0];
148 global_queue.shift();
149 if (global_queue.length > 0) {
150 setTimeout(run_queue, job_delay);
155 queue a call. This is used to serialise long async operations in the
156 browser, so that you get less timeouts. It is especially needed on
157 IE, where the canvas widget is terribly slow.
159 function queue_call(callback, arg) {
160 global_queue.push( { callback: callback, arg : arg });
161 if (global_queue.length == 1) {
162 setTimeout(run_queue, job_delay);
168 date parser. Not completely general, but good enough
170 function parse_date(s, basedate) {
171 if (s.length == 5 && s[2] == ':') {
172 /* its a time since midnight */
173 var h = (+s.substring(0, 2));
174 var m = (+s.substring(3));
175 var d = basedate.getTime() + 1000*(h*60*60 + m*60);
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) {
190 return date_round(new Date(s));
192 /* assume time in milliseconds since 1970 */
198 return a YYYY-MM-DD date string
200 function date_YMD(d) {
201 return '' + intLength(d.getFullYear(),4) + '-' + intLength(d.getMonth()+1,2) + '-' + intLength(d.getDate(),2);
205 parse the date portion of a filename which starts with YYYY-MM-DD after a directory
207 function filename_date(filename) {
208 var idx = filename.lastIndexOf("/");
210 filename = filename.substring(idx+1);
212 if (filename[4] == '-' && filename[7] == '-') {
213 /* looks like a date */
214 var d = date_round(new Date());
215 d.setYear(+filename.substring(0,4));
216 d.setMonth(filename.substring(5,7)-1);
217 d.setDate(filename.substring(8,10));
227 function parse_value(s) {
228 if (s.substring(0,1) == '"') {
229 s = unescape(s.substring(1,s.length-1));
232 var n = new Number(s);
239 /* keep a cache of loaded CSV files */
240 CSV_Cache = new Array();
244 load a CSV file, returing column names and data via a callback
246 function load_CSV(filename, callback) {
248 /* maybe its in the global cache? */
249 if (CSV_Cache[filename] !== undefined) {
251 if (CSV_Cache[filename].pending) {
252 /* its pending load by someone else. Add ourselves to the notify
253 queue so we are told when it is done */
254 CSV_Cache[filename].queue.push({filename:filename, callback:callback});
258 /* its ready in the cache - return it via a delayed callback */
259 if (CSV_Cache[filename].data == null) {
260 var d = { filename: CSV_Cache[filename].filename,
263 queue_call(callback, d);
265 var d = { filename: CSV_Cache[filename].filename,
266 labels: CSV_Cache[filename].labels.slice(0),
267 data: CSV_Cache[filename].data.slice(0) };
268 queue_call(callback, d);
273 /* mark this one pending */
274 CSV_Cache[filename] = { filename:filename, pending: true, queue: new Array()};
277 async callback when the CSV is loaded
279 function load_CSV_callback(caller) {
281 var csv = caller.r.responseText.split(/\n/g);
283 /* assume first line is column labels */
284 var labels = csv[0].split(/,/g);
285 for (var i=0; i<labels.length; i++) {
286 labels[i] = labels[i].replace(" ", " ", "g");
289 /* the rest is data, we assume comma separation */
290 var data = new Array();
291 for (var i=1; i<csv.length; i++) {
292 var row = csv[i].split(/,/g);
293 if (row.length <= 1) {
296 data[i-1] = new Array();
297 data[i-1][0] = parse_date(row[0], caller.basedate);
298 for (var j=1; j<row.length; j++) {
299 data[i-1][j] = parse_value(row[j]);
303 /* save into the global cache */
304 CSV_Cache[caller.filename].labels = labels;
305 CSV_Cache[caller.filename].data = data;
307 /* give the caller a copy of the data (via slice()), as they may
309 var d = { filename: CSV_Cache[filename].filename,
310 labels: CSV_Cache[filename].labels.slice(0),
311 data: CSV_Cache[filename].data.slice(0) };
312 queue_call(caller.callback, d);
314 /* fire off any pending callbacks */
315 while (CSV_Cache[caller.filename].queue.length > 0) {
316 var qe = CSV_Cache[caller.filename].queue.shift();
317 var d = { filename: filename,
318 labels: CSV_Cache[filename].labels.slice(0),
319 data: CSV_Cache[filename].data.slice(0) };
320 queue_call(qe.callback, d);
322 CSV_Cache[caller.filename].pending = false;
323 CSV_Cache[caller.filename].queue = null;
326 /* make the async request for the file */
327 var caller = new Object();
328 caller.r = new XMLHttpRequest();
329 caller.callback = callback;
330 caller.filename = filename;
331 caller.basedate = filename_date(filename);
333 /* check the status when that returns */
334 caller.r.onreadystatechange = function() {
335 if (caller.r.readyState == 4) {
336 if (caller.r.status == 200) {
337 load_CSV_callback(caller);
339 /* the load failed */
340 queue_call(caller.callback, { filename: filename, data: null, labels: null });
341 while (CSV_Cache[caller.filename].queue.length > 0) {
342 var qe = CSV_Cache[caller.filename].queue.shift();
343 var d = { filename: CSV_Cache[filename].filename,
346 queue_call(qe.callback, d);
348 CSV_Cache[caller.filename].pending = false;
349 CSV_Cache[caller.filename].queue = null;
350 CSV_Cache[caller.filename].data = null;
351 CSV_Cache[caller.filename].labels = null;
355 caller.r.open("GET",filename,true);
361 load a comma separated list of CSV files, combining the data
363 function load_CSV_array(filenames, callback) {
364 var c = new Object();
365 c.filename = filenames;
366 c.files = filenames.split(',');
367 c.callback = callback;
368 c.data = new Array();
373 async callback when a CSV is loaded
375 function load_CSV_array_callback(d) {
377 var i = c.files.indexOf(d.filename);
379 if (d.labels != null) {
382 if (c.count == c.files.length) {
383 var ret = { filename: c.filename, data: c.data[0], labels: c.labels};
384 for (var i=1; i<c.files.length; i++) {
385 if (c.data[i] != null) {
386 if (ret.data == null) {
387 ret.data = c.data[i];
389 ret.data = ret.data.concat(c.data[i]);
393 if (ret.data == null) {
394 hide_div("nodata", false);
396 hide_div("nodata", true);
402 for (var i=0; i<c.files.length; i++) {
403 load_CSV(c.files[i], load_CSV_array_callback);
408 format an integer with N digits by adding leading zeros
409 javascript is so lame ...
411 function intLength(v, N) {
413 while (r.length < N) {
421 return the list of CSV files for the inverters for date pvdate
423 function csv_files() {
424 var list = new Array();
425 var oneday = 24*60*60*1000;
426 var start_date = pvdate.getTime() - (period_days-1)*oneday;
427 for (var d=0; d<period_days; d++) {
428 var day = new Date(start_date + (d*oneday));
429 for (var i=0; i<serialnums.length; i++) {
430 var f = CSV_directory + date_YMD(day) + "-WR5KA-08:" +
431 serialnums[i] + ".csv";
444 return the position of v in an array or -1
446 function pos_in_array(a, v) {
447 for (var i=0; i<a.length; i++) {
456 see if v exists in array a
458 function in_array(a, v) {
459 return pos_in_array(a, v) != -1;
464 return a set of columns from a CSV file
466 function get_csv_data(filenames, columns, callback) {
467 var caller = new Object();
468 caller.d = new Array();
469 caller.columns = columns.slice(0);
470 caller.filenames = filenames.slice(0);
471 caller.callback = callback;
473 /* initially blank data - we can tell a load has completed when it
475 for (var i=0; i<caller.filenames.length; i++) {
476 caller.d[i] = { filename: caller.filenames[i], labels: null, data: null};
479 /* process one loaded CSV, mapping the data for
480 the requested columns */
481 function process_one_csv(d) {
482 var labels = new Array();
484 if (d.data == null) {
485 queue_call(caller.callback, d);
489 /* form the labels */
491 for (var i=0; i<caller.columns.length; i++) {
492 labels[i+1] = caller.columns[i];
495 /* get the column numbers */
496 var cnums = new Array();
498 for (var i=0; i<caller.columns.length; i++) {
499 cnums[i+1] = pos_in_array(d.labels, caller.columns[i]);
503 var data = new Array();
504 for (var i=0; i<d.data.length; i++) {
505 data[i] = new Array();
506 for (var j=0; j<cnums.length; j++) {
507 data[i][j] = d.data[i][cnums[j]];
513 for (var f=0; f<caller.filenames.length; f++) {
514 if (d.filename == caller.d[f].filename) {
515 caller.d[f].labels = labels;
516 caller.d[f].data = data;
520 /* see if all the files are now loaded */
521 for (var f=0; f<caller.filenames.length; f++) {
522 if (caller.d[f].data == null) {
527 /* they are all loaded - make the callback */
528 queue_call(caller.callback, caller.d);
531 /* start the loading */
532 for (var i=0; i<caller.filenames.length; i++) {
533 load_CSV_array(caller.filenames[i], process_one_csv);
539 apply a function to a set of data, giving it a new label
541 function apply_function(d, func, label) {
545 for (var i=0; i<d.data.length; i++) {
547 d.data[i] = r.slice(0,1);
548 d.data[i][1] = func(r.slice(1))
550 d.labels = d.labels.slice(0,1);
555 /* currently displayed graphs, indexed by divname */
556 global_graphs = new Array();
559 find a graph by divname
561 function graph_find(divname) {
562 for (var i=0; i<global_graphs.length; i++) {
563 var g = global_graphs[i];
564 if (g.divname == divname) {
571 function nameAnnotation(ann) {
572 return "(" + ann.series + ", " + ann.xval + ")";
578 try to save an annotation via annotation.cgi
580 function save_annotation(ann) {
581 var r = new XMLHttpRequest();
583 "cgi/annotation.cgi?series="+escape(ann.series)+"&xval="+ann.xval+"&text="+escape(ann.text), true);
588 load annotations from annotations.csv
590 function load_annotations(g) {
591 function callback(d) {
592 var anns_by_name = new Array();
594 for (var i=0; i<d.data.length; i++) {
595 var xval = d.data[i][0] + (tz_difference*60*60*1000);
596 xval = round_time(xval, defaultAttrs.averaging);
597 if (xval.valueOf() < pvdate.valueOf() ||
598 xval.valueOf() >= (pvdate.valueOf() + (24*60*60*1000))) {
602 xval: xval.valueOf(),
603 series: d.data[i][1],
605 text: decodeURIComponent(d.data[i][2])
607 var a = anns_by_name[nameAnnotation(ann)];
608 if (a == undefined) {
609 anns_by_name[nameAnnotation(ann)] = annotations.length;
610 annotations.push(ann);
612 annotations[a] = ann;
613 if (ann.text == '') {
614 annotations.splice(a,1);
618 for (var i=0; i<global_graphs.length; i++) {
619 var g = global_graphs[i];
620 g.setAnnotations(annotations);
624 load_CSV("../CSV/annotations.csv", callback);
627 function annotation_highlight(ann, point, dg, event) {
628 saveBg = ann.div.style.backgroundColor;
629 ann.div.style.backgroundColor = '#ddd';
632 function annotation_unhighlight(ann, point, dg, event) {
633 ann.div.style.backgroundColor = saveBg;
637 handle annotation updates
639 function annotation_click(ann, point, dg, event) {
640 ann.text = prompt("Enter annotation", ann.text);
641 for (var i=0; i<annotations.length; i++) {
642 if (annotations[i].xval == ann.xval && annotations[i].series == ann.series) {
643 annotations[i].text = ann.text;
644 if (ann.text == '' || ann.text == null) {
646 writeDebug("removing annnotation");
647 annotations.splice(i,1);
652 for (var i=0; i<global_graphs.length; i++) {
653 var g = global_graphs[i];
654 if (g.series_names.indexOf(ann.series) != -1) {
655 g.setAnnotations(annotations);
658 save_annotation(ann);
662 add a new annotation to one graph
664 function annotation_add_graph(g, p, ann) {
665 var anns = g.annotations();
668 if (ann.text == '') {
669 var idx = anns.indexOf(p);
674 p.annotation.text = ann.text;
679 g.setAnnotations(anns);
685 function annotation_add(event, p) {
688 xval: p.xval - (tz_difference*60*60*1000),
690 text: prompt("Enter annotation", ""),
692 if (ann.text == '' || ann.text == null) {
695 for (var i=0; i<global_graphs.length; i++) {
696 var g = global_graphs[i];
697 if (g.series_names.indexOf(p.name) != -1) {
698 annotation_add_graph(g, p, ann);
702 save_annotation(ann);
706 /* default dygraph attributes */
712 annotationMouseOverHandler: annotation_highlight,
713 annotationMouseOutHandler: annotation_unhighlight,
714 annotationClickHandler: annotation_click,
715 pointClickCallback: annotation_add
719 round to averaged time
721 function round_time(t, n) {
722 var t2 = t / (60*1000);
723 t2 = Math.round((t2/n)-0.5);
729 average some data over time
731 function average_data(data, n) {
732 var ret = new Array();
733 var rem = data.length % n;
735 for (y=0; y<data.length-rem; y++) {
736 var y2 = Math.round((y/n)-0.5);
737 var t = round_time(data[y][0], n);
738 if (ret[y2] == undefined) {
740 for (var x=1; x<ret[y2].length; x++) {
744 for (var x=1; x<ret[y2].length; x++) {
745 ret[y2][x] += data[y][x]/n;
750 for (; y<data.length; y++) {
752 var t = round_time(data[y][0], 1);
760 graph results from a set of CSV files:
761 - apply func1 to the name columns within each file
762 - apply func2 between the files
764 function graph_csv_files_func(divname, filenames, columns, func1, func2, attrs) {
765 /* load the csv files */
766 var caller = new Object();
767 caller.divname = divname;
768 caller.filenames = filenames.slice(0);
769 caller.columns = columns.slice(0);
770 caller.func1 = func1;
771 caller.func2 = func2;
772 caller.attrs = attrs;
774 if (attrs.series_base != undefined) {
775 caller.colname = attrs.series_base;
776 } else if (columns.length == 1) {
777 caller.colname = columns[0]
779 caller.colname = divname;
782 /* called when all the data is loaded and we're ready to apply the
783 functions and graph */
784 function loaded_callback(d) {
786 if (d[0] == undefined) {
791 for (var i=0; i<caller.filenames.length; i++) {
792 apply_function(d[i], caller.func1, caller.colname);
795 /* work out the y offsets to align the times */
796 var yoffsets = new Array();
798 for (var i=1; i<caller.filenames.length; i++) {
800 if (d[i].data[0][0] < d[0].data[0][0]) {
801 while (d[i].data[yoffsets[i]][0] < d[0].data[0][0]) {
804 } else if (d[i].data[0][0] > d[0].data[0][0]) {
805 while (d[i].data[0][0] > d[0].data[-yoffsets[i]][0]) {
811 if (caller.attrs.missingValue !== undefined) {
812 missingValue = attrs.missingValue;
818 var data = d[0].data;
819 for (var j=0; j<data.length; j++) {
820 if (data[j][1] == missingValue) {
823 for (var i=1; i<caller.filenames.length; i++) {
824 var y = j + yoffsets[i];
825 if (y < 0 || y >= d[i].data.length || d[i].data[y][1] == missingValue) {
828 data[j][i+1] = d[i].data[y][1];
833 labels = new Array();
834 labels[0] = d[0].labels[0];
835 for (var i=0; i<caller.filenames.length; i++) {
836 labels[i+1] = caller.colname + (i+1);
839 var d2 = { labels: labels, data: data };
840 apply_function(d2, caller.func2, caller.colname);
842 /* add the labels to the given graph attributes */
843 caller.attrs.labels = d2.labels;
845 for (a in defaultAttrs) {
846 if (caller.attrs[a] == undefined) {
847 caller.attrs[a] = defaultAttrs[a];
851 caller.attrs['labelsDiv'] = divname + ":labels";
853 /* we need to create a new one, as otherwise we can't remove
855 for (var i=0; i<global_graphs.length; i++) {
856 var g = global_graphs[i];
857 if (g.divname == divname) {
858 global_graphs.splice(i, 1);
864 var max_points = 900;
868 if (auto_averaging) {
869 if (d2.data != null && (d2.data.length/defaultAttrs.averaging) > max_points) {
870 set_averaging(Math.round(0.5+(d2.data.length / max_points)));
874 var avg_data = average_data(d2.data, defaultAttrs.averaging);
876 /* create a new dygraph */
877 if (hashvars['nograph'] != '1') {
878 g = new Dygraph(document.getElementById(divname), avg_data, caller.attrs);
879 g.series_names = caller.attrs.labels;
881 g.setAnnotations(annotations);
882 global_graphs.push(g);
888 /* fire off a request to load the data */
892 get_csv_data(caller.filenames, caller.columns, loaded_callback);
896 function product(v) {
898 for (var i=1; i<v.length; i++) {
906 for (var i=1; i<v.length; i++) {
915 graph one column from a set of CSV files
917 function graph_csv_files(divname, filenames, column, attrs) {
918 return graph_csv_files_func(divname, filenames, [column], null, null, attrs);
922 graph one column from a set of CSV files as a sum over multiple files
924 function graph_sum_csv_files(divname, filenames, column, attrs) {
925 return graph_csv_files_func(divname, filenames, [column], null, sum, attrs);
929 show all the live data graphs
931 function show_graphs() {
932 hide_div("nodata", true);
934 pvdate_base = pvdate.getTime();
936 graph_sum_csv_files("Total AC Power (W)",
939 { includeZero: true });
942 graph_csv_files("AC Power from each inverter (W) [Pac]",
945 { includeZero: true });
947 graph_csv_files("DC Voltage for each inverter (V) [UpvIst]",
950 { includeZero: false,
951 missingValue: 666 });
953 graph_csv_files("Target DC Voltage for each inverter (V) [UpvSoll]",
956 { includeZero: false,
957 missingValue: 666 });
960 graph_sum_csv_files("Total DC current (A)",
963 { includeZero: true });
965 graph_csv_files("DC Current for each inverter (A) [Ipv]",
968 { includeZero: false });
970 graph_csv_files_func("DC Power for each inverter (W) [Ipv*UpvIst]",
972 [ "Ipv", "Upv-Ist" ],
975 series_base: 'Pdc' });
977 graph_csv_files_func("Total DC Power (W)",
979 [ "Ipv", "Upv-Ist" ],
981 { includeZero: true });
984 function efficiency(v) {
985 var dc_pow = v[1] * v[2];
989 return 100.0*(v[0] / dc_pow);
992 graph_csv_files_func("Inverter efficiencies (%) [(Ipv*UpvIst)/Pac]",
994 [ "Pac", "Ipv", "Upv-Ist" ],
996 { includeZero: false,
997 series_base: 'Eff'});
999 graph_csv_files("AC Voltage for each inverter (V) [Uac]",
1002 { includeZero: false });
1004 graph_csv_files("Lifetime Power for each inverter (kWh) [E-total]",
1007 { includeZero: false });
1009 graph_sum_csv_files("Total Lifetime Power (kWh)",
1012 { includeZero: false });
1014 graph_csv_files("Fan voltage for each inverter (V) [UFan]",
1017 { includeZero: true,
1019 valueRange: [0, 12] });
1027 called when the user selects a date
1029 function set_date(e) {
1030 var dp = datePickerController.getDatePicker("pvdate");
1031 pvdate = date_round(dp.date);
1032 hashvars['date'] = date_YMD(pvdate);
1033 rewrite_hashvars(hashvars);
1034 writeDebug("redrawing for: " + pvdate);
1035 annotations = new Array();
1040 setup the datepicker widget
1042 function setup_datepicker() {
1043 document.getElementById("pvdate").value =
1044 intLength(pvdate.getDate(),2) + "/" + intLength(pvdate.getMonth()+1, 2) + "/" + pvdate.getFullYear();
1045 datePickerController.addEvent(document.getElementById("pvdate"), "change", set_date);
1050 called to reload every few minutes
1052 function reload_timer() {
1053 /* flush the old CSV cache */
1054 CSV_Cache = new Array();
1055 writeDebug("reloading on timer");
1056 if (loading_counter == 0) {
1059 setup_reload_timer();
1063 setup for automatic reloads
1065 function setup_reload_timer() {
1066 setTimeout(reload_timer, 300000);
1071 toggle display of a div
1073 function toggle_div(divname)
1075 var div = document.getElementById(divname);
1076 var img = document.getElementById("img-" + divname);
1077 var current_display = div.style.display;
1078 var old_src = img.getAttribute("src");
1079 if (current_display != "none") {
1080 div.style.display = "none";
1081 img.setAttribute("src", old_src.replace("_unhide", "_hide"));
1083 div.style.display = "block";
1084 img.setAttribute("src", old_src.replace("_hide", "_unhide"));
1089 change display period
1091 function change_period(p) {
1093 if (period_days != p) {
1104 function change_averaging() {
1105 var v = +document.getElementById('averaging').value;
1106 defaultAttrs.averaging = v;
1114 function set_averaging(v) {
1115 var a = document.getElementById('averaging');
1117 defaultAttrs.averaging = v;