"use strict";
var version="$Id: fhemweb.js 13392 2017-02-11 18:32:13Z rudolfkoenig $";
var FW_serverGenerated;
var FW_serverFirstMsg = (new Date()).getTime()/1000;
var FW_serverLastMsg = FW_serverFirstMsg;
var FW_isIE = (navigator.appVersion.indexOf("MSIE") > 0);
var FW_isiOS = navigator.userAgent.match(/(iPad|iPhone|iPod)/);
var FW_scripts = {}, FW_links = {};
var FW_docReady = false, FW_longpollType;
var FW_root = "/fhem"; // root
var embedLoadRetry = 100;
// createFn returns an HTML Element, which may contain
// - setValueFn, which is called when data via longpoll arrives
// - activateFn, which is called after the HTML element is part of the DOM.
var FW_widgets = {
select: { createFn:FW_createSelect },
slider: { createFn:FW_createSlider },
time: { createFn:FW_createTime },
noArg: { createFn:FW_createNoArg },
multiple: { createFn:FW_createMultiple },
"multiple-strict": { createFn:FW_createMultiple },
textfield: { createFn:FW_createTextField },
"textfield-long": { createFn:FW_createTextField }
};
window.onbeforeunload = function(e)
{
FW_leaving = 1;
return undefined;
}
window.onerror = function(errMsg, url, lineno)
{
url = url.replace(/.*\//,'');
if($("body").attr("data-confirmJSError") != 0)
FW_okDialog(url+" line "+lineno+":
"+errMsg);
}
function
FW_replaceWidgets(parent)
{
parent.find("div.fhemWidget").each(function() {
var dev=$(this).attr("dev");
var cmd=$(this).attr("cmd");
var rd=$(this).attr("reading");
var params = cmd.split(" ");
var type=$(this).attr("type");
if( type == undefined ) type = "set";
FW_replaceWidget(this, dev, $(this).attr("arg").split(","),
$(this).attr("current"), rd, params[0], params.slice(1),
function(arg) {
FW_cmd(FW_root+"?cmd="+type+" "+dev+
(params[0]=="state" ? "":" "+params[0])+" "+arg+"&XHR=1");
});
});
}
function
FW_jqueryReadyFn()
{
if(FW_docReady) // loading fhemweb.js twice is hard to debug
return;
log(version);
FW_docReady = true;
FW_serverGenerated = $("body").attr("generated");
FW_longpollType = $("body").attr("longpoll");
if(FW_longpollType != "0")
setTimeout("FW_longpoll()", 100);
$("a").each(function() { FW_replaceLink(this); })
$("head script").each(function() {
var sname = $(this).attr("src"),
p = FW_scripts[sname];
if(!p) {
FW_scripts[sname] = { loaded:true };
return;
}
FW_scripts[sname].loaded = true;
if(p.callbacks && !p.called) {
p.called = true; // Avoid endless loop
for(var i1=0; i1< p.callbacks.length; i1++)
if(p.callbacks[i1]) // pushing undefined callbacks on the stack is ok
p.callbacks[i1]();
delete(p.callbacks);
}
});
$("head link").each(function() { FW_links[$(this).attr("href")] = 1 });
$("div.makeSelect select").each(function() {
FW_detailSelect(this);
$(this).change(FW_detailSelect);
});
// Activate the widgets
var r = $("head").attr("root");
if(r)
FW_root = r;
FW_replaceWidgets($("html"));
FW_confirmDelete();
// Fix the td count by setting colspan on the last column
$("table.block.wide").each(function(){ // table
var id = $(this).attr("id");
if(!id || id.indexOf("TYPE") != 0)
return;
var maxTd=0, tdCount=[];
$(this).find("tr").each(function(){ // count the td's
var cnt=0;
$(this).find("td").each(function(){ cnt++; });
if(maxTd < cnt) maxTd = cnt;
tdCount.push(cnt);
});
$(this).find("tr").each(function(){ // set the colspan
$(this).find("td").last().attr("colspan", maxTd-tdCount.shift()+1);
});
});
// Replace the FORM-POST in detail-view by XHR
/* Inactive, as Internals and Attributes arent auto updated.
$("form input[type=submit]").click(function(e) {
var cmd = "";
$(this).parent().find("[name]").each(function() {
cmd += (cmd?"&":"")+$(this).attr("name")+"="+$(this).val();
});
if(cmd.indexOf("detail=") < 0)
return;
e.preventDefault();
FW_cmd(FW_root+"?"+cmd+"&XHR=1");
});
*/
$("form input.get[type=submit]").click(function(e) { //"get" via XHR to dialog
e.preventDefault();
var cmd = "", el=this;
$(el).parent().find("input,[name]").each(function() {
cmd += (cmd?"&":"")+encodeURIComponent($(this).attr("name"))+
"="+encodeURIComponent($(this).val());
});
FW_cmd(FW_root+"?"+cmd+"&XHR=1&addLinks=1", function(data) {
if(!data.match(/^[\r\n]*$/)) // ignore empty answers
FW_okDialog('
'+data+'', el); }); }); $("#saveCheck") .css("cursor", "pointer") .click(function(){ var parent = this; FW_cmd(FW_root+"?cmd=save ?&XHR=1", function(data) { FW_okDialog('
'+data+'',parent); }); }); $("form").each(function(){ // shutdown handling var input = $(this).find("input.maininput"); if(!input.length) return; $(this).on("submit", function(e) { var val = $(input).val(); if(val.match(/^\s*shutdown/)) { FW_cmd(FW_root+"?XHR=1&cmd="+val); $(input).val(""); return false; } else if(val.match(/^\s*get\s+/)) { // make get use xhr instead of reload //return true; FW_cmd(FW_root+"?cmd="+encodeURIComponent(val)+"&XHR=1", function(data){ if( !data.match( /^.*<\/html>/ ) ) { data = data.replace( '<', '<' ); data = '
'+data+''; } if( location.href.indexOf('?') === -1 ) $('#content').html(data); else FW_okDialog(data); }); e.preventDefault(); $(input).val(""); return false; } return true; }); }); $("div.devSpecHelp a").each(function(){ // Help on detail window var dev = FW_getLink(this).split("#").pop(); $(this).unbind("click"); $(this).attr("href", "#"); // Desktop: show underlined Text $(this).removeAttr("onclick"); $(this).click(function(evt){ if($("#devSpecHelp").length) { $("#devSpecHelp").remove(); return; } $("#content").append(''); FW_cmd(FW_root+"?cmd=help "+dev+"&XHR=1", function(data) { $("#devSpecHelp").html(data); var off = $("#devSpecHelp").position().top-20; $('body, html').animate({scrollTop:off}, 500); }); }); }); $("table.attributes tr div.dname") // Click on attribute fills input value .each(function(){ $(this) .html(''+$(this).html()+'') .css({cursor:"pointer"}) .click(function(){ var aname = "#sel_attr"+$(this).attr("data-name").replace(/\./g,'_'); $(aname).val($(this).text()); FW_detailSelect(aname); }); }); $("[name=icon-filter]").on("change keyup paste", function() { clearTimeout($.data(this, 'delayTimer')); var wait = setTimeout(FW_filterIcons, 300); $(this).data('delayTimer', wait); }); FW_smallScreenCommands(); FW_inlineModify(); FW_rawDef(); } function FW_filterIcons() { var icons = $('.dist[title]'); icons.show(); var filterText = $('[name=icon-filter]').val(); if (filterText != '') { var re = RegExp(filterText,"i"); icons.filter(function() { return !re.test(this.title); }).hide(); } } function FW_confirmDelete() { var b = $("body"); var cd = $(b).attr("data-confirmDelete"); if(!cd || cd == 0) return; var wn = $(b).attr("data-webName"); $("div#content").find("a").each(function(){ var href = $(this).attr("href"); if(!href) return; var ma = $(this).attr("href").match(/.*cmd[^=]*=(delete[^&]*).*$/); if(!ma || ma.length != 2) return; $(this).attr("href", "#"); $(this).unbind("click"); $(this).click(function(e){ e.preventDefault(); var div = $("
'+txt+'', el);
else
FW_errmsg(txt, 5000);
});
});
$(el).css("cursor", "pointer");
}
function
FW_inlineModify() // Do not generate a new HTML page upon pressing modify
{
var cm;
if( typeof AddCodeMirror == 'function' ) {
// init codemirror for FW_style edit textarea
var s = $('textarea[name="data"]');
if( s.length && !s[0].editor ) {
s[0].editor = true; AddCodeMirror( s[0] );
}
}
$('#DEFa').click(function(){
var old = $('#edit').css('display');
$('#edit').css('display', old=='none' ? 'block' : 'none');
$('#disp').css('display', old=='none' ? 'none' : 'block');
if( typeof AddCodeMirror == 'function' ) {
var s=document.getElementById("edit").getElementsByTagName("textarea");
if(!s[0].editor) {
s[0].editor=true; AddCodeMirror(s[0], function(pcm) {cm = pcm;});
}
}
});
$("div input.psc[type=submit]:not(.get)").click(function(e){
e.preventDefault();
var newDef = typeof cm !== 'undefined' ?
cm.getValue() : $(this).closest("form").find("textarea").val();
var cmd = $(this).attr("name")+"="+$(this).attr("value")+" "+newDef;
var isDef = true;
if( newDef == undefined ) {
isDef = false;
var div = $(this).closest("div.makeSelect");
var devName = $(div).attr("dev"),
cmd = $(div).attr("cmd");
var sel = $(this).closest("form").find("select");
var arg = $(sel).val();
var ifid = devName.replace(/\./g, '\\.');
if($(".dval[informid="+ifid+"-"+arg+"]").length == 0) {
console.log(this);
$(this).unbind('click').click();// No element found to replace, reload
return;
}
newDef = $(this).closest("form").find("input:text").val();
if(newDef == undefined)
newDef = $(this).closest("form").find("select:last").val();
cmd = $(this).attr("name")+"="+cmd+" "+devName+" "+arg+" "+newDef;
}
FW_cmd(FW_root+"?"+encodeURIComponent(cmd)+"&XHR=1", function(resp){
if(resp)
return FW_okDialog(resp);
newDef = newDef.replace(/&/g, '&') // Same as in 01_FHEMWEB
.replace(//g, '>');
if(isDef) {
if(newDef.indexOf("\n") >= 0)
newDef = ''+newDef+'
';
$("div#disp").html(newDef).css("display", "");
$("div#edit").css("display", "none");
}
});
});
}
function
FW_rawDef()
{
$("div.rawDef a").each(function(){ // Help on detail window
var dev = FW_getLink(this).split(" ").pop().split("&")[0];
$(this).unbind("click");
$(this).attr("href", "#"); // Desktop: show underlined Text
$(this).removeAttr("onclick");
$(this).click(function(evt){
if($("#rawDef").length) {
$("#rawDef").remove();
return;
}
$("#content").append(''+
''+
''+
' Dump "Probably associated with" too '+
'');
function
fillData(opt)
{
FW_cmd(FW_root+"?cmd=list "+opt+" "+dev+"&XHR=1", function(data) {
var re = new RegExp("^define", "gm");
data = data.replace(re, "defmod");
$("#rawDef textarea").val(data);
var off = $("#rawDef").position().top-20;
$('body, html').animate({scrollTop:off}, 500);
$("#rawDef button").hide();
$('#rawDef textarea').bind('input propertychange', function() {
var nData = $("#rawDef textarea").val();
if(nData != data)
$("#rawDef button").show();
else
$("#rawDef button").hide();
});
});
}
fillData("-r");
$("#rawDef input").click(function(){fillData(this.checked ?"-R":"-r")});
$("#rawDef button").click(function(){
var data = $("#rawDef textarea").val();
var arr = data.split("\n"), str="", i1=-1;
function
doNext()
{
if(++i1 >= arr.length) {
return FW_okDialog("Executed everything, no errors found.");
}
str += arr[i1];
if(arr[i1].charAt(arr[i1].length-1) === "\\") {
str += "\n";
return doNext();
}
if(str != "") {
str = str.replace(/\\\n/g, "\n")
.replace(/;;/g, ";");
FW_cmd(FW_root+"?cmd."+dev+"="+encodeURIComponent(str)+"&XHR=1",
function(r){
if(r)
return FW_okDialog(''+r+'
');
str = "";
doNext();
});
} else {
doNext();
}
}
doNext();
});
});
});
}
/*************** LONGPOLL START **************/
var FW_pollConn;
var FW_longpollOffset = 0;
var FW_leaving;
var FW_lastDataTime=0;
function
FW_doUpdate(evt)
{
var errstr = "Connection lost, trying a reconnect every 5 seconds.";
var input="";
var retryTime = 5000;
var now = new Date()/1000;
// iOS closes HTTP after 60s idle, websocket after 240s idle
if(now-FW_lastDataTime > 59) {
errstr="";
retryTime = 100;
}
FW_lastDataTime = now;
// Websocket starts with Android 4.4, and IE10
if(typeof WebSocket == "function" && evt && evt.target instanceof WebSocket) {
if(evt.type == 'close' && !FW_leaving) {
FW_errmsg(errstr, retryTime-100);
FW_pollConn.close();
FW_pollConn = undefined;
setTimeout(FW_longpoll, retryTime);
return;
}
input = evt.data;
FW_longpollOffset = 0;
} else {
if(FW_pollConn.readyState == 4 && !FW_leaving) {
if(FW_pollConn.status == "401") {
location.reload();
return;
}
FW_errmsg(errstr, retryTime-100);
setTimeout(FW_longpoll, retryTime);
return;
}
if(FW_pollConn.readyState != 3)
return;
input = FW_pollConn.responseText;
}
var devs = new Array();
if(!input || input.length <= FW_longpollOffset)
return;
FW_serverLastMsg = (new Date()).getTime()/1000;
for(;;) {
var nOff = input.indexOf("\n", FW_longpollOffset);
if(nOff < 0)
break;
var l = input.substr(FW_longpollOffset, nOff-FW_longpollOffset);
FW_longpollOffset = nOff+1;
log("Rcvd: "+(l.length>132 ? l.substring(0,132)+"...("+l.length+")":l));
if(!l.length)
continue;
if(l.indexOf("<")== 0) { // HTML returned by proxy, if FHEM behind is dead
FW_closeConn();
FW_errmsg(errstr, retryTime-100);
setTimeout(FW_longpoll, retryTime);
return;
}
var d = JSON.parse(l);
if(d.length != 3)
continue;
if( d[0].match(/^#FHEMWEB:/) ) {
eval(d[1]);
} else {
$("[informId='"+d[0]+"']").each(function(){
if(this.setValueFn) { // change the select/etc value
this.setValueFn(d[1].replace(/\n/g, '\u2424'));
} else {
if(d[2].match(/\n/))
d[2] = ''+d[2]+'
';
var ma = /^([\s\S]*)<\/html>$/.exec(d[2]);
if(!d[0].match("-")) // not a reading
$(this).html(d[2]);
else if(ma)
$(this).html(ma[1]);
else
$(this).text(d[2]);
if(d[0].match(/-ts$/)) // timestamps
$(this).addClass('changed');
$(this).find("a").each(function() { FW_replaceLink(this) });
}
});
}
for(var w in FW_widgets)
if(FW_widgets[w].updateLine) // updateLine is deprecated, use setValueFn
FW_widgets[w].updateLine(d);
devs.push(d);
}
for(var w in FW_widgets)
if(FW_widgets[w].updateDevs) // used for SVG to avoid double-reloads
FW_widgets[w].updateDevs(devs);
// reset the connection to avoid memory problems
if(FW_longpollOffset > 1024*1024 && FW_longpollOffset==input.length)
FW_longpoll();
}
function
FW_closeConn()
{
FW_leaving = 1;
if(!FW_pollConn)
return;
if(typeof FW_pollConn.close == "function")
FW_pollConn.close();
else if(typeof FW_pollConn.abort == "function")
FW_pollConn.abort();
FW_pollConn = undefined;
}
function
FW_longpoll()
{
FW_closeConn();
FW_leaving = 0;
FW_longpollOffset = 0;
// Build the notify filter for the backend
var filter = $("body").attr("longpollfilter");
if(filter == null)
filter = "";
var retry;
if(filter == "") {
$("embed").each(function() {
if(FW_getSVG(this) == undefined && !retry &&
filter != ".*" && --embedLoadRetry > 0) {
retry = 1;
setTimeout(FW_longpoll, 100);
return;
}
if($(FW_getSVG(this)).find("svg[flog]").attr("flog"))
filter=".*";
});
if(retry)
return;
}
if(filter == "") {
var sa = location.search.substring(1).split("&");
for(var i = 0; i < sa.length; i++) {
if(sa[i].substring(0,5) == "room=")
filter=sa[i];
if(sa[i].substring(0,7) == "detail=")
filter=sa[i].substring(7);
}
}
if($("#floorplan").length>0) //floorplan special
filter += ";iconPath="+$("body").attr("name");
if(filter == "") {
var content = document.getElementById("content");
if(content) {
var room = content.getAttribute("room");
if(room)
filter="room="+room;
}
}
var iP = $("body").attr("iconPath");
if(iP != null)
filter = filter +";iconPath="+iP;
var since = "null";
if(FW_serverGenerated)
since = FW_serverLastMsg + (FW_serverGenerated-FW_serverFirstMsg);
var query = "?XHR=1"+
"&inform=type=status;filter="+filter+";since="+since+";fmt=JSON"+
'&fw_id='+$("body").attr('fw_id')+
"×tamp="+new Date().getTime();
var loc = (""+location).replace(/\?.*/,"");
if(typeof WebSocket == "function" && FW_longpollType == "websocket") {
FW_pollConn = new WebSocket((loc+query).replace(/^http/i, "ws"));
FW_pollConn.onclose =
FW_pollConn.onerror =
FW_pollConn.onmessage = FW_doUpdate;
} else {
FW_pollConn = new XMLHttpRequest();
FW_pollConn.open("GET", location.pathname+query, true);
if(FW_pollConn.overrideMimeType) // Win 8.1, #66004
FW_pollConn.overrideMimeType("application/json");
FW_pollConn.onreadystatechange = FW_doUpdate;
FW_pollConn.send(null);
}
log("Inform-channel opened ("+(FW_longpollType==1 ? "HTTP":FW_longpollType)+
") with filter "+filter);
}
/*************** LONGPOLL END **************/
/*************** WIDGETS START **************/
/*************** "Double" select in detail window ****/
function
FW_detailSelect(selEl)
{
if(selEl.target)
selEl = selEl.target;
var selVal = $(selEl).val();
var div = $(selEl).closest("div.makeSelect");
var arg,
listArr = $(div).attr("list").split(" "),
devName = $(div).attr("dev"),
cmd = $(div).attr("cmd");
for(var i1=0; i1 selVal.length)
vArr = arg.substr(selVal.length+1).split(",");
var newEl = FW_replaceWidget($(selEl).next(), devName, vArr,undefined,selVal);
if(cmd == "attr")
FW_queryValue('{AttrVal("'+devName+'","'+selVal+'","")}', newEl);
if(cmd == "set")
FW_queryValue('{ReadingsVal("'+devName+'","'+selVal+'","")}', newEl);
}
function
FW_replaceWidget(oldEl, devName, vArr, currVal, reading, set, params, cmd)
{
var newEl, wn;
var elName = $(oldEl).attr("name");
if(!elName)
elName = $(oldEl).find("[name]").attr("name");
if(vArr.length == 0) { // No parameters, input field
newEl = FW_createTextField(elName, devName, ["textField"], currVal,
set, params, cmd);
wn = "textField";
} else {
for(wn in FW_widgets) {
if(FW_widgets[wn].createFn) {
newEl = FW_widgets[wn].createFn(elName, devName, vArr, currVal,
set, params, cmd);
if(newEl)
break;
}
}
if(!newEl) { // Select as fallback
vArr.unshift("select");
newEl = FW_createSelect(elName, devName, vArr, currVal, set, params, cmd);
wn = "select";
}
}
if(!newEl) { // Simple link
newEl = $('');
$(newEl).click(function(arg) { cmd(params[0]) });
$(oldEl).replaceWith(newEl);
return newEl;
}
$(newEl).addClass(wn+"_widget");
if( $(newEl).find("[informId]").length == 0 && !$(newEl).attr("informId") ) {
if(reading)
$(newEl).attr("informId", devName+"-"+reading);
}
$(oldEl).replaceWith(newEl);
if(newEl.activateFn) // CSS is not applied if newEl is not in the document
newEl.activateFn();
return newEl;
}
function
FW_queryValue(cmd, el)
{
log("FW_queryValue:"+cmd);
var query = location.pathname+"?cmd="+cmd+"&XHR=1";
query = addcsrf(query);
var qConn = new XMLHttpRequest();
qConn.onreadystatechange = function() {
if(qConn.readyState != 4)
return;
var qResp = qConn.responseText.replace(/\n$/, '');
qResp = qResp.replace(/\n/g, '\u2424');
if(el.setValueFn)
el.setValueFn(qResp);
qConn.abort();
}
qConn.open("GET", query, true);
qConn.send(null);
}
/*************** TEXTFIELD **************/
function
FW_createTextField(elName, devName, vArr, currVal, set, params, cmd)
{
if(vArr.length != 1 ||
(vArr[0] != "textField" &&
vArr[0] != "textFieldNL" &&
vArr[0] != "textField-long" &&
vArr[0] != "textFieldNL-long") ||
(params && params.length))
return undefined;
var is_long = (vArr[0].indexOf("long") > 0);
var newEl = $("").get(0);
if(set && set != "state" && vArr[0].indexOf("NL") < 0)
$(newEl).append(set+":");
$(newEl).append('');
var inp = $(newEl).find("input").get(0);
if(elName)
$(inp).attr('name', elName);
if(currVal != undefined)
$(inp).val(currVal);
function addBlur() { if(cmd) $(inp).blur(function() { cmd($(inp).val()) }); };
newEl.setValueFn = function(arg){ $(inp).val(arg) };
addBlur();
var myFunc = function(){
$(inp).unbind("blur");
$('body').append(
'');
var txt = $(inp).val();
txt = txt.replace(/\u2424/g, '\n');
$("#td_longText").val(txt);
var cm;
if(typeof AddCodeMirror == 'function' && !$("#td_longText").get(0).editor) {
$("#td_longText").get(0).editor = true;
AddCodeMirror($("#td_longText").get(0), function(pcm) {cm = pcm;});
}
$('#editdlg').dialog(
{ modal:true, closeOnEscape:true, width:$(window).width()*3/4,
maxHeight:$(window).height()*3/4,
close:function(){ $('#editdlg').remove(); },
buttons:[
{ text:"Cancel", click:function(){
$(this).dialog('close');
addBlur();
}},
{ text:"OK", click:function(){
if(cm)
$("#td_longText").val(cm.getValue());
var res=$("#td_longText").val();
res = res.replace(/\n/g, '\u2424' );
$(this).dialog('close');
$(inp).val(res);
addBlur();
}}]
});
};
if( is_long )
$(newEl).click(myFunc);
return newEl;
}
/*************** select **************/
function
FW_createSelect(elName, devName, vArr, currVal, set, params, cmd)
{
if(vArr.length < 2 || vArr[0] != "select" || (params && params.length))
return undefined;
var newEl = document.createElement('select');
var vHash = {};
for(var j=1; j < vArr.length; j++) {
var o = document.createElement('option');
o.text = o.value = vArr[j].replace(/#/g," ");
vHash[vArr[j]] = 1;
newEl.options[j-1] = o;
}
if(currVal)
$(newEl).val(currVal);
if(elName)
$(newEl).attr('name', elName);
if(cmd)
$(newEl).change(function(arg) { cmd($(newEl).val()) });
newEl.setValueFn = function(arg) { if(vHash[arg]) $(newEl).val(arg); };
return newEl;
}
/*************** noArg **************/
function
FW_createNoArg(elName, devName, vArr, currVal, set, params, cmd)
{
if(vArr.length != 1 || vArr[0] != "noArg" || (params && params.length))
return undefined;
var newEl = $('