//# sourceURL=J_LuaView1_UI7.js
/**
* J_LuaView1_UI7.js
* Configuration interface for LuaView.
*/
/* globals api,jsonp,jQuery,$,unescape,MultiBox,ace */
/* jshint multistr: true, laxcomma: true */
//"use strict"; // fails on UI7, works fine with ALTUI
var LuaView = (function(api, $) {
/* unique identifier for this plugin... */
var uuid = '7513412a-a7e8-11e8-afe3-74d4351650de';
var pluginVersion = "1.8develop-20353";
var myModule = {};
var serviceId = "urn:toggledbits-com:serviceId:LuaView1";
// var deviceType = "urn:schemas-toggledbits-com:device:LuaView:1";
var configModified = false;
var isOpenLuup = false;
var logTab = false;
var logAtBottom = false;
var logSeenEOF = false;
function D(m) {
console.log("J_LuaView1_UI7.js: " + m);
}
function initModule() {
D("jQuery version is " + String( jQuery.fn.jquery ) );
var ud = api.getUserData();
for (var i=0; i < ud.devices.length; ++i ) {
if ( ud.devices[i].device_type == "openLuup" ) {
isOpenLuup = true;
break;
}
}
logTab = false;
logAtBottom = false;
logSeenEOF = false;
}
/* Push header to document head */
function header() {
var html = "";
jQuery('head').append( ' ' );
var s = api.getDeviceState( api.getCpanelDeviceId(), serviceId, "LoadACE" ) || "1";
if ( "0" !== s && ! window.ace ) {
s = api.getDeviceState( api.getCpanelDeviceId(), serviceId, "ACEURL" ) || "";
if ( "" === s ) s = "https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.12/ace.js";
jQuery( "head" ).append( '' );
// jQuery( "head" ).append( '' );
// jQuery( "head" ).append( '' );
}
html = '';
jQuery('head').append( html );
}
/* Return footer */
function footer() {
var html = '';
html += '
';
html += '
Find LuaView useful? Please consider a small one-time donation to support this and my other plugins on
my web site . I am grateful for any support you choose to give!
';
html += '
';
return html;
}
/* Closing the control panel. */
function onBeforeCpanelClose(args) {
D( 'onBeforeCpanelClose args: ' + JSON.stringify(args) );
}
/* Return a Promise that resolves when Luup is reloaded and ready, as evidenced
by the functional state of the Reactor plugin's request handler. */
function waitForReloadComplete( msg ) {
return new Promise( function( resolve, reject ) {
var expire = Date.now() + 90000;
var dlg = false;
function tryAlive() {
$.ajax({
url: api.getDataRequestURL(),
data: {
id: "lr_LuaView",
action: "alive"
},
dataType: "json",
timeout: 5000
}).done( function( data ) {
if ( data && data.status ) {
if (dlg) $("#myModal").modal("hide");
resolve( true );
} else {
if ( ! $("#myModal").is(":visible") ) {
api.showCustomPopup( msg || "Waiting for Luup ready before operation...", { autoHide: false, category: 3 } );
dlg = true;
}
if ( Date.now() >= expire ) {
if (dlg) $("#myModal").modal("hide");
reject( "timeout" );
} else {
setTimeout( tryAlive, 2000 );
}
}
}).fail( function() {
if ( Date.now() >= expire ) {
if (dlg) $("#myModal").modal("hide");
reject( "timeout" );
} else {
if ( ! $("#myModal").is(":visible") ) {
api.showCustomPopup( msg || "Waiting for Luup ready before operation...", { autoHide: false, category: 3 } );
dlg = true;
}
setTimeout( tryAlive, 5000 );
}
});
}
tryAlive();
});
}
/* Swiped from Reactor, this function checks the Lua fragment */
function testLua( lua, row ) {
jQuery( 'div.tberrmsg', row ).remove();
$.ajax({
url: api.getDataRequestURL(),
method: 'POST', /* data could be long */
data: {
id: "lr_LuaView",
action: "testlua",
lua: lua
},
cache: false,
dataType: 'json',
timeout: 5000
}).done( function( data, statusText, jqXHR ) {
if ( data.status ) {
/* Good Lua */
return;
} else if ( data.status === false ) { /* specific false, not undefined */
jQuery( 'div.editor', row ).prepend( jQuery( '
' ).text( data.message || "Error in Lua" ) );
$( 'button.runlua', row ).prop( 'disabled', true );
}
}).fail( function( stat ) {
console.log("Failed to check Lua: " + stat);
});
}
function handleTextAreaChange( ev ) {
var url = api.getDataRequestURL();
var f = jQuery( ev.currentTarget );
var row = f.closest( 'div.row' );
row.addClass("modified");
var lua = f.val() || "";
lua = lua.replace( /\r\n/gm, "\n" ).replace( /\r/gm, "\n" ).trimEnd();
lua = unescape( encodeURIComponent( lua ) ); // Fanciness to keep UTF-8 chars well
testLua( lua, row );
var scene = row.attr( 'id' );
D("Changed " + scene);
if ( scene == "__startup" ) {
D("Posting startup lua change to " + url);
jQuery.ajax({
url: url,
method: "POST",
scriptCharset: "utf-8",
contentType: "application/x-www-form-urlencoded; charset=utf-8",
data: { id: "lr_LuaView", action: "saveStartupLua", lua: lua },
dataType: "text",
timeout: 5000
}).done( function( data, statusText, jqXHR ) {
if ( "OK" === data ) {
row.removeClass("modified");
$('button.runlua', row).prop( 'disabled', false );
if ( ! isOpenLuup ) {
try {
/* ??? Discovered/undocumented/unpublished */
api.application.sendCommandSaveUserData(true);
}
catch( e ) {
alert("Startup Lua saved, but you must hard-refresh your browser to get the UI to show it consistently.");
}
}
configModified = false;
} else {
alert("An error occurred while trying to save. Luup may be restarting. Try again in a moment.");
throw new Error( "Save returned: " + data );
}
}).fail( function( jqXHR, textStatus, errorThrown ) {
// Bummer.
D("Failed to load scene: " + textStatus + " " + String(errorThrown));
D(jqXHR.responseText);
alert("Save failed! Vera may be busy/restarting. Wait a moment, and try again.");
});
} else {
D("Loading scene data from " + url);
scene = parseInt( scene );
/* Query the scene as it currently is. */
jQuery.ajax({
url: url + "?id=scene&action=list&scene=" + scene,
dataType: "json",
timeout: 5000
}).done( function( data, statusText, jqXHR ) {
// Excellent.
D( "Loaded the scene, updating..." );
if ( lua == "" || isOpenLuup ) {
data.encoded_lua = 0;
data.lua = lua;
} else {
data.encoded_lua = 1;
data.lua = btoa( lua );
}
// Save it.
var ux = JSON.stringify( data );
/* PHR 2018-08-25: jQuery.post() fails on older firmware due to jQuery version/bug, but ajax()+method seems to work fine. */
jQuery.ajax({
url: url,
method: "POST",
scriptCharset: "utf-8",
contentType: "application/x-www-form-urlencoded; charset=utf-8",
data: { id: "scene", action: "create", json: ux },
dataType: "text",
timeout: 5000
}).done( function( data, statusText, jqXHR ) {
D("Save returns " + jqXHR.responseText);
if ( data == "OK" ) {
row.removeClass("modified");
$('button.runlua', row).prop( 'disabled', false );
if ( ! isOpenLuup ) {
try {
/* ??? Discovered/undocumented/unpublished */
api.application.sendCommandSaveUserData(true);
}
catch( e ) {
console.log( e );
}
}
configModified = false;
} else {
throw new Error( "Save replied: " + String(data) );
}
}).fail( function( jqXHR, textStatus, errorThrown ) {
D("Failed to save scene: " + textStatus + " " + String(errorThrown));
D(jqXHR.responseText);
alert("Save failed! Vera may be busy/restarting. Wait a moment, and try again.");
});
}).fail( function( jqXHR, textStatus, errorThrown ) {
// Bummer.
D("Failed to load scene: " + textStatus + " " + String(errorThrown));
D(jqXHR.responseText);
alert("Save failed! Vera may be busy/restarting. Wait a moment, and try again.");
});
}
}
function doTextArea( el, code ) {
var t = jQuery( '
' );
t.val( code || "" );
t.on( 'change.luaview', handleTextAreaChange );
el.append( t );
}
function handleEditorChange( editor, session, delta ) {
configModified = true;
var $row = jQuery( editor.container ).closest('div.row');
$row.addClass('modified');
$('button.runlua', $row).prop( 'disabled', true );
}
function handleEditorSave( editor, session, ev ) {
var url = api.getDataRequestURL();
var f = jQuery( ev.currentTarget );
var row = f.closest( 'div.row' );
row.addClass("modified");
var lua = session.getValue();
lua = lua.replace( /\r\n/gm, "\n" ).replace( /\r/gm, "\n" ).trimEnd();
lua = unescape( encodeURIComponent( lua ) ); // Fanciness to keep UTF-8 chars well
testLua( lua, row );
var scene = row.attr( 'id' );
D("Changed " + scene);
if ( scene == "__startup" ) {
D("Posting startup lua change to " + url);
jQuery.ajax({
url: url,
method: "POST",
data: { id: "lr_LuaView", action: "saveStartupLua", lua: lua },
dataType: "text",
timeout: 5000
}).done( function( data, statusText, jqXHR ) {
if ( "OK" === data ) {
row.removeClass("modified");
$( 'button.runlua', row ).prop( 'disabled', false );
if ( ! isOpenLuup ) {
try {
/* ??? Discovered/undocumented/unpublished */
api.application.sendCommandSaveUserData(true);
}
catch( e ) {
alert("Startup Lua saved, but you must hard-refresh your browser to get the UI to show it consistently.");
}
}
configModified = false;
} else {
throw new Error( "Save returned: " + data );
}
}).fail( function( jqXHR, textStatus, errorThrown ) {
// Bummer.
D("Failed to load scene: " + textStatus + " " + String(errorThrown));
D(jqXHR.responseText);
alert("Save failed! Vera may be busy/restarting. Wait a moment, and try again.");
});
} else {
D("Loading scene data from " + url);
scene = parseInt( scene );
/* Query the scene as it currently is. */
jQuery.ajax({
url: url + "?id=scene&action=list&scene=" + scene,
dataType: "json",
timeout: 5000
}).done( function( data, statusText, jqXHR ) {
// Excellent.
D( "Loaded the scene, updating..." );
if ( lua == "" || isOpenLuup ) {
data.encoded_lua = 0;
data.lua = lua;
} else {
data.encoded_lua = 1;
data.lua = btoa( lua );
}
// Save it.
var ux = JSON.stringify( data );
/* PHR 2018-08-25: jQuery.post() fails on older firmware due to jQuery version/bug, but ajax()+method seems to work fine. */
jQuery.ajax({
url: url,
method: "POST",
data: { id: "scene", action: "create", json: ux },
dataType: "text",
timeout: 5000
}).done( function( data, statusText, jqXHR ) {
D("Save returns " + jqXHR.responseText);
if ( data == "OK" ) {
row.removeClass("modified");
$( 'button.runlua', row ).prop( 'disabled', false );
if ( ! isOpenLuup ) {
try {
/* ??? Discovered/undocumented/unpublished */
api.application.sendCommandSaveUserData(true);
}
catch( e ) {
console.log( e );
}
}
configModified = false;
} else {
throw new Error( "Save replied: " + String(data) );
}
}).fail( function( jqXHR, textStatus, errorThrown ) {
D("Failed to save scene: " + textStatus + " " + String(errorThrown));
D(jqXHR.responseText);
alert("Save failed! Vera may be busy/restarting. Wait a moment, and try again.");
});
}).fail( function( jqXHR, textStatus, errorThrown ) {
// Bummer.
D("Failed to load scene: " + textStatus + " " + String(errorThrown));
D(jqXHR.responseText);
alert("Save failed! Vera may be busy/restarting. Wait a moment, and try again.");
});
}
}
function doEditor( el, code ) {
var editor = ace.edit( jQuery(el).get(0), {
minLines: 8,
maxLines: 32,
theme: "ace/theme/xcode",
mode: "ace/mode/lua",
fontSize: "16px",
tabSize: 4
});
/* Apply options from state if set */
var exopts = api.getDeviceState( api.getCpanelDeviceId(), serviceId, "AceOptions" ) || "";
if ( exopts !== "" ) {
try {
var opts = JSON.parse( exopts );
if ( opts !== undefined ) {
editor.setOptions( opts );
}
} catch( e ) {
alert("Can't apply your custom AceOptions: " + String(e));
}
}
var session = editor.session;
session.setValue( code );
editor.on( 'change', function( delta ) { handleEditorChange( editor, session, delta ); } );
editor.on( 'blur', function( ev ) { handleEditorSave( editor, session, ev ); } );
}
function handleReloadClick( ev ) {
var btn = jQuery( ev.target );
btn.prop( 'disabled', true );
api.showCustomPopup( "Reloading Luup...", { autoHide: false, category: 3 } );
setTimeout( function() {
api.performActionOnDevice( 0, "urn:micasaverde-com:serviceId:HomeAutomationGateway1", "Reload",
{ actionArguments: { Reason: "User-requested reload from LuaView UI" } } );
setTimeout( function() {
waitForReloadComplete().finally( function() {
$("#myModal").modal("hide");
btn.prop( 'disabled', false );
});
}, 5000 );
}, 2000 );
}
function handleBackupClick( ev ) {
var txt = '';
txt += '-- INSTRUCTIONS: RIGHT-CLICK in this window and choose "Save as..." to save this display as a backup of your Lua.';
txt += "\n-- To restore all or part later, just copy/paste from the saved file.";
txt += "\n-- Snapshot time is " + String( new Date() );
txt += "\n\n";
var ud = api.getUserData();
var code = ( parseInt(ud.encoded_lua || 0) ? atob( ud.StartupCode ) : ud.StartupCode ) || "";
if ( "" !== code ) {
code = decodeURIComponent(escape(code)); /* UTF-8 handling, reversal */
txt += "-- Startup Lua\n";
txt += code;
txt += "\n\n";
}
var scenes = api.cloneObject( ud.scenes );
for (var i=0; i
' ).text( txt );
$body.empty().append( $pre );
}, 1000 );
win.focus();
} else {
alert('Please allow popups for this interface.');
}
}
function updateDisplay( sort ) {
var ud = api.getUserData();
var list = jQuery("div#codelist");
list.empty();
if ( sort === undefined ) {
sort = api.getDeviceState( api.getCpanelDeviceId(), serviceId, "Sort" ) || "name,asc";
}
var sortopt = sort.split(/,/);
if ( sortopt.length < 2 ) {
sortopt = [ "name", "asc" ];
}
var el = jQuery('
');
el.append('Sort by: Name ID ascending descending
');
jQuery('select#sortby', el).val( sortopt[0] );
jQuery('select#sortasc', el).val( sortopt[1] );
jQuery('select', el).on( 'change', handleSortChange );
var col = jQuery( '
' )
.appendTo( el );
jQuery( 'Back Up Lua ' )
.on( 'click.luaview', handleBackupClick )
.appendTo( col );
jQuery( 'Reload Luup ' )
.on( 'click.luaview', handleReloadClick )
.appendTo( col );
list.append(el);
var code = ( parseInt(ud.encoded_lua || 0) ? atob( ud.StartupCode ) : ud.StartupCode ) || "";
code = decodeURIComponent(escape(code)); /* UTF-8 handling, reversal */
el = jQuery('
');
el.attr('id', '__startup');
el.append('Startup Lua
');
el.append('
');
if ( ! window.ace ) {
doTextArea( jQuery("div.editor", el), code );
} else {
jQuery( 'div.editor', el ).append( '
' );
doEditor( jQuery("div.luacode", el), code );
}
list.append(el);
var scenes = api.cloneObject( ud.scenes );
scenes.sort(
function( a, b ) {
if ( sortopt[0] == "id" ) {
if ( sortopt[1] == 'desc' ) {
return a.id < b.id ? 1 : -1;
} else {
return a.id < b.id ? -1 : 1;
}
} else {
if ( sortopt[1] == 'desc' ) {
return a.name.toLowerCase() < b.name.toLowerCase() ? 1 : -1;
} else {
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
}
}
}
);
for (var i=0; i ');
el.attr('id', scenes[i].id);
el.append('Loading... please wait...
';
html += footer();
api.setCpanelContent( html );
waitForAce( Date.now() );
}
var logTask = false;
var chunkSize = 250;
function loadNextLogChunk( tries ) {
var container = jQuery( 'div#tblogdata' );
if ( !logTab || 0 === container.length ) {
logTab = false;
console.log("Log tab no longer foreground/available.");
return;
}
tries = tries || 1;
if ( tries > 10 ) {
$( "div.tbloadstatus", container ).text( "Too many errors. Giving up." );
return;
}
var lastline = parseInt( container.data( 'lastline' ) || 0 );
if ( isNaN(lastline) ) lastline = 0;
if ( 0 === lastline ) {
logSeenEOF = false;
}
// $( "div.tbloadstatus", container ).text( "Fetching log data" + ( lastline > 0 ? ( " after " + lastline ) : "" ) );
jQuery.ajax( {
url: api.getDataRequestURL(),
data: {
id: "lr_LuaView",
action: "log",
first: ( lastline + 1 ),
count: chunkSize,
r: Math.random()
},
dataType: "text",
timeout: 15000
}).done( function( data ) {
if ( ! data.match( /^\$LOC:/ ) ) {
console.log("Invalid response data: "+data.substring(0,255));
$( "div.tbloadstatus", container ).text("Error... retrying... " + String(tries));
if ( !logTask ) {
logTask = window.setTimeout( function () {
logTask = false;
loadNextLogChunk( tries+1 );
}, 10000 );
}
return;
}
data = data.replace( /\r\n/g, "\n" ).replace( /\r/g, "\n" ).replace( /[\u2028\u2029]/g, "\n" );
data = data.replace( /^\$LOC:[^\n]*\n/, "" );
data = data.replace( /&/, "&" ).replace( /[<]/g, "<" ).replace( /[>]/g, ">" );
data = data.replace( /\x1b\[(\d+);1m(.*)\x1b\[0m/g, function( m, p1, p2 ) {
return '