mirror of https://github.com/lumapu/ahoy.git
				
				
			
			You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							439 lines
						
					
					
						
							20 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							439 lines
						
					
					
						
							20 KiB
						
					
					
				| <!doctype html> | |
| <html> | |
|     <head> | |
|         <title>Live</title> | |
|         {#HTML_HEADER} | |
|         <meta name="apple-mobile-web-app-capable" content="yes"> | |
|     </head> | |
|     <body> | |
|         {#HTML_NAV} | |
|         <div id="wrapper"> | |
|             <div id="content"> | |
|                 <div id="live"></div> | |
|                 <p>Every <span id="refresh"></span> seconds the values are updated</p> | |
|             </div> | |
|         </div> | |
|         {#HTML_FOOTER} | |
|         <script type="text/javascript"> | |
|             var exeOnce = true; | |
|             var units, ivEn; | |
|             var mIvHtml = []; | |
|             var mNum = 0; | |
|             var total = Array(6).fill(0); | |
|             var tPwrAck; | |
| 
 | |
|             function parseGeneric(obj) { | |
|                 if(true == exeOnce){ | |
|                     parseNav(obj); | |
|                     parseESP(obj); | |
|                 } | |
|                 parseRssi(obj); | |
|             } | |
| 
 | |
|             function numBig(val, unit, des) { | |
|                 return ml("div", {class: "col-6 col-sm-4 a-c"}, [ | |
|                     ml("div", {class: "row"}, | |
|                         ml("div", {class: "col"}, [ | |
|                             ml("span", {class: "fs-5 fs-md-4"}, String(Math.round(val * 100) / 100)), | |
|                             ml("span", {class: "fs-6 fs-md-7 mx-1"}, unit) | |
|                         ])), | |
|                     ml("div", {class: "row"}, | |
|                         ml("div", {class: "col"}, | |
|                             ml("span", {class: "fs-9 px-1"}, des) | |
|                         ) | |
|                     ) | |
|                 ]); | |
|             } | |
| 
 | |
|             function numMid(val, unit, des) { | |
|                 return ml("div", {class: "col-6 col-sm-4 col-md-3 mb-2"}, [ | |
|                     ml("div", {class: "row"}, | |
|                         ml("div", {class: "col"}, [ | |
|                             ml("span", {class: "fs-6"}, String(Math.round(val * 100) / 100)), | |
|                             ml("span", {class: "fs-8 mx-1"}, unit) | |
|                         ])), | |
|                     ml("div", {class: "row"}, | |
|                         ml("div", {class: "col"}, | |
|                             ml("span", {class: "fs-9"}, des) | |
|                         ) | |
|                     ) | |
|                 ]); | |
|             } | |
| 
 | |
|             function totals() { | |
|                 for(var i = 0; i < 6; i++) { | |
|                     total[i] = Math.round(total[i] * 100) / 100; | |
|                 } | |
| 
 | |
|                 return ml("div", {class: "row mt-3 mb-5"}, | |
|                     ml("div", {class: "col"}, [ | |
|                         ml("div", {class: "p-2 total-h"}, | |
|                             ml("div", {class: "row"}, | |
|                                 ml("div", {class: "col mx-2 mx-md-1"}, "TOTAL") | |
|                             ), | |
|                         ), | |
|                         ml("div", {class: "p-2 total-bg"}, [ | |
|                             ml("div", {class: "row"}, [ | |
|                                 numBig(total[0], "W", "AC Power"), | |
|                                 numBig(total[1], "Wh", "Yield Day"), | |
|                                 numBig(total[2], "kWh", "Yield Total") | |
|                             ]), | |
|                             ml("div", {class: "hr"}), | |
|                             ml("div", {class: "row"}, [ | |
|                                 numMid(total[3], "W", "Max Power"), | |
|                                 numMid(total[4], "W", "DC Power"), | |
|                                 numMid(total[5], "var", "Reactive Power") | |
|                             ]) | |
|                         ]) | |
|                     ]) | |
|                 ); | |
|             } | |
|             function ivHead(obj) { | |
|                 if(0 != obj.status) { // only add totals if inverter is online | |
|                     total[0] += obj.ch[0][2]; // P_AC | |
|                     total[3] += obj.ch[0][11]; // MAX P_AC | |
|                     total[4] += obj.ch[0][8]; // P_DC | |
|                     total[5] += obj.ch[0][10]; // Q_AC | |
|                 } | |
|                 total[1] += obj.ch[0][7]; // YieldDay | |
|                 total[2] += obj.ch[0][6]; // YieldTotal | |
|  | |
|                 var t = span(" °C"); | |
|                 var clh  = (0 == obj.status) ? "iv-h-dis" : "iv-h"; | |
|                 var clbg = (0 == obj.status) ? "iv-bg-dis" : "iv-bg"; | |
|                 var pwrLimit = "n/a"; | |
| 
 | |
|                 if(65535 != obj.power_limit_read) { | |
|                     pwrLimit = obj.power_limit_read + " %"; | |
|                     if(0 != obj.max_pwr) | |
|                         pwrLimit += ", " + Math.round(obj.max_pwr * obj.power_limit_read / 100) + "W"; | |
|                 } | |
|                 return ml("div", {class: "row mt-2"}, | |
|                     ml("div", {class: "col"}, [ | |
|                         ml("div", {class: "p-2 " + clh}, | |
|                             ml("div", {class: "row"}, [ | |
|                                 ml("div", {class: "col mx-2 mx-md-1"}, ml("span", { class: "pointer", onclick: function() { | |
|                                     getAjax("/api/inverter/version/" + obj.id, parseIvVersion); | |
|                                 }}, obj.name)), | |
|                                 ml("div", {class: "col a-c", onclick: function() {limitModal(obj)}}, [ | |
|                                     ml("span", {class: "d-none d-sm-block pointer"}, "Active Power Control: " + pwrLimit), | |
|                                     ml("span", {class: "d-block d-sm-none pointer"}, "APC: " + pwrLimit) | |
|                                 ]), | |
|                                 ml("div", {class: "col a-c"}, ml("span", { class: "pointer", onclick: function() { | |
|                                     getAjax("/api/inverter/alarm/" + obj.id, parseIvAlarm); | |
|                                 }}, ("Alarms: " + obj.alarm_cnt))), | |
|                                 ml("div", {class: "col a-r mx-2 mx-md-1"}, String(obj.ch[0][5].toFixed(1)) + t.innerText) | |
|                             ]) | |
|                         ), | |
|                         ml("div", {class: "p-2 " + clbg}, [ | |
|                             ml("div", {class: "row"},[ | |
|                                 numBig(obj.ch[0][2], "W", "AC Power"), | |
|                                 numBig(obj.ch[0][7], "Wh", "Yield Day"), | |
|                                 numBig(obj.ch[0][6], "kWh", "Yield Total") | |
|                             ]), | |
|                             ml("div", {class: "hr"}), | |
|                             ml("div", {class: "row mt-2"},[ | |
|                                 numMid(obj.ch[0][11], "W", "Max AC Power"), | |
|                                 numMid(obj.ch[0][8], "W", "DC Power"), | |
|                                 numMid(obj.ch[0][0], "V", "AC Voltage"), | |
|                                 numMid(obj.ch[0][1], "A", "AC Current"), | |
|                                 numMid(obj.ch[0][3], "Hz", "Frequency"), | |
|                                 numMid(obj.ch[0][9], "%", "Efficiency"), | |
|                                 numMid(obj.ch[0][10], "var", "Reactive Power"), | |
|                                 numMid(obj.ch[0][4], "", "Power Factor") | |
|                             ]) | |
|                         ]) | |
|                     ]) | |
|                 ); | |
|             } | |
| 
 | |
|             function numCh(val, unit, des) { | |
|                 return ml("div", {class: "col-12 col-sm-6 col-md-12 mb-2"}, [ | |
|                     ml("div", {class: "row"}, | |
|                         ml("div", {class: "col"}, [ | |
|                             ml("span", {class: "fs-6 fs-md-7"}, String(Math.round(val * 100) / 100)), | |
|                             ml("span", {class: "fs-8 mx-2"}, unit) | |
|                         ])), | |
|                     ml("div", {class: "row"}, | |
|                         ml("div", {class: "col"}, | |
|                             ml("span", {class: "fs-9"}, des) | |
|                         ) | |
|                     ) | |
|                 ]); | |
|             } | |
| 
 | |
|             function ch(status, name, vals) { | |
|                 var clh  = (0 == status) ? "iv-h-dis" : "ch-h"; | |
|                 var clbg = (0 == status) ? "iv-bg-dis" : "ch-bg"; | |
|                 return ml("div", {class: "col-6 col-md-3 mt-2"}, [ | |
|                     ml("div", {class: "p-2 a-c " + clh}, name), | |
|                     ml("div", {class: "p-2 " + clbg}, [ | |
|                         ml("div", {class: "row"}, [ | |
|                             numCh(vals[2], units[2], "DC Power"), | |
|                             numCh(vals[6], units[2], "Max Power"), | |
|                             numCh(vals[5], units[5], "Irradiation"), | |
|                             numCh(vals[3], units[3], "Yield Day"), | |
|                             numCh(vals[4], units[4], "Yield Total"), | |
|                             numCh(vals[0], units[0], "DC Voltage"), | |
|                             numCh(vals[1], units[1], "DC Current") | |
|                         ]) | |
|                     ]) | |
|                 ]); | |
|             } | |
| 
 | |
|             function tsInfo(obj) { | |
|                 var ageInfo = "Last received data requested at: "; | |
|                 if(obj.ts_last_success > 0) { | |
|                     var date = new Date(obj.ts_last_success * 1000); | |
|                     ageInfo += toIsoDateStr(date); | |
|                 } | |
|                 else | |
|                     ageInfo += "nothing received"; | |
| 
 | |
|                 if(obj.rssi > -127) { | |
|                     if(obj.generation < 2) | |
|                         ageInfo += " (RSSI: " + ((obj.rssi == -64) ? ">=" : "<") + " -64dBm)"; | |
|                     else | |
|                         ageInfo += " (RSSI: " + obj.rssi + "dBm)"; | |
|                 } | |
| 
 | |
|                 return ml("div", {class: "mb-5"}, [ | |
|                     ml("div", {class: "row p-1 ts-h mx-2"}, | |
|                         ml("div", {class: "col"}, "") | |
|                     ), | |
|                     ml("div", {class: "row p-2 ts-bg mx-2"}, | |
|                         ml("div", { class: "pointer col mx-2", onclick: function() { | |
|                             getAjax("/api/inverter/radiostat/" + obj.id, parseIvRadioStats); | |
|                         }}, ageInfo) | |
|                     ) | |
|                 ]); | |
|             } | |
| 
 | |
|             function parseIv(obj) { | |
|                 mNum++; | |
| 
 | |
|                 var chn = []; | |
|                 for(var i = 1; i < obj.ch.length; i++) { | |
|                     var name = obj.ch_name[i]; | |
|                     if(name.length == 0) | |
|                         name = "CHANNEL " + i; | |
|                     if(obj.ch_max_pwr[i] > 0) // show channel only if max mod pwr | |
|                         chn.push(ch(obj.status, name, obj.ch[i])); | |
|                 } | |
|                 mIvHtml.push( | |
|                     ml("div", {}, [ | |
|                         ivHead(obj), | |
|                         ml("div", {class: "row mb-2"}, chn), | |
|                         tsInfo(obj) | |
|                     ]) | |
|                 ); | |
| 
 | |
| 
 | |
|                 var last = true; | |
|                 for(var i = obj.id + 1; i < ivEn.length; i++) { | |
|                     if((i != ivEn.length) && ivEn[i]) { | |
|                         last = false; | |
|                         getAjax("/api/inverter/id/" + i, parseIv); | |
|                         break; | |
|                     } | |
|                 } | |
|                 if(last) { | |
|                     if(mNum > 1) | |
|                         mIvHtml.unshift(totals()); | |
|                     document.getElementById("live").replaceChildren(...mIvHtml); | |
|                 } | |
|             } | |
| 
 | |
|             function parseIvAlarm(obj) { | |
|                 var html = []; | |
|                 var offs = new Date().getTimezoneOffset() * -60; | |
|                 html.push( | |
|                     ml("div", {class: "row"}, [ | |
|                         ml("div", {class: "col"}, ml("strong", {}, "Event")), | |
|                         ml("div", {class: "col"}, ml("strong", {}, "ID")), | |
|                         ml("div", {class: "col"}, ml("strong", {}, "Start")), | |
|                         ml("div", {class: "col"}, ml("strong", {}, "End")) | |
|                     ]) | |
|                 ); | |
| 
 | |
|                 for(a of obj.alarm) { | |
|                     if(a.code != 0) { | |
|                         html.push( | |
|                             ml("div", {class: "row"}, [ | |
|                                 ml("div", {class: "col mt-3"}, String(a.str)), | |
|                                 ml("div", {class: "col mt-3"}, String(a.code)), | |
|                                 ml("div", {class: "col mt-3"}, String(toIsoTimeStr(new Date((a.start + offs) * 1000)))), | |
|                                 ml("div", {class: "col mt-3"}, String(toIsoTimeStr(new Date((a.end + offs) * 1000)))) | |
|                             ]) | |
|                         ); | |
|                     } | |
|                 } | |
|                 modal("Alarms of inverter " + obj.iv_name, ml("div", {}, html)); | |
|             } | |
| 
 | |
|             function parseIvVersion(obj) { | |
|                 var model; | |
|                 switch(obj.generation) { | |
|                     case 0: model = "MI-"; break; | |
|                     case 1: model = "HM-"; break; | |
|                     case 2: model = "HMS-"; break; | |
|                     case 3: model = "HMT-"; break; | |
|                     default: model = "???-"; break; | |
|                 } | |
|                 model += String(obj.max_pwr) + " (Serial: " + obj.serial + ")"; | |
| 
 | |
| 
 | |
|                 var html = ml("table", {class: "table"}, [ | |
|                     ml("tbody", {}, [ | |
|                         tr("Model", model), | |
|                         tr("Firmware Version / Build", String(obj.fw_ver) + " (build: " + String(obj.fw_date) + " " + String(obj.fw_time) + ")"), | |
|                         tr("Hardware Version / Build", (obj.hw_ver/100).toFixed(2) + " (build: " + String(obj.prod_cw) + "/" + String(obj.prod_year) + ")"), | |
|                         tr("Hardware Number", obj.part_num.toString(16)), | |
|                         tr("Bootloader Version", (obj.boot_ver/100).toFixed(2)) | |
|                     ]) | |
|                 ]); | |
|                 modal("Info for inverter " + obj.name, ml("div", {}, html)); | |
|             } | |
| 
 | |
| 
 | |
| 
 | |
|             function parseIvRadioStats(obj) { | |
|                 var html = ml("table", {class: "table"}, [ | |
|                     ml("tbody", {}, [ | |
|                         tr2(["TX count", obj.tx_cnt, ""]), | |
|                         tr2(["RX success", obj.rx_success, String(Math.round(obj.rx_success / obj.tx_cnt * 10000) / 100) + "%"]), | |
|                         tr2(["RX fail", obj.rx_fail, String(Math.round(obj.rx_fail / obj.tx_cnt * 10000) / 100) + "%"]), | |
|                         tr2(["RX no answer", obj.rx_fail_answer, String(Math.round(obj.rx_fail_answer / obj.tx_cnt * 10000) / 100) + "%"]), | |
|                         tr2(["RX fragments", obj.frame_cnt, ""]), | |
|                         tr2(["TX retransmits", obj.retransmits, ""]) | |
|                     ]) | |
|                 ]); | |
|                 modal("Radio statistics for inverter " + obj.name, ml("div", {}, html)); | |
|             } | |
| 
 | |
|             function limitModal(obj) { | |
|                 var opt = [["pct", "%"], ["watt", "W"]]; | |
|                 var html = ml("div", {}, [ | |
|                     ml("div", {class: "row mb-3"}, [ | |
|                         ml("div", {class: "col-12 col-sm-5 my-2"}, "Limit Value"), | |
|                         ml("div", {class: "col-8 col-sm-5"}, ml("input", {name: "limit", type: "number"}, "")), | |
|                         ml("div", {class: "col-4 col-sm-2"}, sel("type", opt, "pct")) | |
|                     ]), | |
|                     ml("div", {class: "row mb-3"}, [ | |
|                         ml("div", {class: "col-8 col-sm-5"}, "Keep limit over inverter restart"), | |
|                         ml("div", {class: "col-4 col-sm-7"}, ml("input", {type: "checkbox", name: "keep"})) | |
|                     ]), | |
|                     ml("div", {class: "row my-3"}, | |
|                         ml("div", {class: "col a-r"}, ml("input", {type: "button", value: "Apply", class: "btn", onclick: function() { | |
|                             applyLimit(obj.id); | |
|                         }}, null)) | |
|                     ), | |
|                     ml("div", {class: "row my-4"}, [ | |
|                         ml("div", {class: "col-12 col-sm-5 my-2"}, "Control"), | |
|                         ml("div", {class: "col col-sm-7 a-r"}, [ | |
|                             ml("input", {type: "button", value: "restart", class: "btn", onclick: function() { | |
|                                 applyCtrl(obj.id, "restart"); | |
|                             }}, null), | |
|                             ml("input", {type: "button", value: "turn off", class: "btn mx-1", onclick: function() { | |
|                                 applyCtrl(obj.id, "power", 0); | |
|                             }}, null), | |
|                             ml("input", {type: "button", value: "turn on", class: "btn", onclick: function() { | |
|                                 applyCtrl(obj.id, "power", 1); | |
|                             }}, null) | |
|                         ]) | |
|                     ]), | |
|                     ml("div", {class: "row mt-1"}, [ | |
|                         ml("div", {class: "col-12 col-sm-5 my-2"}, "Result"), | |
|                         ml("div", {class: "col-sm-7 my-2"}, ml("span", {name: "pwrres"}, "-")) | |
|                     ]) | |
|                 ]); | |
|                 modal("Active Power Control for inverter " + obj.name, html); | |
|             } | |
| 
 | |
|             function applyLimit(id) { | |
|                 var cmd = "limit_"; | |
|                 if(!document.getElementsByName("keep")[0].checked) | |
|                     cmd += "non"; | |
|                 cmd += "persistent_"; | |
|                 if(document.getElementsByName("type")[0].value == "pct") | |
|                     cmd += "relative"; | |
|                 else | |
|                     cmd += "absolute"; | |
| 
 | |
|                 var val = document.getElementsByName("limit")[0].value; | |
|                 if(isNaN(val)) | |
|                     val = 100; | |
| 
 | |
|                 var obj = new Object(); | |
|                 obj.id = id; | |
|                 obj.cmd = cmd; | |
|                 obj.val = val; | |
|                 getAjax("/api/ctrl", ctrlCb, "POST", JSON.stringify(obj)); | |
|             } | |
| 
 | |
|             function applyCtrl(id, cmd, val=0) { | |
|                 var obj = new Object(); | |
|                 obj.id  = id; | |
|                 obj.cmd = cmd; | |
|                 obj.val = val; | |
|                 getAjax("/api/ctrl", ctrlCb2, "POST", JSON.stringify(obj)); | |
|             } | |
| 
 | |
|             function ctrlCb(obj) { | |
|                 var e = document.getElementsByName("pwrres")[0]; | |
|                 if(obj.success) { | |
|                     e.innerHTML = "received command, waiting for inverter acknowledge ..."; | |
|                     tPwrAck = window.setInterval("getAjax('/api/inverter/pwrack/" + obj.id + "', updatePwrAck)", 1000); | |
|                 } | |
|                 else | |
|                     e.innerHTML = "Error: " + obj["error"]; | |
|             } | |
| 
 | |
|             function ctrlCb2(obj) { | |
|                 var e = document.getElementsByName("pwrres")[0]; | |
|                 if(obj.success) | |
|                     e.innerHTML = "command received"; | |
|                 else | |
|                     e.innerHTML = "Error: " + obj["error"]; | |
|             } | |
| 
 | |
|             function updatePwrAck(obj) { | |
|                 if(!obj.ack) | |
|                     return; | |
|                 var e = document.getElementsByName("pwrres")[0]; | |
|                 clearInterval(tPwrAck); | |
|                 if(null == e) | |
|                     return; | |
|                 e.innerHTML = "inverter acknowledged active power control command"; | |
|             } | |
| 
 | |
|             function parse(obj) { | |
|                 if(null != obj) { | |
|                     parseGeneric(obj["generic"]); | |
|                     units = Object.assign({}, obj["fld_units"]); | |
|                     ivEn = Object.values(Object.assign({}, obj["iv"])); | |
|                     mIvHtml = []; | |
|                     mNum = 0; | |
|                     total.fill(0); | |
|                     for(var i = 0; i < obj.iv.length; i++) { | |
|                         if(obj.iv[i]) { | |
|                             getAjax("/api/inverter/id/" + i, parseIv); | |
|                             break; | |
|                         } | |
|                     } | |
|                     if(obj.refresh < 5) | |
|                         obj.refresh = 5; | |
|                     document.getElementById("refresh").innerHTML = obj.refresh; | |
|                     if(true == exeOnce) { | |
|                         window.setInterval("getAjax('/api/live', parse)", obj.refresh * 1000); | |
|                         exeOnce = false; | |
|                     } | |
|                 } | |
|                 else | |
|                     document.getElementById("refresh").innerHTML = "n/a"; | |
|             } | |
| 
 | |
|             getAjax("/api/live", parse); | |
|         </script> | |
|     </body> | |
| </html>
 | |
| 
 |