let miniservers; let miniservers_used = []; let rooms; let rooms_used; let categories; let categories_used = []; let controls = []; let statsconfig; let statsconfigLoxone; let controlstable = ""; let elementTypes_used = []; let loxone_elements; let filters = {}; let hints_hide = {}; let filterSearchDelay; var filterSearchString = ""; let timer = false; let timer_interval = 7000; let getImportSchedulerReport_running = false; let imports = []; $(function() { // debugger; restore_hints_hide(); if(hints_hide?.hint_activatestatistics != true) { $("#hint_activatestatistics").show(); } miniservers = JSON.parse( $("#miniservers_json").text() ); loxone_elements = JSON.parse( $("#loxone_elements_json").text() ); getLoxplan(); // Create filter radio and select bindings $('.filter_radio, .filter_select').on( "change", function(event, ui){ var filter_parent = event.currentTarget.name; console.log( "filter_select", event, ui ); if( event.currentTarget.nodeName == "INPUT" ) { var filterval = $("input[name='"+filter_parent+"']:checked").val(); } if( event.currentTarget.nodeName == "SELECT" ) { var filterval = $("#"+filter_parent+" option:selected").val(); if( filterval != "all" ) $(event.currentTarget.parentNode).addClass('filter-highlight'); else $(event.currentTarget.parentNode).removeClass('filter-highlight'); } console.log("Filter parent", filter_parent, filterval); filters[filter_parent] = filterval; // After changing the filter, recreate the table saveFilters(); updateTable(); updateReportTables(); }); // Create S4L Stat change bindings (Detail View) jQuery(document).on('change focusout keyup','.s4lchange',function (event, ui) { console.log( ".s4lchange binding entered"); if( typeof event.keyCode !== "undefined" && event.keyCode != 13) // NOT pressed Enter return; target = event.target; uid = $(target).closest('table').data("uid"); msno = $(target).closest('table').data("msno"); var control = controls.find( obj => { return obj.UID === uid && obj.msno == msno }) var stat = statsconfigLoxone.find(obj => { return obj.uuid === control.UID && obj.msno == control.msno }) // Validations and changes of dependent inputs if( target.id == "LoxoneDetails_s4lmeasurementname" ) { // s4lmeasurementname MUST be defined var measurementname = $("#LoxoneDetails_s4lmeasurementname").val(); measurementname = validateMeasurementname( measurementname, msno, uid ); $("#LoxoneDetails_s4lmeasurementname").val( measurementname ).textinput("refresh"); } if( target.id == "LoxoneDetails_s4lstatactive" ) { // Stats active checkbox was pressed console.log( "LoxoneDetails_s4lstatactive pressed", $(target).is(":checked") ); if( $(target).is(":checked") ) { // Activated if( ( $("#LoxoneDetails_s4lstatinterval").val() ) == "" ) { // Fill a number to the interval $("#LoxoneDetails_s4lstatinterval").val("5").textinput("refresh"); } // Activate interval field $("#LoxoneDetails_s4lstatinterval").prop( "disabled", false ).textinput("refresh"); $('[name="LoxoneDetails_s4loutput"]').prop( "disabled", false ); // Enable "Default" setting only when nothing else is activated if( !stat ) { // This element does not exist in stats.json yet, therefore enable "Default" on activation $('[name="LoxoneDetails_s4loutput"][value="Default"').prop( "checked", true ); } } else { // Disable interval field and outputs $("#LoxoneDetails_s4lstatinterval").prop( "disabled", true ).textinput("refresh"); $('[name="LoxoneDetails_s4loutput"]').prop( "disabled", true ); } } else if ( target.id == "LoxoneDetails_s4lstatinterval" ) { // Interval was changed console.log( "LoxoneDetails_s4lstatinterval changed" ); var interval = parseInt( $("#LoxoneDetails_s4lstatinterval").val() ); if ( isNaN( interval ) || interval <= 0 ) { // Not a number $("#LoxoneDetails_s4lstatinterval").val( "5" ).textinput("refresh"); } } else if ( target.name == "LoxoneDetails_s4loutput" ) { // Checkboxes of outputs were changed console.log( "LoxoneDetails_s4loutput changed" ); } // Now collect latest data of inputs var stat_active = $("#LoxoneDetails_s4lstatactive").is(":checked") ? "true" : "false"; var measurementname = $("#LoxoneDetails_s4lmeasurementname").val(); var stat_interval = parseInt($("#LoxoneDetails_s4lstatinterval").val()) * 60; // Collect checkboxes var stat_outputs = []; $('[name="LoxoneDetails_s4loutput"]:checked').each(function(){ stat_outputs.push($(this).val()); }); var output_labels = []; console.log("stat details data to send", control, stat_active, stat_interval, stat_outputs); // Post data $.post( "ajax.cgi", { action : "updatestat", name : control.Title, description :control.Desc, uuid : uid, msno : msno, type : control.Type, category : control.Category, room: control.Place, active: stat_active, measurementname: measurementname, interval: stat_interval, outputs : stat_outputs.join(','), outputlabels : control.outputlabels ? control.outputlabels.toString() : "", outputkeys : control.outputkeys ? control.outputkeys.toString() : "", minval : control.MinVal, maxval : control.MaxVal, unit : control.Unit }) .done(function(data){ // Find internal key of statistic element var statkey = statsconfigLoxone.findIndex(obj => { return obj.uuid === control.UID && obj.msno == control.msno }) // Enable the Import button if a stats.json entry exists now, and Loxone Stats are active if( control.StatsType != 0 ) { $("#LoxoneDetails_s4lstatimportbutton") .removeClass("ui-disabled"); } if( statkey != -1 ) { // If element found in internal data // Update internal statsconfigLoxone with ajax result statsconfigLoxone[statkey] = data; } else { // Not found in internal data - add object to array statsconfigLoxone.push( data ); } // We should have found the element in the table updateTable(); updateReportTables(data); }); }); // Bind on Search text box $("#filter_search").on( "input", function(event, ui){ window.clearTimeout(filterSearchDelay); filterSearchString = $(event.target).val(); filters["filter_search"] = filterSearchString; saveFilters(); // console.log("Text filter", filterSearchString); filterSearchDelay = window.setTimeout(function() { updateTable(); updateReportTables(); }, 500); }); $("#filter_search").on( "change", function(event, ui){ if( $(event.target).val() == "" ) { // $('#filter_search').css({'backgroundColor':'white'}); $('#filter_search').removeClass('filter-highlight'); $('#filter_search').attr("data-clear-btn", false); window.clearTimeout(filterSearchDelay); filterSearchString = $(event.target).val(); filters["filter_search"] = filterSearchString; saveFilters(); updateTable(); updateReportTables(); } else { // $('#filter_search').css({'backgroundColor':'#FFFF99'}); $('#filter_search').addClass('filter-highlight'); $('#filter_search').attr("data-clear-btn", true); } }); // Bind Loxone Details button jQuery(document).on('click', '.btnLoxoneDetails', function(event, ui){ target = event.target; uid = $(target).closest('tr').data("uid"); msno = $(target).closest('tr').data("msno"); popupLoxoneDetails(uid, msno); }); // Bind Import Now button jQuery(document).on('click', '#LoxoneDetails_s4lstatimportbutton', function(event, ui){ target = event.target; uid = $(target).data("uid"); msno = $(target).data("msno"); scheduleImport(msno, uid); }); }); function getLoxplan() { $("#popupProgress").popup("open"); var msupdateTextPre = "Fetching Loxone Config from Miniservers..."; $("#progressState").html(msupdateTextPre); // Get elements of all Miniservers var async_request=[]; var responses=[]; for (msno in miniservers) { async_request.push( $.post( "ajax.cgi", { action : "getloxplan", msno : msno }, function(data){ console.log(data); responses.push(data); }) ); } async_request.push( $.post( "ajax.cgi", { action : "getstatsconfig" }, function(data){ try { statsconfig = data; statsconfigLoxone = Object.values( statsconfig.loxone ); } catch(e) { // $("#progress_errors").append( "
Could not get stats.json data. Assuming empty stats.json
"); // $("#box_progress_errors").fadeIn(); console.log( "statsconfigLoxone seems to be empty" ); statsconfigLoxone = []; } }) ) $.when.apply( null, async_request).done( function(){ $("#progressState").html("Preparing controls..."); consolidateLoxPlan( responses ); $("#progressState").html("Generating display..."); updateTable(); $("#popupProgress").popup("close"); $("#progressState").html(""); setTimer(); getImportSchedulerReport(); }); } function consolidateLoxPlan( data ) { for (const [key, msobj] of Object.entries(data)) { if( data[key]?.error ) { console.log( "Error", data[key] ); $("#progress_errors").append( ""+data[key]?.error+"
" ); $("#box_progress_errors").fadeIn(); } rooms = $.extend( rooms, data[key].rooms ); rooms_used = $.extend( rooms_used, data[key].rooms_used ); categories = $.extend( categories, data[key].categories ); categories_used = $.extend( categories_used, data[key].categories_used ); miniservers_used = $.extend ( miniservers_used, data[key].miniservers ); elementTypes_used = elementTypes_used.concat( data[key].elementTypes ); if( typeof data[key].controls !== "undefined" ) { console.log( "controls from key", key, Object.keys(data[key].controls).length ); var objarr = Object.values( data[key].controls ); controls = controls.concat( objarr ); } } // Sort controls by Title controls = controls.filter ( control => control.msno > 0 ); // Filter controls not on an MS in LoxBerry controls.sort( dynamicSortMultiple( "Title" ) ); // Uniquify elementTypes_used elementTypes_used = elementTypes_used.filter( function(item, pos) { return elementTypes_used.indexOf(item) == pos; }) elementTypes_used.sort(); // Uniquify categories_used // categories_used = categories_used.filter( function(item, pos) { // return categories_used.indexOf(item) == pos; // }) miniservers_used = Object.values( miniservers_used ); miniservers_used = miniservers_used.filter( item => item.msno > 0 ); // Filter MS that are not in LoxBerry miniservers_used.sort( dynamicSort( "msno" ) ); // console.log("controls array", controls); // console.log("Controls", controls); var rooms_tmp = []; for (var roomid in rooms) { if(! roomid in rooms_used ) continue; rooms_tmp.push([rooms[roomid], roomid]); } rooms = rooms_tmp.sort(); var cat_tmp = []; for (var catid in categories) { if(categories_used.includes(catid)) { cat_tmp.push([categories[catid], catid]); } } categories = cat_tmp.sort(); generateFilter(); } function generateFilter() { // Add Miniservers to options for( const [key, msobj] of Object.entries(miniservers_used) ) { $('#filter_miniserver').append( ``); } // Add used rooms to options for( obj of rooms ) { // console.log(obj); $('#filter_room').append( ``); } // Add used categories to options for( obj of categories ) { // console.log(obj); $('#filter_category').append( ``); } // Add used elements to options in native language var elementsArr = []; for( var key in elementTypes_used ) { var ucKey = typeof elementTypes_used[key] !== "undefined" ? elementTypes_used[key].toUpperCase() : "undefined"; elementsArr.push( [ ucKey, typeof loxone_elements[ucKey]?.localname !== "undefined" ? loxone_elements[ucKey]?.localname : ucKey ] ); } elementsArr.sort(function(a, b) { a=a[1]; b=b[1]; return a b ? 1 : 0); }); for( obj of elementsArr ) { // console.log(obj); $('#filter_element').append( ``); } restoreFilters(); } function updateTable() { console.log("updateTable called"); controlstable = ""; createTableHead(); createTableBody(); createTableEnd(); $("#loxonecontrolstablediv").html( controlstable ); $("#loxonecontrolstablediv").removeClass("datahidden"); } function createTableHead() { controlstable += `| MS | Name (Type) | Location | Statistics | Import |
|---|
`;
var uncheckedImg = `
`;
var statDisplay;
if (statmatch?.active === "true" ) {
statDisplay = "";
s4l_interval = statmatch.interval/60;
}
else {
statDisplay = "display:none;";
s4l_interval = "";
// controlstable += "Not enabled";
}
controlstable += `
`;
var uncheckedImg = `
`;
$("#LoxoneDetails_visu").html(isLoxVisu ? checkedImg : uncheckedImg);
$("#LoxoneDetails_loxstat").html(control.StatsType > 0 ? checkedImg : uncheckedImg);
// S4L Settings
var statmatch = statsconfigLoxone.find(obj => {
return obj.uuid === control.UID && obj.msno == control.msno
})
if( statmatch?.outputs ) {
console.log("outputs", statmatch.outputs );
}
console.log("s4lstats checkboxes", statmatch);
if( statmatch?.active == "true" || statmatch?.active == true ) {
console.log("active = true");
$("#LoxoneDetails_s4lstatactive")
.prop('checked', true)
.prop('disabled', false)
.checkboxradio('refresh');
$("#LoxoneDetails_s4lstatinterval")
//.addClass("s4l_interval_highlight")
.prop('disabled', false)
.textinput( "refresh" );
}
else {
console.log("active = false");
$("#LoxoneDetails_s4lstatactive")
.prop('checked', false)
.prop('disabled', true)
.checkboxradio('refresh');
$("#LoxoneDetails_s4lstatinterval")
.prop('disabled', true)
//.removeClass("s4l_interval_highlight")
.textinput( "refresh" );
}
if( statmatch?.measurementname ) {
$("#LoxoneDetails_s4lmeasurementname").val(statmatch?.measurementname);
}
else {
$("#LoxoneDetails_s4lmeasurementname").val( validateMeasurementname( control.Desc ? control.Desc : control.Title, msno, uid) );
}
if( statmatch?.interval ) {
$("#LoxoneDetails_s4lstatinterval").val(statmatch?.interval / 60);
}
else {
$("#LoxoneDetails_s4lstatinterval").val("");
}
// Import now button
if( statmatch ) {
var importobj = imports.find(obj => {
return obj.data?.msno == msno && obj.data?.uuid == uid && obj.data?.status?.status == "running" })
if( importobj ) {
$("#LoxoneDetails_s4lstatimportbutton")
.addClass("ui-disabled");
}
else {
$("#LoxoneDetails_s4lstatimportbutton")
.removeClass("ui-disabled");
}
}
else {
$("#LoxoneDetails_s4lstatimportbutton")
.addClass("ui-disabled");
}
if( control.StatsType == 0 ) {
$("#LoxoneDetails_s4lstatimportbutton")
.addClass("ui-disabled");
}
// Live Data from Miniserver
$("#valuesLoxoneDetailsLive_title").html("Updating data...");
liveTable = $("#valuesLoxoneDetailsLive_table");
liveTable.empty();
$.post( "ajax.cgi", {
action : "lxlquery",
uuid : uid,
msno : control.msno,
})
.done(function(data){
console.log("Response from ajax lxlquery", data);
var dataStr;
if( data.error == null && data?.code == "200" ) {
$("#valuesLoxoneDetailsLive_title").html(`Live Data from Miniserver ${miniservers[control.msno].Name}`);
// Get mapping for this control type
var typeMappings = typeof data.mappings[control.Type.toUpperCase()] != "undefined" ? data.mappings[control.Type.toUpperCase()] : data.mappings["Default"];
console.log("Mappings for "+control.Type, typeMappings);
for( var key in data.response ) {
console.log("Output loop", key, data.response[key]);
var outputKey = data.response[key].Key;
var outputName = data.response[key].Name;
// Find mapping for outputKey
var mapKey = typeMappings.findIndex( element => element.lxlabel == outputName );
data.response[key].mapString = mapKey != -1 ? (parseInt(typeMappings[mapKey].statpos)+1) : "";
data.response[key].mapImg = data.response[key].mapString != "" ? `
Finished ${endtime}`;
break;
case "error":
case "dead":
html+= `
Finished with error ${endtime}`;
break;
case "scheduled":
html+= `
Queued`;
break;
}
// Update Detail View Import status
if( detail_msno == msno && detail_uuid == uuid ) {
$("#LoxoneDetails_importstatus").html(html);
}
$(target).html(html);
}
}
// Validates a measurementname and returns a suggestion if not valid
// measurementname is the string to verify
// selfIndex is the index key in statsconfigLoxone of itself (otherwise it may find itself)
function validateMeasurementname( measurementname, msno, uid ) {
// Find own array index in statsconfigLoxone
var selfIndex = statsconfigLoxone.findIndex( obj => { return obj.uuid === uid && obj.msno == msno })
if( typeof measurementname === 'undefined' || measurementname == "" ) {
// Check if statsconfigLoxone already knows name or description
// console.log( "1st try: measurementname is undefined" );
measurementname = statsconfigLoxone[selfIndex]?.description ? statsconfigLoxone[selfIndex].description : statsconfigLoxone[selfIndex]?.name;
}
console.log("measurementname", measurementname);
var measurementnameDefault = measurementname;
if( typeof measurementname === 'undefined') {
console.log( "2st try: measurementname is undefined" );
throw "measurementname is empty. Must be defined for validation.";
}
// Retrying different things
var counter =-1;
while(1) {
counter++;
// console.log("----------------", counter, "---------------");
switch(counter) {
case 0:
break;
case 1:
if( statsconfigLoxone[selfIndex]?.name && measurementname == statsconfigLoxone[selfIndex].name)
measurementname = statsconfigLoxone[selfIndex].name;
else
continue;
break;
case 20:
throw `Could not find an alternative measurementname after ${counter} tries`;
default:
measurementname = measurementnameDefault+"_"+counter.toString();
}
// Check if measurementname is already defined
var foundIndex = statsconfigLoxone.findIndex( function(obj, index) {
// console.log("findIndex", measurementname, selfIndex, obj["measurementname"], index);
if(selfIndex == index) { return false };
if(obj["measurementname"] != measurementname) { return false; }
return true;
})
// console.log("Try", counter, measurementname, foundIndex);
if(foundIndex == -1) {
break;
}
}
// console.log("foundIndex", foundIndex);
return measurementname.trim();
}
function clearTimer() {
console.log("Timer cleared");
window.clearInterval(timer);
timer = false;
}
function setTimer() {
console.log("Timer set");
timer = window.setInterval(getImportSchedulerReport, timer_interval);
}
function restore_hints_hide() {
try {
hints_hide = JSON.parse( localStorage.getItem("s4l_loxone_hints_hide") );
if( hints_hide == null ) {
hints_hide = { };
}
}
catch(e) {
console.log("restore_hints_hide", e);
hints_hide = { };
}
}
function hint_hide(hintid) {
hints_hide[hintid] = true;
$("#"+hintid).fadeOut();
localStorage.setItem("s4l_loxone_hints_hide", JSON.stringify(hints_hide));
}
// Sort function for arrays of objects
// https://stackoverflow.com/a/4760279/3466839
// Usage: arrayOfObjects.sort(dynamicSortMultiple("Name", "-Surname"));
function dynamicSort(property) {
var sortOrder = 1;
if(property[0] === "-") {
sortOrder = -1;
property = property.substr(1);
}
return function (a,b) {
/* next line works with strings and numbers,
* and you may want to customize it to your needs
*/
var result = (a[property] < b[property]) ? -1 : (a[property] > b[property]) ? 1 : 0;
return result * sortOrder;
}
}
function dynamicSortMultiple() {
/*
* save the arguments object as it will be overwritten
* note that arguments object is an array-like object
* consisting of the names of the properties to sort by
*/
var props = arguments;
return function (obj1, obj2) {
var i = 0, result = 0, numberOfProperties = props.length;
/* try getting a different result from 0 (equal)
* as long as we have extra properties to compare
*/
while(result === 0 && i < numberOfProperties) {
result = dynamicSort(props[i])(obj1, obj2);
i++;
}
return result;
}
}
// https://stackoverflow.com/a/22581382/3466839
function copyToClipboard(elem) {
// create hidden text element, if it doesn't already exist
var targetId = "_hiddenCopyText_";
var isInput = elem.tagName === "INPUT" || elem.tagName === "TEXTAREA";
var origSelectionStart, origSelectionEnd;
if (isInput) {
// can just use the original source element for the selection and copy
target = elem;
origSelectionStart = elem.selectionStart;
origSelectionEnd = elem.selectionEnd;
} else {
// must use a temporary form element for the selection and copy
target = document.getElementById(targetId);
if (!target) {
var target = document.createElement("textarea");
target.style.position = "absolute";
target.style.left = "-9999px";
target.style.top = "0";
target.id = targetId;
document.body.appendChild(target);
}
target.textContent = elem.textContent;
}
// select the content
var currentFocus = document.activeElement;
target.focus();
target.setSelectionRange(0, target.value.length);
// copy the selection
var succeed;
try {
succeed = document.execCommand("copy");
} catch(e) {
succeed = false;
}
// restore original focus
if (currentFocus && typeof currentFocus.focus === "function") {
currentFocus.focus();
}
if (isInput) {
// restore prior selection
elem.setSelectionRange(origSelectionStart, origSelectionEnd);
} else {
// clear temporary content
target.textContent = "";
}
return succeed;
}