//# sourceURL=J_ReactorSensor_UI7.js
/**
* J_ReactorSensor_UI7.js
* Configuration interface for ReactorSensor
*
* Copyright 2018-2022 Patrick H. Rigney, All Rights Reserved.
* This file is part of Reactor. Use subject to license; please see
* license details at https://www.toggledbits.com/static/reactor/docs/Installation#license-and-use-restrictions
*
*/
/* globals api,jQuery,ace,MultiBox,ALTUI_revision,Promise,escape,unescape */
/* jshint browser: true, devel: true, multistr: true, laxcomma: true, undef: true, unused: false */
//"use strict"; // fails on UI7, works fine with ALTUI
var ReactorSensor = (function(api, $) {
/* unique identifier for this plugin... */
var uuid = '21b5725a-6dcd-11e8-8342-74d4351650de';
var pluginVersion = "3.11 (22314)";
var _UIVERSION = 22314; /* must coincide with Lua core */
var _CDATAVERSION = 20045; /* must coincide with Lua core */
var DEVINFO_MINSERIAL = 482;
var _DOCURL = "https://www.toggledbits.com/static/reactor/docs/3.9/";
var _FORUMURL = "https://community.getvera.com/c/plugins-and-plugin-development/reactor/178";
var _MIN_ALTUI_VERSION = 2536;
var _MAX_ALTUI_VERSION = 2553;
var myModule = {};
var serviceId = "urn:toggledbits-com:serviceId:ReactorSensor";
var deviceType = "urn:schemas-toggledbits-com:device:ReactorSensor:1";
var moduleReady = false;
var needsRestart = false;
var iData = [];
var roomsByName = false;
var actions = {};
var deviceActionData = {};
var deviceInfo = {};
var userIx = {};
var userNameIx = {};
var configModified = false;
var inStatusPanel = false;
var spyDevice = false;
var lastx = 0;
var isOpenLuup = false;
var isALTUI = false;
var devVeraAlerts = false;
var devVeraTelegram = false;
var dateFormat = "%F"; /* ISO8601 defaults */
var timeFormat = "%T";
var unsafeLua = true;
var onBeforeCpanelClose; /* forward declaration */
var condTypeName = {
"comment": "Comment",
"service": "Device State",
"housemode": "House Mode",
"weekday": "Weekday",
"sun": "Sunrise/Sunset",
"trange": "Date/Time",
"interval": "Interval",
"ishome": "Geofence",
"reload": "Luup Reloaded",
"grpstate": "Group State",
"var": "Expression Value",
"group": "Group"
};
/* Note: default true for the following: hold, pulse, latch */
var condOptions = {
"group": { sequence: true, duration: true, repeat: true },
"service": { sequence: true, duration: true, repeat: true },
"housemode": { sequence: true, duration: true, repeat: true },
"weekday": { },
"sun": { sequence: true },
"trange": { },
"interval": { pulse: false, latch: false },
"ishome": { sequence: true, duration: true },
"reload": { },
"grpstate": { sequence: true, duration: true, repeat: true },
"var": { sequence: true, duration: true, repeat: true }
};
var weekDayName = [ '?', 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ];
var monthName = [ '?', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ];
var opName = { "bet": "between", "nob": "not between", "after": "after", "before": "before" };
var houseModeName = [ '?', 'Home', 'Away', 'Night', 'Vacation' ];
var inttypes = {
"ui1": { min: 0, max: 255 }, "i1": { min: -128, max: 127 },
"ui2": { min: 0, max: 65535 }, "i2": { min: -32768, max: 32767 },
"ui4": { min: 0, max: 4294967295 }, "i4": { min: -2147483648, max: 2147483647 },
"int": { min: -2147483648, max: 2147483647 }
};
var serviceOps = [
{ op: '=', desc: 'equals', args: 1, optional: 1 },
{ op: '<>', desc: 'not equals', args: 1, optional: 1 },
{ op: '<', desc: '<', args: 1, numeric: 1, },
{ op: '<=', desc: '<=', args: 1, numeric: 1, },
{ op: '>', desc: '>', args: 1, numeric: 1, },
{ op: '>=', desc: '>=', args: 1, numeric: 1, },
{ op: 'bet', desc: 'between', args: 2, numeric: 1, format: "%1 and %2" },
{ op: 'nob', desc: 'not between', args: 2, numeric: 1, format: "%1 and %2" },
{ op: 'starts', desc: 'starts with', args: 1, },
{ op: 'notstarts', desc: 'does not start with', args: 1, },
{ op: 'ends', desc: 'ends with', args: 1, },
{ op: 'notends', desc: 'does not end with', args: 1, },
{ op: 'contains', desc: 'contains', args: 1, },
{ op: 'notcontains', desc: 'does not contain', args: 1, },
{ op: 'in', desc: 'in', args: 1 },
{ op: 'notin', desc: 'not in', args: 1 },
{ op: 'istrue', desc: 'is TRUE', args: 0, nocase: false },
{ op: 'isfalse', desc: 'is FALSE', args: 0, nocase: false },
{ op: 'isnull', desc: 'is NULL', args: 0, nocase: false },
{ op: 'change', desc: 'changes', args: 2, format: "from %1 to %2", optional: 2, blank: "(any)" },
{ op: 'update', desc: 'updates', args: 0, nocase: false }
];
var varRefPattern = /^\{([^}]+)\}\s*$/;
var notifyMethods = [
{ id: "", name: "Vera-native" }
, { id: "SM", name: "SMTP Mail", users: false, extra: [
{ id: "recipient", label: "Recipient(s):", placeholder: "blank=default recipient; comma-separate multiple", optional: true },
{ id: "subject", label: "Subject:", placeholder: "blank=this ReactorSensor's name", optional: true }
], config: { name: "SMTPServer" } }
, { id: "PR", name: "Prowl", users: false, requiresUnsafeLua: true, extra: [
{ id: "priority", label: "Priority:", type: "select", default: "0", values: [ "-2=Very low", "-1=Low", "0=Normal", "1=High", "2=Emergency" ] }
], config: { name: "ProwlAPIKey" } }
, { id: "PO", name: "Pushover", users: false, requiresUnsafeLua: true, extra: [
{ id: "title", label: "Message Title", placeholder: "blank=this ReactorSensor's name", default: "", optional: true },
{ id: "podevice", label: "Device:", placeholder: "optional", default: "", optional: true },
{ id: "priority", label: "Priority:", type: "select", default: "0", values: [ "-2=Very low", "-1=Low", "0=Normal", "1=High" ] }, /* 2=Emergency doesn't seem to work, no alert is received 2020-09-23 */
{ id: "sound", label: "Sound:", Xtype: "select", default: "", optional: true, placeholder: "blank=device default; select from list or enter your own value",
datalist: [
"=(device default)", "none=(none/silent)", "vibrate=(vibrate only)", "pushover=Pushover",
"bike=Bike", "bugle=Bugle", "cashregister=Cash Register", "classical=Classical", "cosmic=Cosmic", "falling=Falling",
"gamelan=Gamelan", "incoming=Incoming", "intermission=Intermission", "magic=Magic", "mechanical=Mechanical",
"pianobar=Piano Bar", "siren=Siren", "spacealarm=Space Alarm", "tugboat=Tug Boat", "alien=Alien Alarm (long)",
"climb=Climb (long)", "persistent=Persistent (long)", "echo=Pushover Echo (long)", "updown=Up Down (long)"
]
},
{ id: "token", label: "Pushover Token:", placeholder: "blank=from Reactor config", default:"", optional: true }
], config: { name: "PushoverUser" } }
, { id: "SD", name: "Syslog", users: false, extra: [
{ id: "hostip", label: "Syslog Server IP:", placeholder: "Host IP4 Address", validpattern: "^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$" },
{ id: "facility", label: "Facility:", type: "select", default: "23", values: [ "0=kern","1=user","2=mail","3-daemon","4=auth","5=syslog","6=lp","7=news","8=uucp","9=clock","10=security","11=FTP","12=NTP","13=audit","14=alert","16=local0","17=local1","18=local2","19=local3","20=local4","21=local5","22=local6","23=local7" ] },
{ id: "severity", label: "Severity:", type: "select", default: "5", values: [ "0=emerg","1=alert","2=crit","3=err","4=warn","5=notice","6=info","7=debug" ] }
] }
, { id: "UU", name: "User URL", users: false, requiresUnsafeLua: true, extra: [
{ id: "url", label: "URL:", type: "textarea", placeholder: "URL", validpattern: "^https?://", default: "http://localhost/alert?message={message}", fullwidth: true }
] }
, { id: "VA", name: "VeraAlerts" }
, { id: "VT", name: "VeraTelegram", users: false, extra: [
{ id: "imageurl", label: "Image URL:", type: "textarea", placeholder: "URL", validpattern: "^https?://", default: "", optional: true, fullwidth: true },
{ id: "videourl", label: "Video URL:", type: "textarea", placeholder: "URL", validpattern: "^https?://", default: "", optional: true, fullwidth: true },
{ id: "chatid", label: "Chat ID:", default: "", optional: true },
{ id: "disablenotification", label: "Disable Notification:", type: "select", values: [ "False=No", "True=Yes" ] }
] }
];
var msgUnsavedChanges = "You have unsaved changes! Press OK to save them, or Cancel to discard them.";
var msgGroupIdChange = "Click to change group name";
var msgOptionsShow = "Show condition options";
var msgOptionsHide = "Hide condition options";
var msgRemoteAlert = "You appear to be using remote access for this session. Editing of ReactorSensor configurations via remote access is possible, but not recommended due to the latency and inconsistency of cloud connections and infrastructure. You may experience issues, particularly when saving large configurations. Using local access exclusively is strongly recommended. It is also a good idea to back up your ReactorSensors (using the Backup/Restore tab in the Reactor master device) prior to editing via remote access.";
var NULLCONFIG = { conditions: {} };
/* Insert the header items */
/* Checkboxes, see https://codepen.io/VoodooSV/pen/XoZJme */
function header() {
if ( 0 !== $( 'style#reactor-core-styles' ).length ) return;
/* Load material design icons */
var $head = $( 'head' );
$head.append('');
$head.append( '\
');
if ( isALTUI ) {
$head.append( '' );
} else {
/* Vera */
$head.append( '' );
}
}
/* Return footer */
function footer() {
var html = '';
html += '
';
html += '
Find Reactor 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!
';
} catch( e ) {}
return html;
}
function checkRemoteAccess() {
// return !isOpenLuup && null === api.getDataRequestURL().match( /^https?:\/\/(\d+)\.(\d+)\.(\d+)\.(\d+)/ );
return false; /* LATER 3.5 2019-12-02... not yet, let's see how other var changes work out */
}
/* Create an ID that's functionally unique for our purposes. */
function getUID( prefix ) {
/* Not good, but good enough. */
var newx = Date.now() - 1529298000000;
if ( newx <= lastx ) newx = lastx + 1;
lastx = newx;
return ( prefix === undefined ? "" : prefix ) + newx.toString(36);
}
function isEmpty( s ) {
return undefined === s || null === s || "" === s ||
( "string" === typeof( s ) && null !== s.match( /^\s*$/ ) );
}
function quot( s ) {
return JSON.stringify( s );
}
/* Remove special characters that disrupt JSON processing on Vera (dkjson 1.2 in particular */
/* Ref http://dkolf.de/src/dkjson-lua.fsl/home (see 1.2 comments) */
/* Ref https://docs.microsoft.com/en-us/openspecs/ie_standards/ms-es3/def92c0a-e69f-4e5e-8c5e-9f6c9e58e28b */
function purify( s ) {
return "string" !== typeof(s) ? s :
s.replace(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/g, "");
/* or... s.replace( /[\u007F-\uFFFF]/g, function(ch) { return "\\u" + ("0000"+ch.charCodeAt(0).toString(16)).substr(-4); } ) */
}
function hasAnyProperty( obj ) {
// assert( "object" === typeof( obj );
if ( "object" === typeof( obj ) ) {
for ( var p in obj ) {
if ( obj.hasOwnProperty( p ) ) return true;
}
}
return false;
}
/* Find a value in an array using a function to match; returns value, not index. */
function arrayFindValue( arr, func, start ) {
var l = arr.length;
for ( var k=(start || 0); k' ).val( val ).text( txt || ( val + '? (missing)' ) );
$mm.addClass( "tberror" ).append( $opt );
}
val = $opt.val(); /* actual value now */
$mm.val( val );
return val;
}
/** getWiki - Get (as jQuery) a link to Wiki for topic */
function getWiki( where ) {
var $v = $( '', {
"class": "tbdocslink",
"alt": "Link to documentation for topic",
"title": "Link to documentation for topic",
"target": "_blank",
"href": _DOCURL + String(where || "")
} );
$v.append( 'help_outline' );
return $v;
}
/* Return value or default if undefined */
function coalesce( v, d ) {
return ( null === v || undefined === v ) ? d : v;
}
/* Evaluate input string as integer, strict (no non-numeric chars allowed other than leading/trailing whitespace, empty string fails). */
function getInteger( s ) {
s = String(s).trim().replace( /^\+/, '' ); /* leading + is fine, ignore */
if ( s.match( /^-?[0-9]+$/ ) ) {
return parseInt( s );
}
return NaN;
}
/* Like getInteger(), but returns dflt if no value provided (blank/all whitespace) */
function getOptionalInteger( s, dflt ) {
if ( /^\s*$/.test( String(s) ) ) {
return dflt;
}
return getInteger( s );
}
function getDeviceFriendlyName( dev, devobj ) {
if ( -1 === dev ) return '(self)';
devobj = devobj || api.getDeviceObject( dev );
if ( ! devobj ) {
console.log( "getDeviceFriendlyName() dev=(" + typeof(dev) + ")" + String(dev) + ", devobj=(" + typeof(devobj) + ")" + String(devobj) + ", returning false" );
return false;
}
return String(devobj.name) + " (#" + String(devobj.id) + ")";
}
/* Get parent state */
function getParentState( varName, myid ) {
var me = api.getDeviceObject( myid || api.getCpanelDeviceId() );
return api.getDeviceState( me.id_parent || me.id, "urn:toggledbits-com:serviceId:Reactor", varName );
}
/* Set parent state */
function setParentState( varName, val, myid ) {
var me = api.getDeviceObject( myid || api.getCpanelDeviceId() );
return api.setDeviceStatePersistent( me.id_parent || me.id, "urn:toggledbits-com:serviceId:Reactor", varName, val );
}
function checkUpdate() {
if ( isOpenLuup ) {
return Promise.resolve( false );
}
return new Promise( function( resolve, reject ) {
$.ajax({
url: api.getDataRequestURL(),
data: {
id: "lr_Reactor",
action: "updateplugin",
r: Math.random()
},
dataType: "json",
timeout: 15000,
cache: false
}).fail( function( /* jqXHR, textStatus, errorThrown */ ) {
reject();
}).done( function( data ) {
if ( !data.status ) {
reject( data.message );
return;
}
var newest = false;
for ( var j=0; j= 1620345600000 ) { // 2021-05-07T00:00:00Z
rel.published_at = pubtime;
if ( !newest || pubtime > newest.published_at ) {
newest = rel;
}
}
}
}
/* Now see if newest is not current */
if ( newest ) {
var st = getParentState( "grelease", false ) || "";
var r = st.split( /\|/ );
if ( r.length > 0 && r[0] == String(newest.id) ) {
/* Installed version is current version */
newest = false;
}
}
resolve( newest );
});
});
}
/* Get data for this instance */
function getInstanceData( myid ) {
myid = myid || api.getCpanelDeviceId();
iData[ myid ] = iData[ myid ] || {};
return iData[ myid ];
}
/* Generate an inline checkbox. */
function getCheckbox( id, value, label, classes, help ) {
var $div = $( '' );
if ( isALTUI ) {
$div.removeClass().addClass( 'form-check' );
$('').attr( { type: 'checkbox', id: id } )
.val( value )
.addClass( 'form-check-input' )
.addClass( classes || "" )
.appendTo( $div );
$('').attr( 'for', id )
.addClass( 'form-check-label' )
.html( label )
.appendTo( $div );
} else {
$( '' ).attr( 'id', id ).val( value )
.addClass( classes || "" )
.appendTo( $div );
$( '' ).attr( 'for', id ).html( label )
.appendTo( $div );
}
if ( help ) {
getWiki( help ).appendTo( $div );
}
return $div;
}
/* Generate an inline radio button */
function getRadio( name, ix, value, label, classes ) {
var $div;
if ( isALTUI ) {
$div = $( '' ).addClass( 'form-check' );
$('').attr( { type: 'radio', id: name + ix, name: name } )
.val( value )
.addClass( 'form-check-input' )
.addClass( classes || "" )
.appendTo( $div );
$('').attr( 'for', name + ix )
.addClass( 'form-check-label' )
.html( label )
.appendTo( $div );
} else {
$div = $( '' )
.html( label );
$( '' )
.attr( { id: name+ix, name: name } )
.val( value )
.addClass( classes || "" )
.prependTo( $div );
}
return $div;
}
/**
* ALTUI: Inconsistencies between versions of UI7, and intra- and inter-ALTUI,
* have to be resolved with some logic.
*/
function hideModal() {
if ( api.hideMessagePopup ) {
api.hideMessagePopup();
} else {
/* Sigh. Do both on ALTUI, in case amg0 fixes his dialogs some day. */
if ( isALTUI ) {
$( 'div#showMessagePopup' ).modal( 'hide' );
}
$( 'div#myModal' ).modal( 'hide' );
}
}
/**
* ALTUI doesn't implement on_cpanel_close event. Use tab behavior.
* Pass any element on the current tab.
*/
function captureControlPanelClose( $el ) {
if ( isALTUI ) {
/** On ALTUI, we break plugin encapsulation by necessity and use
* its tab handler to help us.
*/
var paneId = $el.closest( '.tab-pane' ).attr( 'id' );
$( 'a[href="#' + paneId + '"]' ).off( 'hide.bs.tab.reactor' )
.on( 'hide.bs.tab.reactor', function() {
console.log("captureControlPanelClose() ALTUI module tab hide handler");
onBeforeCpanelClose();
});
$( 'a[href="#altui-toggle-control-panel"]' ).off( 'hide.bs.tab.reactor' )
.on( 'hide.bs.tab.reactor', function() {
console.log("ALTUI control panel hide handler");
onBeforeCpanelClose();
});
} else {
api.registerEventHandler('on_ui_cpanel_before_close', ReactorSensor, 'onBeforeCpanelClose');
}
}
/* Load configuration data. As of 3.5, we do not do any updates here. */
function loadConfigData( myid ) {
var me = api.getDeviceObject( myid );
if ( ! ( me && deviceType === me.device_type ) ) {
throw "Device " + String(myid) + " not found or incorrect type";
}
// PHR??? Dynamic false needs more testing. Save/update of local/lustatus should be sufficient
/* Empty configs are not allowed, but happen when the Vera UI gets wildly out of sync with Vera,
which has happened increasingly since 7.29. */
var s = api.getDeviceState( myid, serviceId, "cdata" /* , { dynamic: false } */ ) || "";
if ( isEmpty( s ) ) {
console.log( "ReactorSensor " + myid + ": EMPTY DATA" );
alert( 'Reactor has detected that the Vera UI may be badly out of sync with the Vera itself. To remedy this, please (1) reload Luup or reboot your Vera, and then (2) do a "hard-refresh" of your browser (refresh with cache flush). Do not edit any devices or do anything else until this issue has been remedied.' );
throw "empty configuration";
} else if ( "###" === s ) {
alert( 'Please go back out to the device list and make sure this ReactorSensor is ENABLED before re-entering configuration.' );
throw "reset configuration";
}
var cdata;
try {
cdata = JSON.parse( s );
/* Old Luup's json library doesn't support __jsontype metadata,
so fix up empty objects, which it renders as empty arrays. */
if ( cdata.variables && Array.isArray( cdata.variables ) && cdata.variables.length == 0 ) {
console.log("Fixing cdata.variables from array to object");
cdata.variables = {};
}
if ( cdata.activities && Array.isArray( cdata.activities ) && cdata.activities.length == 0 ) {
console.log("Fixing cdata.activities from array to object");
cdata.activities = {};
}
} catch (e) {
console.log("Unable to parse cdata: " + String(e));
throw e;
}
/* Special version check */
if ( ( cdata.version || 0 ) > _CDATAVERSION ) {
alert("This ReactorSensor configuration is an unsupported format/version " +
String( cdata.version ) + " for this version of Reactor (" +
pluginVersion + " " + _CDATAVERSION + "). If you've downgraded Reactor from a later " +
"version, you need to restore a backup of this ReactorSensor's configuration made " +
"from the earlier version.");
console.log("The configuration for this ReactorSensor is an unsupported format/version (" +
String( cdata.version ) + "). Upgrade Reactor or restore an older config from backup.");
throw "Incompatible configuration format/version";
}
/* Check for upgrade tasks from prior versions */
delete cdata.undefined;
if ( undefined === cdata.variables ) {
/* Fixup v2 */
cdata.variables = {};
}
if ( undefined === cdata.activities ) {
cdata.activities = {};
}
if ( undefined === cdata.conditions.root ) {
var root = { id: "root", name: api.getDeviceObject( myid ).name, type: "group", operator: "and", conditions: [] };
cdata.conditions = { root: root };
}
/* Update device */
cdata.device = myid;
/* Store config on instance data */
var d = getInstanceData( myid );
d.cdata = cdata;
delete d.ixCond; /* Remove until needed/rebuilt */
configModified = false;
return cdata;
}
/* Get configuration; load if needed */
function getConfiguration( myid, force ) {
myid = myid || api.getCpanelDeviceId();
var d = getInstanceData( myid );
if ( force || ! d.cdata ) {
try {
loadConfigData( myid );
console.log("getConfiguration(): loaded config serial " + String(d.cdata.serial) + ", timestamp " + String(d.cdata.timestamp));
} catch ( e ) {
console.log("getConfiguration(): can't load config for "+myid+": "+String(e));
console.log(e);
return false;
}
} else {
console.log("getConfiguration(): returning cached config serial " + String(d.cdata.serial));
}
return d.cdata;
}
/* Get condition index; build if needed (used by Status and Condition tabs) */
function getConditionIndex( myid ) {
var d = getInstanceData( myid );
if ( undefined === d.ixCond ) {
var cf = getConfiguration( myid );
d.ixCond = {};
var makeix = function( grp, level ) {
d.ixCond[grp.id] = grp;
grp.__depth = level; /* assigned to groups only */
for ( var ix=0; ix<(grp.conditions || []).length; ix++ ) {
grp.conditions[ix].__parent = grp;
grp.conditions[ix].__index = ix;
d.ixCond[grp.conditions[ix].id] = grp.conditions[ix];
if ( "group" === ( grp.conditions[ix].type || "group" ) ) {
makeix( grp.conditions[ix], level+1 );
}
}
};
makeix( cf.conditions.root || {}, 0 );
}
return d.ixCond;
}
function getConditionStates( myid ) {
myid = myid || api.getCpanelDeviceId();
var s = api.getDeviceState( myid, serviceId, "cstate" ) || "";
var cstate = {};
if ( ! isEmpty( s ) ) {
try {
cstate = JSON.parse( s );
return cstate;
} catch (e) {
console.log("cstate cannot be parsed: " + String(e));
}
} else {
console.log("cstate unavailable");
}
/* Return empty cstate structure */
return { vars: {} };
}
/* Generic filter for DOtraverse to return groups only */
function isGroup( node ) {
return "group" === ( node.type || "group" );
}
/* Traverse - pre-order */
function DOtraverse( node, op, args, filter ) {
if ( node ) {
if ( ( !filter ) || filter( node ) ) {
op( node, args );
}
if ( "group" === ( node.type || "group" ) ) {
var l = node.conditions ? node.conditions.length : 0;
for ( var ix=0; ix' +
" ERROR! The Reactor plugin core version and UI version do not agree." +
" This may cause errors or corrupt your ReactorSensor configuration." +
" Please hard-reload your browser and try again " +
' (how?).' +
" If you have installed hotfix patches, you may not have successfully installed all required files." +
" Expected " + String(_UIVERSION) + " got " + String(s) +
".
" );
return false;
}
if ( undefined === Promise ) {
alert( "Warning! The browser you are using does not support features required by this interface. The recommended browsers are Firefox, Chrome, Safari, and Edge. If you are using a modern version of one of these browsers and getting this message, please report to rigpapa via the Vera Community forums." );
return false;
}
if ( isALTUI ) {
console.log("initModule() supported ALTUI versions:",_MIN_ALTUI_VERSION,"to",_MAX_ALTUI_VERSION);
var validALTUI = false;
var av;
var av_range = String(_MIN_ALTUI_VERSION) + " to " + String(_MAX_ALTUI_VERSION);
if ( "undefined" !== typeof ALTUI_revision ) {
av = ALTUI_revision.match( /: *(\d+)/ );
if ( null !== av ) {
av = parseInt( av[1] );
console.log("initModule(): ALTUI release",av,"from ALTUI_revision");
if ( !isNaN( av ) && av >= _MIN_ALTUI_VERSION && av <= _MAX_ALTUI_VERSION ) {
validALTUI = true;
}
}
}
if ( !validALTUI ) {
alert("The running version of ALTUI has not been confirmed to be compatible with this version of the Reactor UI and is therefore not supported. Incompatibilities may cause loss of functionality or errors that result in data/configuration loss, and it is recommended that you up/downgrade to a compatible version of ALTUI before continuing.\n\nSupported versions are: " + av_range + ".\nYou are running " + String( ALTUI_revision ));
return false;
}
try {
var jq = String($.fn.jquery).split('.');
jq = parseInt( jq[0] ) * 100 + parseInt( jq[1] );
if ( jq >= 306 ) {
alert("This version of the Reactor UI does not support the version of jQuery you are using on this system. Typically, the jQuery version is determined by the ALTUI version being used, and if that version of ALTUI is supported, its default jQuery is as well. Overriding ALTUI's default to a higher version can result in incompatibilities that could result in configuration corruption.\n\nNot supported: jQuery "+String($.fn.jquery));
return false;
}
} catch( e ) {
console.log("initModule() error checking jQuery version:",e);
}
}
/* Load ACE. */
s = getParentState( "UseACE", myid ) || "1";
if ( "1" === s && ! window.ace ) {
s = getParentState( "ACEURL" ) || "https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.12/ace.js";
$( "head" ).append( '' );
}
/* Initialize for instance */
console.log("initModule() initializing instance data for " + myid);
iData[myid] = iData[myid] || {};
getConfiguration( myid );
/* Force this false every time, and make the status panel change it. */
inStatusPanel = false;
return true;
}
/**
* Return list of devices sorted alpha by room sorted alpha with "no room"
* forced last. Store this for future returns; deferred effort/memory use.
*/
function getSortedDeviceList() {
if ( roomsByName ) return roomsByName;
var myid = api.getCpanelDeviceId();
var devices = api.cloneObject( api.getListOfDevices() );
var noroom = { "id": 0, "name": "No Room", "devices": [] };
var rooms = [ noroom ];
var roomIx = {};
roomIx[String(noroom.id)] = noroom;
var dd = devices.sort( function( a, b ) {
if ( a.id == myid ) return -1;
if ( b.id == myid ) return 1;
if ( a.name.toLowerCase() === b.name.toLowerCase() ) {
return a.id < b.id ? -1 : 1;
}
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
});
var l = dd.length;
for (var i=0; i b.name.toLowerCase() ? 1 : -1;
}
);
return roomsByName;
}
/* zero-fill */
function fill( s, n, p ) {
if ( "string" !== typeof(s) ) {
s = String(s);
}
p = p || "0";
var l = n - s.length;
while ( l-- > 0 ) {
s = p + s;
}
return s;
}
/* Format timestamp to string (models strftime) */
function ftime( t, fmt ) {
var dt = new Date();
dt.setTime( t );
var str = fmt || dateFormat;
str = str.replace( /%(.)/g, function( m, p ) {
switch( p ) {
case 'Y':
return String( dt.getFullYear() );
case 'm':
return fill( dt.getMonth()+1, 2 );
case 'd':
return fill( dt.getDate(), 2 );
case 'H':
return fill( dt.getHours(), 2 );
case 'I':
var i = dt.getHours() % 12;
if ( 0 === i ) i = 12;
return fill( i, 2 );
case 'p':
return dt.getHours() < 12 ? "AM" : "PM";
case 'M':
return fill( dt.getMinutes(), 2 );
case 'S':
return fill( dt.getSeconds(), 2 );
case '%':
return '%';
case 'T':
return ftime( t, "%H:%M:%S" );
case 'F':
return ftime( t, "%Y-%m-%d" );
default:
return m;
}
});
return str;
}
function textDateTime( y, m, d, hh, mm, isEnd ) {
hh = parseInt( hh || "0" );
mm = parseInt( mm || "0" );
var tstr = ( hh < 10 ? '0' : '' ) + hh + ':' + ( mm < 10 ? '0' : '' ) + mm;
/* Possible forms are YMD, MD, D with time, or just time */
if ( isEmpty( m ) ) {
if ( isEmpty( d ) ) {
return tstr;
}
return tstr + " on day " + d + " of each month";
}
m = parseInt( m );
return monthName[m] + ' ' + d + ( isEmpty( y ) ? '' : ' ' + y ) + ' ' + tstr;
}
/**
* Convert Lua timestamp (secs since Epoch) to text; if within 24 hours,
* show time only.
*/
function shortLuaTime( dt ) {
if ( ( dt || 0 ) === 0 ) {
return "";
}
var dtms = dt * 1000;
var ago = Date.now() - dtms;
if ( ago < 86400000 ) {
return ftime( dtms, timeFormat );
}
return ftime( dtms, dateFormat + " " + timeFormat );
}
/**
* Delete a state variable (with callback). Note that failing to delete a
* variable isn't fatal, as we get ample opportunities to try again later.
*/
function deleteStateVariable( devnum, serviceId, variable, fnext ) {
console.log("deleteStateVariable: deleting " + devnum + "." + serviceId + "/" + variable);
$.ajax({
url: api.getDataRequestURL(),
data: {
id: "variableset",
DeviceNum: devnum,
serviceId: serviceId,
Variable: variable,
Value: "",
r: Math.random()
},
dataType: "text",
cache: false,
timeout: 5000
}).fail( function( /* jqXHR, textStatus, errorThrown */ ) {
console.log( "deleteStateVariable: failed, maybe try again later" );
}).always( function() {
console.log("deleteStateVariable: finished, calling next");
if ( fnext ) { fnext(); }
});
}
/**
* Attempt to remove state variables that are no longer used.
*/
function clearUnusedStateVariables( myid, cdata ) {
if ( isOpenLuup ) return; /* Can't delete state vars on openLuup */
var ud = api.getUserData();
var dx = api.getDeviceIndex( myid );
var deletes = [];
var myinfo = ud.devices[dx];
if ( undefined == myinfo ) return;
/* N.B. ixCond will be present in the condition editor only */
var ixCond = getConditionIndex( myid );
var l = myinfo.states.length;
for ( var ix=0; ix= expire ) {
if (dlg) {
hideModal();
}
reject( "timeout" );
} else {
setTimeout( tryAlive, 2000 );
}
}
}).fail( function() {
if ( Date.now() >= expire ) {
if (dlg) {
hideModal();
}
reject( "timeout" );
} else {
if ( ! dlg ) {
api.showCustomPopup( msg || "Waiting for Luup ready before operation...", { autoHide: false, category: 3 } );
dlg = true;
}
setTimeout( tryAlive, 5000 );
}
});
}
tryAlive();
});
}
function saveConfiguration( myid, successFunc, failFunc ) {
/* Save to persistent state */
myid = myid || api.getCpanelDeviceId();
var cdata = getConfiguration( myid );
cdata.timestamp = Math.floor( Date.now() / 1000 );
cdata.serial = ( cdata.serial || 0 ) + 1;
cdata.device = myid;
console.log("saveConfiguration(): saving #" + myid + " config serial " + String(cdata.serial) + ", timestamp " + String(cdata.timestamp));
waitForReloadComplete( "Waiting for system ready before saving configuration..." ).then( function() {
console.log("saveConfiguration() writing cdata");
var jsstr = JSON.stringify( cdata,
function( k, v ) { return ( k.match( /^__/ ) || v === null ) ? undefined : purify(v); }
);
api.setDeviceStateVariablePersistent( myid, serviceId, "cdata", jsstr,
{
'onSuccess' : function() {
if ( ! isALTUI ) {
api.setDeviceState( myid, serviceId, "cdata", jsstr ); /* force local/lu_status */
}
configModified = false;
updateSaveControls();
console.log("saveConfiguration(): successful");
successFunc && successFunc();
},
'onFailure' : function() {
console.log("saveConfiguration(): FAILED");
failFunc && failFunc();
}
}
); /* setDeviceStateVariable */
}); /* then */
}
/**
* Handle save click: save the current configuration.
*/
function handleSaveClick( ev, fnext, fargs ) {
var myid = api.getCpanelDeviceId();
var cdata = getConfiguration( myid );
$( "button.revertconf" ).prop( "disabled", true );
$( "button.saveconf" ).text("Wait...").prop( "disabled", true );
saveConfiguration( myid, function() {
updateSaveControls();
configModified = false;
fnext && fnext.apply( null, fargs );
if ( cdata.__reloadneeded ) {
delete cdata.__reloadneeded;
api.showCustomPopup( "Reloading Luup...", { autoHide: false, category: 3 } );
setTimeout( function() {
api.performActionOnDevice( 0, "urn:micasaverde-com:serviceId:HomeAutomationGateway1", "Reload",
{ actionArguments: { Reason: "Reactor saved config needs reload" } } );
setTimeout( function() {
waitForReloadComplete().catch( function(reason) {
/* Errors here don't matter */
}).finally( function() {
hideModal();
});
}, 5000 );
}, 5000 );
} else {
clearUnusedStateVariables( myid, cdata );
}
}, function() {
alert('There was a problem saving the configuration. Vera/Luup may have been restarting. Please try saving again in a moment.');
updateSaveControls();
fnext && fnext.apply( null, fargs );
});
}
/**
* Check for unsaved changes, save...
*/
function checkUnsaved( myid ) {
if ( configModified ) {
if ( confirm( msgUnsavedChanges ) ) {
handleSaveClick( undefined );
} else {
/* Discard unsaved config */
var d = getInstanceData( myid );
delete d.cdata;
delete d.ixCond;
}
}
configModified = false;
}
/* Closing the control panel. */
onBeforeCpanelClose = function() {
console.log( "onBeforeCpanelClose" );
if ( configModified ) {
if ( confirm( msgUnsavedChanges ) ) {
handleSaveClick( undefined );
}
}
configModified = false;
clearModule();
};
/**
* Handle revert button click: restore setting to last saved and redisplay.
*/
function handleRevertClick( ev ) {
if ( ! confirm( "Discard changes and revert to last saved configuration?" ) ) {
return;
}
var myid = api.getCpanelDeviceId();
getConfiguration( myid, true );
configModified = false;
updateSaveControls();
/* Be careful about which tab we're on here. */
/* ??? when all tabs are modules, module.redraw() is a one-step solution */
var ctx = $( ev.currentTarget ).closest('div.reactortab').attr('id');
if ( ctx === "tab-vars" ) {
redrawVariables();
} else if ( ctx === "tab-conds" ) {
CondBuilder.redraw( myid );
} else if ( ctx === "tab-actions" ) {
redrawActivities();
} else {
alert("OK, I did the revert, but now I'm lost. Go to the Status tab, and then come back to this tab.");
}
}
function conditionValueText( v, forceNumber ) {
if ( "number" === typeof(v) ) return v;
v = String(v).trim();
if ( v.match( varRefPattern ) ) return v;
if ( forceNumber ) {
var n;
if ( v.match( /^[+-]?[0-9]+$/ ) ) {
n = parseInt( v );
} else {
n = parseFloat( v );
}
if ( isNaN( n ) ) return JSON.stringify( v ) + "(invalid)";
return String( n );
}
return JSON.stringify( v );
}
function makeConditionDescription( cond ) {
if ( cond === undefined ) {
return "(undefined)";
}
var str = "", t, k;
switch ( cond.type || "group" ) {
case 'group':
str = String(cond.name || cond.id);
break;
case 'service':
case 'var':
if ( "var" === cond.type ) {
str += cond.var || "(undefined)";
} else {
t = getDeviceFriendlyName( cond.device );
str += t ? t : '#' + cond.device + ' ' + ( cond.devicename || cond.deviceName || "name unknown" ) + ' (missing)';
str += ' ' + ( cond.variable || "?" );
}
t = arrayFindValue( serviceOps, function( v ) { return v.op === cond.operator; } );
if ( !t ) {
str += ' ' + cond.operator + '? ' + conditionValueText( cond.value );
} else {
str += ' ' + (t.desc || t.op);
if ( undefined === t.args || t.args > 0 ) {
if ( t.args > 1 ) {
var fmt = t.format || "%1,%2";
k = coalesce( cond.value, "" ).split( /,/ );
/* Remove trailing empties if they are optional */
while ( k.length > 0 && ( t.optional || 0 ) >= k.length && isEmpty( k[k.length-1] ) ) {
k.pop();
}
/* ??? FIXME -- Future pattern replacement loop. For now, never more than 2, so simple */
fmt = fmt.replace( '%1', k.length > 0 && !isEmpty( k[0] ) ? conditionValueText( k[0], t.numeric ) : (t.blank || '""' ) );
fmt = fmt.replace( '%2', k.length > 1 && !isEmpty( k[1] ) ? conditionValueText( k[1], t.numeric ) : (t.blank || '""' ) );
str += ' ' + fmt;
} else {
str += ' ' + conditionValueText( cond.value, t.numeric );
}
}
}
if ( ( !t || 0 === ( t.numeric || 0 ) ) &&
0 === coalesce( cond.nocase, 1 ) ) {
str += ' (match case)';
}
break;
case "grpstate":
t = getDeviceFriendlyName( cond.device );
str += t ? t : '#' + cond.device + ' ' + ( cond.devicename || cond.deviceName || "name unknown" ) + ' (missing)';
try {
var devnum = -1 === ( cond.device || -1 ) ? api.getCpanelDeviceId() : cond.device;
t = ( getConditionIndex( devnum ) || {} )[ cond.groupid ];
str += ' ' + ( t ? ( t.name || cond.groupid || "?" ) : ( ( cond.groupid || "?" ) + " (MISSING!)" ) );
} catch( e ) {
str += ' ' + ( cond.groupid || "?" ) + ' (' + String(e) + ')';
}
t = arrayFindValue( serviceOps, function( v ) { return v.op === cond.operator; } );
if ( t ) {
str += ' ' + ( t.desc || t.op );
} else {
str += ' ' + String(cond.operator) + '?';
}
break;
case 'comment':
str = cond.comment;
break;
case 'housemode':
t = ( cond.value || "" ).split( /,/ );
if ( cond.operator == "change" ) {
str += "changes from ";
if ( t.length > 0 && t[0] !== "" ) {
str += houseModeName[t[0]] || t[0];
} else {
str += "any mode";
}
str += " to ";
if ( t.length > 1 && t[1] !== "" ) {
str += houseModeName[t[1]] || t[1];
} else {
str += "any mode";
}
} else {
str += "is ";
if ( t.length == 0 || t[0] === "" ) {
str += "invalid";
} else {
for ( k=0; k 0 ) {
str = str + String(offs) + " mins after ";
}
str = str + ( names[k[1]] || k[1] );
return str;
}
}
t = ( cond.value || "sunrise+0,sunset+0" ).split(/,/);
str += sunrange( t[0] || "sunrise+0" );
if ( cond.operator == "bet" || cond.operator == "nob" ) {
str += " and ";
str += sunrange( t[1] || "sunset+0" );
}
break;
case 'trange':
if ( opName[ cond.operator ] !== undefined ) {
str += opName[ cond.operator ];
} else {
str += cond.operator + '?';
}
t = ( cond.value || "" ).split(/,/);
str += ' ' + textDateTime( t[0], t[1], t[2], t[3], t[4], false );
if ( cond.operator !== "before" && cond.operator !== "after" ) {
str += ' and ' + textDateTime( t[5], t[6], t[7], t[8], t[9], true );
}
break;
case 'interval':
str += "every";
if ( cond.days > 0 ) {
str += " " + String(cond.days) + " days";
}
if ( cond.hours > 0 ) {
str += " " + String(cond.hours) + " hours";
}
if ( cond.mins > 0 ) {
str += " " + String(cond.mins) + " minutes";
}
if ( "condtrue" === ( cond.relto || "" ) ) {
str += " (relative to ";
str += makeConditionDescription( getConditionIndex()[ cond.relcond ] );
str += ")";
} else {
if ( ! isEmpty( cond.basetime ) ) {
t = cond.basetime.split(/,/);
str += " (relative to ";
if ( ! isEmpty( cond.basedate ) ) {
var d = cond.basedate.split(/,/);
try {
var dt = new Date(
parseInt( d[0] ),
parseInt( d[1] - 1 ),
parseInt( d[2] ),
parseInt( t[0] ),
parseInt( t[1] ),
0, 0 );
str += ftime( dt, dateFormat + " " + timeFormat );
} catch ( e ) {
str += String( cond.basedate ) + ' ' + String( cond.basetime );
}
} else {
if ( t.length == 2 ) {
str += t[0] + ":" + t[1];
} else {
str += String( cond.basetime );
}
}
str += ")";
}
}
break;
case 'ishome':
t = ( cond.value || "" ).split(/,/);
if ( "at" === cond.operator || "notat" === cond.operator ) {
var desc = cond.operator == "at" ? " at " : " not at ";
var uu = userIx[t[0]];
if ( undefined === uu ) {
str += String(t[0]) + desc + " location " + String(t[1]);
} else {
var nn = uu.name || t[0];
if ( uu.tags && uu.tags[t[1]] ) {
str += nn + desc + uu.tags[t[1]].name;
} else {
str += nn + desc + " location " + t[1];
}
}
} else {
if ( t.length < 1 || t[0] == "" ) {
str += cond.operator === "is not" ? "no user is home" : "any user is home";
} else {
/* Replace IDs with names for display */
for ( k=0; k');
getSortedDeviceList().forEach( function( roomObj ) {
var haveItem = false;
var xg = $( '' ).attr( 'label', roomObj.name );
var l = roomObj.devices.length;
for ( var j=0; j' ).val( devobj.id ).text( fn ? fn : '#' + String(devobj.id) + '?' ) );
}
if ( haveItem ) {
el.append( xg );
}
});
el.prepend( $( '' ).val( "-1" ).text( "(this ReactorSensor)" ) );
el.prepend( $( '' ).val( "" ).text( "--choose device--" ) );
menuSelectDefaultInsert( el, val, "(missing) #" + String(val) + " " + String(name || "") );
return el;
}
/**
* Update save/revert buttons (separate, because we use in two diff tabs
*/
function updateSaveControls() {
var errors = $('.tberror');
var pos = $( window ).scrollTop();
$('button.saveconf').text("Save")
.prop('disabled', ! ( configModified && errors.length === 0 ) )
.attr('title', errors.length === 0 ? "" : "Fix errors before saving");
$('button.revertconf').prop('disabled', !configModified);
setTimeout( function() { $(window).scrollTop( pos ); }, 100 );
}
/** ***************************************************************************
*
* S T A T U S
*
** **************************************************************************/
function updateTime( condid, target, prefix, countdown, limit ) {
var $el = $( 'span#' + idSelector(condid) + ".timer" );
if ( 0 === $el.length ) { console.log(condid+" not found, bye!"); return; }
var now = Math.floor( Date.now() / 1000 + 0.5 );
var d;
if ( countdown ) {
/* Count down -- delta is (future) target to now */
d = target - now;
if ( d < ( limit || 0 ) ) {
$el.remove();
return;
}
} else {
/* Count up -- delta is now since target */
d = now - target;
if ( limit && d > limit ) {
$el.remove();
return;
}
}
var hh = Math.floor( d / 3600 );
d -= hh * 3600;
var mm = Math.floor( d / 60 );
d -= mm * 60;
d = (mm < 10 ? '0' : '') + String(mm) + ':' + (d < 10 ? '0' : '') + String(d);
if ( 0 !== hh ) d = (hh < 10 ? '0' : '') + String(hh) + ':' + d;
$el.text( prefix + ' ' + d );
setTimeout( function() { updateTime( condid, target, prefix, countdown, limit ); }, 500 );
}
function getCondOptionDesc( cond ) {
var condOpts = cond.options || {};
var condDesc = "";
if ( undefined !== condOpts.after ) {
condDesc += ( ( condOpts.aftertime || 0 ) > 0 ? ' within ' + condOpts.aftertime + ' secs' : '' ) +
' after ' + makeConditionDescription( getConditionIndex()[ condOpts.after ] ) +
( 0 === ( condOpts.aftermode || 0 ) ? ' (which is still TRUE)' : '' );
}
if ( ( condOpts.repeatcount || 0 ) > 1 ) {
condDesc += " repeats " + condOpts.repeatcount +
" times within " + ( condOpts.repeatwithin || 60 ) + " secs";
} else if ( ( condOpts.duration || 0 ) > 0 ) {
condDesc += " for " +
( condOpts.duration_op === "lt" ? "less than " : "at least " ) +
condOpts.duration + " secs";
}
if ( ( condOpts.holdtime || 0 ) > 0 ) {
condDesc += "; delay reset for " + condOpts.holdtime + " secs";
}
if ( ( condOpts.pulsetime || 0 ) != 0 ) {
condDesc += "; pulse for " + condOpts.pulsetime + " secs";
var pbo = condOpts.pulsebreak || 0;
if ( pbo > 0 ) {
condDesc += ", repeat after " + pbo + " secs";
pbo = condOpts.pulsecount || 0;
if ( pbo > 0 ) {
condDesc += ", up to " + pbo + " times";
}
}
}
if ( ( condOpts.latch || 0 ) != 0 ) {
condDesc += "; latching";
}
return condDesc;
}
function getCondState( cond, currentValue, cstate, el ) {
el.text( "" );
if ( cond.type !== "comment" && undefined !== currentValue ) {
var cs = cstate[cond.id] || {};
var shortVal = String( currentValue );
el.attr( 'title', shortVal ); /* before we cut it */
if ( shortVal.length > 20 ) {
shortVal = shortVal.substring( 0, 17 ) + '...';
}
el.text( shortVal +
( currentValue === cs.laststate ? "" : ( cs.laststate ? " (true)" : " (false)" ) ) +
' as of ' + shortLuaTime( cs.statestamp ) );
if ( condOptions[ cond.type || "group" ].repeat && ( ( cond.options||{} ).repeatcount || 0 ) > 1 ) {
if ( cs.repeats !== undefined && cs.repeats.length > 1 ) {
var dtime = cs.repeats[ cs.repeats.length - 1 ] - cs.repeats[0];
el.append( " (last " + cs.repeats.length + " span " + dtime + " secs)" );
}
}
if ( ( cs.pulsecount || 0 ) > 0 ) {
var lim = ( cond.options||{} ).pulsecount || 0;
el.append( " (pulsed " +
(cs.pulsecount < 1000 ? cs.pulsecount : ">999" ) +
( lim > 0 ? ( " of max " + lim ) : "" ) +
" times)" );
}
if ( cs.latched ) {
el.append( ' (latched)' );
}
/* Generate unique IDs for timers so that redraws will have
different IDs, and the old timers will self-terminate. */
var id;
if ( cs.laststate && cs.waituntil ) {
id = getUID();
el.closest('div.cond').addClass('reactor-timing');
el.append( $('').attr( 'id', id ) );
(function( c, t, l ) {
setTimeout( function() { updateTime( c, t, "; sustained", false, l ); }, 20 );
})( id, cs.statestamp, ( cond.options || {} ).duration );
} else if (cs.evalstate && cs.holduntil) {
id = getUID();
el.closest('div.cond').addClass('reactor-timing');
el.append( $('').attr( 'id', id ) );
(function( c, t, l ) {
setTimeout( function() { updateTime( c, t, "; reset delayed", true, l ); }, 20 );
})( id, cs.holduntil, 0 );
} else if ( cs.pulseuntil) {
id = getUID();
el.closest('div.cond').addClass('reactor-timing');
el.append( $('').attr( 'id', id ) );
(function( c, t, l ) {
setTimeout( function() { updateTime( c, t, "; pulse ", true, l ); }, 20 );
})( id, cs.pulseuntil, 0 );
} else {
el.closest('div.cond').removeClass('reactor-timing');
}
}
}
function handleStatusCondClick( ev ) {
var $el = $( ev.target );
var $grp = $el.closest( 'div.reactorgroup' );
var $body = $( 'div.grpbody', $grp );
$grp.toggleClass( 're-grp-collapsed' );
$body.toggle( ! $grp.hasClass( 're-grp-collapsed' ) );
}
function showGroupStatus( grp, container, cstate ) {
var grpel = $( '\
\
??
\
\
\
\
' );
var title = 'Group: ' + (grp.name || grp.id ) +
( grp.disabled ? " (disabled)" : "" ) + " <" + grp.id + ">";
$( 'span.re-title', grpel ).text( title + getCondOptionDesc( grp ) + "; " );
$( '.condbtn', grpel ).text( (grp.invert ? "NOT " : "") + (grp.operator || "and" ).toUpperCase() )
.on( "click.reactor", handleStatusCondClick );
/* Highlight groups that are "true" */
if ( grp.disabled || "0" === api.getDeviceState( api.getCpanelDeviceId(), serviceId, "Enabled" ) ) {
grpel.addClass( 'groupdisabled' );
} else {
var gs = cstate[ grp.id ] || {};
getCondState( grp, gs.laststate, cstate, $( 'span.currentvalue', grpel ) );
if ( "undefined" === typeof gs.evalstate || null === gs.evalstate ) {
grpel.addClass( "nostate" );
} else if ( gs.evalstate ) {
grpel.addClass( "truestate" );
}
}
container.append( grpel );
grpel = $( 'div.grpcond', grpel );
var l = grp.conditions ? grp.conditions.length : 0;
for ( var i=0; i').attr( 'id', cond.id );
var currentValue = ( cstate[cond.id] || {} ).lastvalue;
var condType = condTypeName[ cond.type ] !== undefined ? condTypeName[ cond.type ] : cond.type;
var condDesc = makeConditionDescription( cond );
switch ( cond.type ) {
case 'service':
case 'grpstate':
case 'var':
if ( -1 !== ( cond.device || -1 ) ) {
row.toggleClass( "re-cond-error", ! api.getDeviceObject( cond.device ) );
}
break;
case 'weekday':
if ( currentValue !== undefined && weekDayName[ currentValue ] !== undefined ) {
currentValue = weekDayName[ currentValue ];
}
break;
case 'housemode':
if ( currentValue !== undefined && houseModeName[ currentValue ] !== undefined ) {
currentValue = houseModeName[ currentValue ];
}
break;
case 'sun':
case 'trange':
if ( currentValue !== undefined ) {
currentValue = shortLuaTime( currentValue );
}
break;
case 'interval':
currentValue = shortLuaTime( currentValue );
break;
case 'ishome':
var t = (currentValue || "").split( /,/ );
if ( "at" === cond.operator || "notat" === cond.operator ) {
/* Nada */
} else {
/* Replace IDs with names for display */
if ( t.length > 0 && t[0] !== "" ) {
for ( var k=0; k' ).html( 'remove' ) );
row.append( $( '' ).text( condType + ': ' + condDesc ) );
/* Append current value and condition state */
var el = $( '' );
row.append( el );
getCondState( cond, currentValue, cstate, el );
/* Apply highlight for state */
if ( cond.type !== "comment" && undefined !== currentValue ) {
var cs = cstate[cond.id] || {};
row.toggleClass( 'truestate', true === cs.evalstate )
.toggleClass( 'falsestate', true === cs.evalstate );
$( 'div.condind i', row ).text( cs.evalstate ? 'check' : 'clear' );
}
grpel.append( row );
}
}
}
/**
* Update status display.
*/
function updateStatus( pdev ) {
var el;
var stel = $('div#reactorstatus');
if ( stel.length === 0 || !inStatusPanel ) {
/* If not displayed, do nothing. */
return;
}
/* Get configuration data and current state */
var cdata = getConfiguration( pdev, true );
if ( undefined === cdata ) {
stel.empty().text("An error occurred while attempting to fetch the configuration data. Luup may be reloading. Try again in a few moments.");
console.log("cdata unavailable");
return;
}
var cstate = getConditionStates( pdev );
stel.empty();
var s = parseInt( api.getDeviceState( pdev, serviceId, "TestTime" ) || "0" );
if ( s && s > 0 ) {
var tid = getUID( "clk" );
$('').attr( 'id', tid ).text("Test Time is in effect!")
.appendTo( stel );
var updateTestClock = function( fid, base ) {
if ( !inStatusPanel ) return;
var $f = $( 'div#' + fid + ".re-alertblock" );
if ( 1 === $f.length ) {
var now = Math.floor( Date.now() / 1000 );
var offs = now - (parseInt( api.getDeviceState( pdev, serviceId, "tref" ) ) || now);
var dt = new Date();
dt.setTime( ( base + offs ) * 1000 );
$f.text("Test Time is in effect! Test clock is " + dt.toLocaleString());
window.setTimeout( function() { updateTestClock(fid, base); }, 500 );
}
};
updateTestClock( tid, s );
}
var thm = api.getDeviceState( pdev, serviceId, "TestHouseMode" ) || "0";
if ( ! ( isEmpty(thm) || "0" === thm ) ) {
$('').text("Test House Mode is in effect!")
.appendTo( stel );
}
var vix = [];
for ( var vn in ( cdata.variables || {} ) ) {
if ( cdata.variables.hasOwnProperty( vn ) ) {
var v = cdata.variables[vn];
vix.push( v );
}
}
if ( vix.length > 0 ) {
vix.sort( function( a, b ) {
var i1 = a.index || -1;
var i2 = b.index || -1;
if ( i1 === i2 ) {
i1 = (a.name || "").toLowerCase();
i2 = (b.name || "").toLowerCase();
if ( i1 === i2 ) return 0;
/* fall through */
}
return ( i1 < i2 ) ? -1 : 1;
});
var grpel = $( '' );
grpel.append( '
Expressions
' );
var body = $( '' );
grpel.append( body );
var l = vix.length;
for ( var ix=0; ix' );
var vv = ((cstate.vars || {})[vd.name] || {}).lastvalue;
if ( null === vv ) {
vv = "(null)";
} else if ( "object" === typeof vv && "null" === vv.__type ) {
vv = "( null )";
} else {
try {
vv = JSON.stringify(vv);
} catch( e ) {
vv = String( vv );
}
}
var ve = coalesce( vs.err, "" );
if ( vv && vv.length > 256 ) {
vv = vv.substring( 0, 253 ) + "...";
}
el.append( $('').text( vd.name ) );
el.append( $('').text( isEmpty( vd.expression ) ? "(no expression)" : vd.expression ) );
el.append( $('').text( "" !== ve ? ve : vv ) );
if ( "" !== ve ) {
el.addClass( 'tb-exprerr' );
} else if ( vs.changed ) {
el.addClass( 'tb-valchanged' );
}
body.append( el );
}
stel.append( grpel );
}
showGroupStatus( cdata.conditions.root, stel, cstate );
}
function onUIDeviceStatusChanged( args ) {
if ( !inStatusPanel ) {
return;
}
var pdev = api.getCpanelDeviceId();
if ( args.id == pdev ) {
if ( arrayFindValue( args.states || [], function( v ) { return null !== v.variable.match( /^(cdata|cstate|Tripped|Armed|Enabled|TestTime|TestHouseMode|LastLoad)$/i ); } ) ) {
try {
updateStatus( pdev );
} catch (e) {
console.log( "Display update failed: " + String(e));
console.log( e );
}
}
}
}
function doStatusPanel()
{
console.log("doStatusPanel()");
/* Make sure changes are saved. */
var myid = api.getCpanelDeviceId();
checkUnsaved( myid );
if ( needsRestart && confirm( 'It is recommended that your ReactorSensor be restarted after setting or clearing the Test Time or House Mode. Press "OK" to restart it now, or "Cancel" to skip the restart.' ) ) {
api.performActionOnDevice( myid, serviceId, "Restart", { actionArguments: {} } );
}
needsRestart = false;
if ( ! initModule() ) {
return;
}
/* Standard header stuff */
header();
/* Our styles. */
if ( 0 === $('style#reactor-status-styles').length ) {
$("head").append( '');
}
api.setCpanelContent( '
If you are considering moving to Home Assistant or Hubitat, check out my Multi-Hub Reactor project. It's an evolution of Reactor for Vera that supports multiple home automation hubs including Vera, to ease your transition between them -- take your time moving devices from one platform to another, rather than trying to do a "fork lift upgrade" of your entire home automation system and devices at once. It also works with MQTT, giving you accecss to its universe of supported devices and integrations, including a ton of inexpensive WiFi devices. And yes, it even supports Ezlo hubs, too.
` )
.insertBefore( $( 'div#reactorstatus' ) );
updateStatus( myid );
checkUpdate().then( function( data ) {
if ( data ) {
$( '' )
.text( 'An update for Reactor is available. The latest release is ' + data.name + '. Go to the About tab on the master device to install it.' )
.insertBefore( $( 'div#reactorstatus' ) );
}
});
}
catch ( e ) {
inStatusPanel = false; /* stop updates */
console.log( e );
alert( e.stack );
}
setTimeout( function() { clearUnusedStateVariables( myid, getConfiguration( myid ) ); }, 2000 );
}
/** ***************************************************************************
*
* C O N D I T I O N S
*
** **************************************************************************/
/**
* The condition builder is encapsulated into its own module for "private"
* implementation. It shares some global functions with other tab code,
* but I'll clean this up as I go along and make everything more modular.
*/
var CondBuilder = (function( api, $ ) {
var redrawGroup; /* fwd decl */
/**
* Renumber group conditions.
*/
function reindexConditions( grp ) {
var $el = $( 'div#' + idSelector( grp.id ) + '.cond-container.cond-group' ).children( 'div.cond-group-body' ).children( 'div.cond-list' );
var ixCond = getConditionIndex();
var ix = 0;
grp.conditions.splice( 0, grp.conditions.length ); /* empty in place */
$el.children().each( function( n, row ) {
var id = $( row ).attr( 'id' );
var obj = ixCond[ id ];
if ( obj ) {
// console.log("reindexConditions(" + grp.id + ") " + id + " is now " + ix);
$( row ).removeClass( 'level' + String( obj.__depth || 0 ) ).removeClass( 'levelmod0 levelmod1 levelmod2 levelmod3' );
grp.conditions[ix] = obj;
obj.__parent = grp;
obj.__index = ix++;
if ( "group" == ( obj.type || "group" ) ) {
obj.__depth = grp.__depth + 1;
$( row ).addClass( 'level' + obj.__depth ).addClass( 'levelmod' + (obj.__depth % 4) );
}
} else {
/* Not found. Remove from UI */
$( row ).remove();
}
});
}
/**
* Remove all properies on condition except those in the exclusion list.
* The id and type properties are always preserved.
*/
function removeConditionProperties( cond, excl ) {
var elist = (excl || "").split(/,/);
var emap = { id: true, type: true, options: true }; /* never remove these */
for ( var ix=0; ix' );
/* Create a list of variables by index, sorted. cdata.variables is a map/hash,
not an array */
var cdata = getConfiguration();
var vix = [];
for ( var vn in ( cdata.variables || {} ) ) {
if ( cdata.variables.hasOwnProperty( vn ) ) {
var v = cdata.variables[vn];
vix.push( v );
}
}
vix.sort( function( a, b ) {
var i1 = a.index || -1;
var i2 = b.index || -1;
if ( i1 === i2 ) return 0;
return ( i1 < i2 ) ? -1 : 1;
});
var l = vix.length;
for ( var ix=0; ix' ).val( vix[ix].name ).text( vix[ix].name ).appendTo( $el );
}
if ( currExpr && 0 === $( "option[value=" + JSON.stringify( currExpr ) + "]", $el ).length ) {
$( '' ).val( currExpr ).text( currExpr + " (undefined)" )
.prependTo( $el );
}
$( '' ).val( "" ).text( '--choose--' ).prependTo( $el );
$el.val( coalesce( currExpr, "" ) );
return $el;
}
/**
* Make a service/variable menu of all state defined for the device. Be
* brief, using only the variable name in the menu, unless that name is
* used by multiple services, in which case the last component of the
* serviceId is added parenthetically to draw the distinction.
*/
function makeVariableMenu( device, service, variable ) {
var el = $('');
var myid = api.getCpanelDeviceId();
if ( -1 === device ) device = myid;
var devobj = api.getDeviceObject( device );
if ( devobj ) {
var mm = {}, ms = [];
var l = devobj.states ? devobj.states.length : 0;
for ( var k=0; k' + sv.text + '' );
});
if ( 0 === r.length ) {
el.append( '' );
}
}
if ( isEmpty( service ) || isEmpty( variable ) ) {
menuSelectDefaultFirst( el, "" );
} else {
var key = service + "/" + variable;
menuSelectDefaultInsert( el, key );
}
return el;
}
function makeServiceOpMenu( op ) {
var el = $('');
var l = serviceOps.length;
for ( var ix=0; ix').val(serviceOps[ix].op).text(serviceOps[ix].desc || serviceOps[ix].op) );
}
if ( undefined !== op ) {
el.val( op );
}
return el;
}
function makeDateTimeOpMenu( op ) {
var el = $('');
el.append( '' );
el.append( '' );
if ( undefined !== op ) {
el.val( op );
}
return el;
}
/* Make a menu of eligible groups in a ReactorSensor */
function makeRSGroupMenu( cond ) {
var mm = $( '' );
mm.empty();
$( '' ).val( "" ).text("--choose--").appendTo( mm );
try {
var dc;
var myid = api.getCpanelDeviceId();
var myself = -1 === cond.device || cond.device === myid;
if ( myself ) {
/* Our own groups */
dc = getConfiguration( myid );
} else {
dc = getConfiguration( cond.device );
}
if ( dc ) {
var appendgrp = function ( grp, sel, pg ) {
/* Don't add ancestors in same RS */
if ( "nul" !== grp.operator && ! ( myself && isAncestor( grp.id, cond.id, myid ) ) ) {
sel.append(
$( '' ).val( grp.id )
.text( "root"===grp.id ? "Tripped/Untripped (root)" : ( grp.name || grp.id ) )
);
}
/* Don't scan siblings or anything below. */
if ( myself && grp.id == pg.id ) return;
var l = grp.conditions ? grp.conditions.length : 0;
for ( var ix=0; ix 1 ) {
// Join simple two value list, but don't save "," on its own.
cond.value = $( 'input#' + idSelector( cond.id + '-val1' ), $row ).val() || "";
val = $( 'input#' + idSelector( cond.id + '-val2' ), $row ).val() || "";
if ( ( isEmpty( cond.value ) || isEmpty( val ) ) && ! op.optional ) {
$( 'input.re-secondaryinput', $row ).addClass( 'tberror' );
}
if ( 1 === op.optional && ( isEmpty( cond.value ) && isEmpty( val ) ) ) {
$( 'input.re-secondaryinput', $row ).addClass( 'tberror' );
}
/* Other possibility is 2 === op.optional, allows both fields blank */
if ( ! isEmpty( val ) ) {
cond.value += "," + val;
}
} else if ( op.args == 1 ) {
cond.value = $("input.operand", $row).val() || "";
if ( isEmpty( cond.value ) && ! op.optional ) {
$( 'input.operand', $row ).addClass( 'tberror' );
}
} else {
delete cond.value;
}
/* For numeric op, check that value is parseable as a number (unless var ref) */
if ( op && op.numeric && ! cond.value.match( varRefPattern ) ) {
val = parseFloat( cond.value );
if ( isNaN( val ) ) {
$( 'input.operand', $row ).addClass( 'tberror' );
}
}
break;
case "grpstate":
removeConditionProperties( cond, "device,devicename,groupid,groupname,operator,options" );
cond.device = parseInt( $( 'div.params select.devicemenu', $row ).val(), $row );
cond.groupid = $( 'div.params select.re-grpmenu', $row ).val() || "";
$( "div.params select.re-grpmenu", $row ).toggleClass( 'tberror', isEmpty( cond.groupid ) );
cond.groupname = $( 'div.params select.re-grpmenu option:selected', $row ).text();
cond.operator = $( 'div.params select.opmenu', $row ).val() || "istrue";
break;
case 'weekday':
removeConditionProperties( cond, "operator,value,options" );
cond.operator = $("div.params select.wdcond", $row).val() || "";
res = [];
$("input.wdopt:checked", $row).each( function( ix, control ) {
res.push( control.value /* DOM element */ );
});
cond.value = res.join( ',' );
break;
case 'housemode':
removeConditionProperties( cond, "operator,value,options" );
cond.operator = $("div.params select.opmenu", $row).val() || "is";
if ( "change" === cond.operator ) {
// Join simple two value list, but don't save "," on its own.
cond.value = $( 'select.re-frommode', $row ).val() || "";
val = $( 'select.re-tomode', $row ).val();
if ( ! isEmpty( val ) ) {
cond.value += "," + val;
}
} else {
res = [];
$("input.hmode:checked", $row).each( function( ix, control ) {
res.push( control.value /* DOM element */ );
});
if ( 0 === res.length ) {
$( 'select.opmenu', $row ).addClass( 'tberror' );
} else {
cond.value = res.join( ',' );
}
}
break;
case 'trange':
cond.operator = $("div.params select.opmenu", $row).val() || "bet";
var between = "bet" === cond.operator || "nob" == cond.operator;
if ( target !== undefined && target.hasClass('year') ) {
var pdiv = target.closest('div');
var newval = target.val().trim();
/* Vera's a 32-bit system, so date range is bound to MAXINT32 (2038-Jan-19 03:14:07 aka Y2K38) */
if ( newval != "" && ( (!newval.match( /^[0-9]+$/ )) || newval < 1970 || newval > 2037 ) ) {
target.addClass( 'tberror' );
} else {
var losOtros;
if ( pdiv.hasClass('start') ) {
losOtros = $('div.re-endfields input.year', $row);
} else {
losOtros = $('div.re-startfields input.year', $row);
}
if ( newval === "" && losOtros.val() !== "" ) {
losOtros.val("");
} else if ( newval !== "" && losOtros.val() === "" ) {
losOtros.val(newval);
}
}
}
var mon = $("div.re-startfields select.monthmenu", $row).val() || "";
if ( isEmpty( mon ) ) {
/* No/any month. Disable years. */
$( '.datespec', $row ).val( "" ).prop( 'disabled', true );
/* Ending month must also be blank */
$( 'div.re-endfields select.monthmenu', $row ).val( "" );
} else {
/* Month specified, year becomes optional, but either both
years must be specified or neither for between/not. */
$( '.datespec', $row ).prop( 'disabled', false );
$( 'div.re-startfields select.daymenu:has(option[value=""]:selected)', $row ).addClass( 'tberror' );
if ( between ) {
$( 'div.re-endfields select.daymenu:has(option[value=""]:selected)', $row ).addClass( 'tberror' );
var y1 = $( 'div.re-startfields input.year', $row ).val() || "";
var y2 = $( 'div.re-endfields input.year', $row ).val() || "";
if ( isEmpty( y1 ) !== isEmpty( y2 ) ) {
$( '.datespec', $row ).addClass( 'tberror' );
}
var m2 = $( 'div.re-endfields select.monthmenu', $row ).val() || "";
if ( isEmpty( m2 ) ) {
/* Ending month may not be blank--flag both start/end */
$( 'select.monthmenu', $row ).addClass( 'tberror' );
}
}
}
var dom = $( 'div.re-startfields select.daymenu', $row ).val() || "";
if ( isEmpty( dom ) ) {
/* Start day is blank. So must be end day */
$( 'div.re-endfields select.daymenu', $row ).val( "" );
} else if ( between ) {
/* Between with start day, end day must also be specified. */
$( 'div.re-endfields select.daymenu:has(option[value=""]:selected)', $row ).addClass( 'tberror' );
}
/* Fetch and load */
res = [];
res.push( isEmpty( mon ) ? "" : $("div.re-startfields input.year", $row).val() || "" );
res.push( mon );
res.push( $("div.re-startfields select.daymenu", $row).val() || "" );
res.push( $("div.re-startfields select.hourmenu", $row).val() || "0" );
res.push( $("div.re-startfields select.minmenu", $row).val() || "0" );
if ( ! between ) {
Array.prototype.push.apply( res, ["","","","",""] );
$('div.re-endfields', $row).hide();
} else {
$('div.re-endfields', $row).show();
res.push( isEmpty( mon ) ? "" : $("div.re-endfields input.year", $row).val() || "" );
res.push( isEmpty( mon ) ? "" : $("div.re-endfields select.monthmenu", $row).val() || "" );
res.push( $("div.re-endfields select.daymenu", $row).val() || "" );
res.push( $("div.re-endfields select.hourmenu", $row).val() || "0" );
res.push( $("div.re-endfields select.minmenu", $row).val() || "0" );
}
cond.value = res.join(',');
break;
case 'sun':
removeConditionProperties( cond, "operator,value,options" );
cond.operator = $('div.params select.opmenu', $row).val() || "after";
res = [];
var whence = $('div.params select.re-sunstart', $row).val() || "sunrise";
var offset = getInteger( $('div.params input.re-startoffset', $row).val() || "0" );
if ( isNaN( offset ) ) {
/* Validation error, flag and treat as 0 */
offset = 0;
$('div.params input.re-startoffset', $row).addClass('tberror');
}
res.push( whence + ( offset < 0 ? '' : '+' ) + String(offset) );
if ( cond.operator == "bet" || cond.operator == "nob" ) {
$( 'div.re-endfields', $row ).show();
whence = $('select.re-sunend', $row).val() || "sunset";
offset = getInteger( $('input.re-endoffset', $row).val() || "0" );
if ( isNaN( offset ) ) {
offset = 0;
$('div.params input.re-endoffset', $row).addClass('tberror');
}
res.push( whence + ( offset < 0 ? '' : '+' ) + String(offset) );
} else {
$( 'div.re-endfields', $row ).hide();
res.push("");
}
cond.value = res.join(',');
break;
case 'interval':
removeConditionProperties( cond, "days,hours,mins,basetime,basedate,relto,relcond,options" );
var nmin = 0;
var v = $('div.params .re-days', $row).val() || "0";
if ( v.match( varRefPattern ) ) {
cond.days = v;
nmin = 1440;
} else {
v = getOptionalInteger( v, 0 );
if ( isNaN(v) || v < 0 ) {
$( 'div.params .re-days', $row ).addClass( 'tberror' );
} else {
cond.days = v;
nmin = nmin + 1440 * v;
}
}
if ( typeof(cond.days) == "string" || ( typeof(cond.days) == "number" && 0 !== cond.days ) ) {
$('div.params .re-hours,.re-mins', $row).prop('disabled', true).val("0");
cond.hours = 0;
cond.mins = 0;
} else {
$('div.params .re-hours,.re-mins', $row).prop('disabled', false);
v = $('div.params .re-hours', $row).val() || "0";
if ( v.match( varRefPattern ) ) {
cond.hours = v;
nmin = 60;
} else {
v = getOptionalInteger( v, 0 );
if ( isNaN(v) || v < 0 || v > 23 ) {
$( 'div.params .re-hours', $row ).addClass( 'tberror' );
} else {
cond.hours = v;
nmin = nmin + 60 * v;
}
}
v = $('div.params .re-mins', $row).val() || "0";
if ( v.match( varRefPattern ) ) {
cond.mins = v;
nmin = 1;
} else {
v = getOptionalInteger( v, 0 );
if ( isNaN(v) || v < 0 || v > 59 ) {
$( 'div.params .re-mins', $row ).addClass( 'tberror' );
} else {
cond.mins = v;
nmin = nmin + v;
}
}
if ( 0 !== nmin ) {
$( '.re-days', $row ).prop( 'disabled', true ).val("0");
} else {
$( '.re-days', $row ).prop( 'disabled', false );
}
}
if ( nmin <= 0 ) {
$( 'div.params .re-days,.re-hours,.re-mins', $row ).addClass( 'tberror' );
}
/* Interval relative to... */
v = $( 'div.params select.re-relto', $row ).val() || "";
if ( "condtrue" === v ) {
cond.relto = v;
cond.relcond = $( 'div.params select.re-relcond', $row).val() || "";
if ( "" === cond.relcond ) {
$( 'div.params select.re-relcond', $row ).addClass( 'tberror' );
}
delete cond.basetime;
delete cond.basedate;
} else {
var ry = $( 'div.params input.re-relyear', $row ).val() || "";
if ( isEmpty( ry ) ) {
delete cond.basedate;
$( '.re-reldate', $row ).prop( 'disabled', true );
} else {
$( '.re-reldate', $row ).prop( 'disabled', false );
cond.basedate = ry + "," +
( $( 'div.params select.re-relmon', $row ).val() || "1" ) +
"," +
( $( 'div.params select.re-relday', $row ).val() || "1" );
}
var rh = $( 'div.params select.re-relhour', $row ).val() || "00";
var rm = $( 'div.params select.re-relmin', $row ).val() || "00";
if ( rh == "00" && rm == "00" && isEmpty( cond.basedate ) ) {
delete cond.basetime;
} else {
cond.basetime = rh + "," + rm;
}
delete cond.relcond;
delete cond.relto;
}
break;
case 'ishome':
removeConditionProperties( cond, "operator,value,options" );
cond.operator = $("div.params select.geofencecond", $row).val() || "is";
res = [];
if ( "at" === cond.operator || "notat" === cond.operator ) {
res[0] = $( 'select.re-userid', $row ).val() || "";
res[1] = $( 'select.re-location', $row ).val() || "";
if ( isEmpty( res[0] ) ) {
$( 'select.re-userid', $row ).addClass( 'tberror' );
}
if ( isEmpty( res[1] ) ) {
$( 'select.re-location', $row ).addClass( 'tberror' );
}
} else {
$("input.useropt:checked", $row).each( function( ix, control ) {
res.push( control.value /* DOM element */ );
});
}
cond.value = res.join( ',' );
break;
case 'reload':
/* No parameters */
removeConditionProperties( cond, "options" );
break;
default:
break;
}
/* If condition options are present, check them, too. */
var $ct = $row.hasClass( 'cond-group' ) ? $row.children( 'div.condopts' ) : $( 'div.condopts', $row );
if ( $ct.length > 0 ) {
cond.options = cond.options || {};
/* Predecessor condition (sequencing) */
var $pred = $( 'select.re-predecessor', $ct );
if ( isEmpty( $pred.val() ) ) {
$( 'input.re-predtime', $ct ).prop( 'disabled', true ).val( "" );
$( 'input.predmode', $ct ).prop( 'disabled', true );
if ( undefined !== cond.options.after ) {
delete cond.options.after;
delete cond.options.aftertime;
delete cond.options.aftermode;
configModified = true;
}
} else {
$( 'input.re-predtime', $ct ).prop( 'disabled', false );
$( 'input.predmode', $ct ).prop( 'disabled', false );
var pt = parseInt( $('input.re-predtime', $ct).val() );
if ( isNaN( pt ) || pt < 0 ) {
pt = 0;
$('input.re-predtime', $ct).val(pt);
}
var predmode = $( 'input.predmode', $ct ).prop( 'checked' ) ? 0 : 1;
if ( cond.options.after !== $pred.val() || cond.options.aftertime !== pt ||
( cond.options.aftermode || 0 ) != predmode ) {
cond.options.after = $pred.val();
cond.options.aftertime = pt;
if ( predmode == 1 ) cond.options.aftermode = 1;
else delete cond.options.aftermode;
configModified = true;
}
}
/* Repeats */
var $rc = $('input.re-repeatcount', $ct);
if ( isEmpty( $rc.val() ) || $rc.prop('disabled') ) {
$('input.re-duration', $ct).prop('disabled', false);
$('select.re-durop', $ct).prop('disabled', false);
$('input.re-repeatspan', $ct).val("").prop('disabled', true);
if ( undefined !== cond.options.repeatcount ) {
delete cond.options.repeatcount;
delete cond.options.repeatwithin;
configModified = true;
}
} else {
val = getInteger( $rc.val() );
if ( isNaN( val ) || val < 2 ) {
$rc.addClass( 'tberror' );
} else if ( val > 1 ) {
$rc.removeClass( 'tberror' );
if ( val != cond.options.repeatcount ) {
cond.options.repeatcount = val;
delete cond.options.duration;
delete cond.options.duration_op;
configModified = true;
}
$('input.re-duration', $ct).val("").prop('disabled', true);
$('select.re-durop', $ct).val("ge").prop('disabled', true);
$('input.re-repeatspan', $ct).prop('disabled', false);
if ( $('input.re-repeatspan', $ct).val() === "" ) {
$('input.re-repeatspan', $ct).val( "60" );
cond.options.repeatwithin = 60;
configModified = true;
}
}
}
var $rs = $('input.re-repeatspan', $ct);
if ( ! $rs.prop('disabled') ) {
var rspan = getInteger( $rs.val() );
if ( isNaN( rspan ) || rspan < 1 ) {
$rs.addClass( 'tberror' );
} else {
$rs.removeClass( 'tberror' );
if ( rspan !== ( cond.options.repeatwithin || 0 ) ) {
cond.options.repeatwithin = rspan;
configModified = true;
}
}
}
/* Duration (sustained for) */
var $dd = $('input.re-duration', $ct);
if ( isEmpty( $dd.val() ) || $dd.prop('disabled') ) {
$('input.re-repeatcount', $ct).prop('disabled', false);
// $('input.re-repeatspan', $ct).prop('disabled', false);
if ( undefined !== cond.options.duration ) {
delete cond.options.duration;
delete cond.options.duration_op;
configModified = true;
}
} else {
var dur = getInteger( $dd.val() );
if ( isNaN( dur ) || dur < 0 ) {
$dd.addClass('tberror');
} else {
$dd.removeClass('tberror');
$('input.re-repeatcount', $ct).val("").prop('disabled', true);
// $('input.re-repeatspan', $ct).val("").prop('disabled', true);
delete cond.options.repeatwithin;
delete cond.options.repeatcount;
if ( ( cond.options.duration || 0 ) !== dur ) {
/* Changed */
if ( dur === 0 ) {
delete cond.options.duration;
delete cond.options.duration_op;
$('input.re-repeatcount', $ct).prop('disabled', false);
// $('input.re-repeatspan', $ct).prop('disabled', false);
} else {
cond.options.duration = dur;
cond.options.duration_op = $('select.re-durop', $ct).val() || "ge";
}
configModified = true;
}
}
}
var mode = $( 'input.opt-output:checked', $ct ).val() || "";
if ( "L" === mode ) {
/* Latching */
$( '.followopts,.pulseopts', $ct ).prop( 'disabled', true );
$( '.latchopts', $ct ).prop( 'disabled', false );
configModified = configModified || ( undefined !== cond.options.holdtime );
delete cond.options.holdtime;
configModified = configModified || ( undefined !== cond.options.pulsetime );
delete cond.options.pulsetime;
delete cond.options.pulsebreak;
delete cond.options.pulsecount;
if ( undefined === cond.options.latch ) {
cond.options.latch = 1;
configModified = true;
}
} else if ( "P" === mode ) {
/* Pulse output */
$( '.followopts,.latchopts', $ct ).prop( 'disabled', true );
$( '.pulseopts', $ct ).prop( 'disabled', false );
configModified = configModified || ( undefined !== cond.options.holdtime );
delete cond.options.holdtime;
$( 'input.re-pulsetime', $ct ).prop( 'disabled', false );
configModified = configModified || ( undefined !== cond.options.latch );
delete cond.options.latch;
var $f = $( 'input.re-pulsetime', $ct );
var pulsetime = $f.val() || "";
if ( isEmpty( pulsetime ) ) {
pulsetime = 15; /* force a default */
$f.val( pulsetime );
configModified = configModified || pulsetime !== cond.options.pulsetime;
cond.options.pulsetime = pulsetime;
} else {
pulsetime = getInteger( pulsetime );
if ( isNaN( pulsetime ) || pulsetime <= 0 ) {
$f.addClass( 'tberror' );
} else if ( pulsetime !== cond.options.pulsetime ) {
cond.options.pulsetime = pulsetime;
configModified = true;
}
}
var repeats = "repeat" === $( 'select.re-pulsemode', $ct ).val();
$( "div.re-pulsebreakopts", $ct ).toggle( repeats );
if ( repeats ) {
$f = $( 'input.re-pulsebreak', $ct );
pulsetime = parseInt( $f.val() || "" );
if ( isNaN( pulsetime ) || pulsetime <= 0 ) {
$f.addClass( 'tberror' );
} else {
if ( pulsetime !== cond.options.pulsebreak ) {
cond.options.pulsebreak = pulsetime;
configModified = true;
}
}
$f = $( 'input.re-pulsecount', $ct );
var lim = $f.val() || "";
if ( isEmpty( lim ) ) {
if ( 0 !== cond.options.pulsecount ) {
delete cond.options.pulsecount;
configModified = true;
}
} else {
lim = parseInt( lim );
if ( isNaN( lim ) || lim < 0 ) {
$f.addClass( 'tberror' );
} else if ( 0 === lim && cond.options.pulsecount ) {
delete cond.options.pulsecount;
configModified = true;
} else if ( cond.options.pulsecount !== lim ) {
cond.options.pulsecount = lim;
configModified = true;
}
}
} else {
if ( undefined !== cond.options.pulsebreak ) {
configModified = true;
}
delete cond.options.pulsebreak;
delete cond.options.pulsecount;
}
} else {
/* Follow mode (default) */
$( '.pulseopts,.latchopts', $ct ).prop( 'disabled', true );
$( '.followopts', $ct ).prop( 'disabled', false );
configModified = configModified || ( undefined !== cond.options.pulsetime );
delete cond.options.pulsetime;
delete cond.options.pulsebreak;
delete cond.options.pulsecount;
configModified = configModified || ( undefined !== cond.options.latch );
delete cond.options.latch;
/* Hold time (delay reset) */
$dd = $( 'input.re-holdtime', $ct );
if ( isEmpty( $dd.val() ) ) {
/* Empty and 0 are equivalent */
configModified = configModified || ( undefined !== cond.options.holdtime );
delete cond.options.holdtime;
} else {
var holdtime = getInteger( $dd.val() );
if ( isNaN( holdtime ) || holdtime < 0 ) {
delete cond.options.holdtime;
$dd.addClass( 'tberror' );
} else if ( ( cond.options.holdtime || 0 ) !== holdtime ) {
if ( holdtime > 0 ) {
cond.options.holdtime = holdtime;
} else {
delete cond.options.holdtime;
}
configModified = true;
}
}
}
}
/* Options open or not, make sure options expander is highlighted */
var optButton = $( $row.hasClass( 'cond-group' ) ? '.cond-group-header > div > button.re-condmore:first' : '.cond-actions > button.re-condmore', $row );
if ( hasAnyProperty( cond.options ) ) {
optButton.addClass( 'attn' );
} else {
optButton.removeClass( 'attn' );
delete cond.options;
}
$row.has('.tberror').addClass('tberror');
updateSaveControls();
}
/**
* Handler for row change (generic change to some value we don't otherwise
* need additional processing to respond to)
*/
function handleConditionRowChange( ev ) {
var el = $( ev.currentTarget );
var row = el.closest('div.cond-container');
console.log('handleConditionRowChange ' + String(row.attr('id')));
row.addClass( 'tbmodified' );
configModified = true;
updateConditionRow( row, el );
}
/**
* Update current value display for service condition
*/
function updateCurrentServiceValue( row ) {
console.assert( row.hasClass("cond-cond") );
var device = parseInt( $("select.devicemenu", row).val() );
var service = $("select.varmenu", row).val() || "";
var variable = service.replace( /^[^\/]+\//, "" );
service = service.replace( /\/.*$/, "" );
var blk = $( 'div.currval', row );
if ( ! ( isNaN(device) || isEmpty( service ) || isEmpty( variable ) ) ) {
var val = api.getDeviceState( device, service, variable );
if ( undefined === val || false === val ) {
blk.text( 'Current value: (not set)' ).attr( 'title', "This variable is not present in the device state." );
} else {
var abbrev = val.length > 64 ? val.substring(0,61) + '...' : val;
blk.text( 'Current value: ' + abbrev ).attr( 'title', val.length==0 ? "The string is blank/empty." : val );
}
} else {
blk.empty().attr( 'title', "" );
}
}
/**
* Handler for variable change. Change the displayed current value.
*/
function handleConditionVarChange( ev ) {
var $el = $( ev.currentTarget );
var $row = $el.closest('div.cond-container');
updateCurrentServiceValue( $row );
/* Same closing as handleConditionRowChange() */
configModified = true;
updateConditionRow( $row, $el );
}
/* Set up fields for condition based on current operator */
function setUpConditionOpFields( $row, cond ) {
var vv = (cond.value || "").split(/,/);
if ( "housemode" === cond.type ) {
if ( "change" == ( cond.operator || "is" ) ) {
$( 'div.re-modechecks', $row ).hide();
$( 'div.re-modeselects', $row ).show();
menuSelectDefaultInsert( $( 'select.re-frommode', $row ), vv.length > 0 ? vv[0] : "" );
menuSelectDefaultInsert( $( 'select.re-tomode', $row ), vv.length > 1 ? vv[1] : "" );
} else {
$( 'div.re-modechecks', $row ).show();
$( 'div.re-modeselects', $row ).hide();
vv.forEach( function( ov ) {
$('input#' + idSelector( cond.id + '-mode-' + ov ), $row).prop('checked', true);
});
}
} else if ( "service" === cond.type || "var" === cond.type ) {
var op = arrayFindValue( serviceOps, function( v ) { return v.op === cond.operator; } ) || serviceOps[0];
var $inp = $( 'input#' + idSelector( cond.id + '-value' ), $row );
if ( op.args > 1 ) {
if ( $inp.length > 0 ) {
/* Single input field; change this one for double */
$inp.attr( 'id', cond.id + '-val1' ).show();
} else {
/* Already there */
$inp = $( 'input#' + idSelector( cond.id + '-val1' ), $row );
}
/* Work on second field */
var $in2 = $( 'input#' + idSelector( cond.id + '-val2' ), $row );
if ( 0 === $in2.length ) {
$in2 = $inp.clone().attr('id', cond.id + '-val2')
.off( 'change.reactor' ).on( 'change.reactor', handleConditionRowChange );
$in2.insertAfter( $inp );
}
if ( op.optional ) {
$inp.attr( 'placeholder', 'blank=any value' );
$in2.attr( 'placeholder', 'blank=any value' );
} else {
$inp.attr( 'placeholder', "" );
$in2.attr( 'placeholder', "" );
}
/* Labels */
$( 'label.re-secondaryinput', $row ).remove();
var fmt = op.format || "%1,%2";
var lbl = fmt.match( /^([^%]*)%\d+([^%]*)%\d+(.*)$/ );
if ( null !== lbl ) {
if ( !isEmpty( lbl[1] ) ) {
$( '' )
.attr( 'for', cond.id + "-val1" )
.text( lbl[1] )
.insertBefore( $inp );
}
if ( !isEmpty( lbl[2] ) ) {
$( '' )
.attr( 'for', cond.id + "-val2" )
.text( lbl[2] )
.insertBefore( $in2 );
}
if ( !isEmpty( lbl[3] ) ) {
$( '' )
.text( lbl[3] )
.insertAfter( $in2 );
}
}
/* Restore values */
$inp.val( vv.length > 0 ? String(vv[0]) : "" );
$( 'input#' + idSelector( cond.id + '-val2' ), $row ).val( vv.length > 1 ? String(vv[1]) : "" );
} else {
if ( $inp.length == 0 ) {
/* Convert double fields back to single */
$inp = $( 'input#' + idSelector( cond.id + '-val1' ), $row )
.attr( 'id', cond.id + '-value' )
.attr( 'placeholder', '' );
$( 'input#' + idSelector( cond.id + '-val2' ) + ',label.re-secondaryinput', $row ).remove();
}
$inp.val( vv.length > 0 ? String(vv[0]) : "" );
if ( 0 === op.args ) {
$inp.val("").hide();
} else {
$inp.show();
}
}
var $opt = $( 'div.re-nocaseopt', $row );
if ( 0 === ( op.numeric || 0 ) && false !== op.nocase ) {
$opt.show();
$( 'input.nocase', $opt ).prop( 'checked', coalesce( cond.nocase, 1 ) !== 0 );
} else {
$opt.hide();
}
} else if ( "grpstate" === cond.type ) {
/* nada */
} else {
console.log( "Invalid row type in handleConditionOperatorChange(): " + String( cond.type ) );
return;
}
}
/**
* Handler for operator change
*/
function handleConditionOperatorChange( ev ) {
var $el = $( ev.currentTarget );
var val = $el.val();
var $row = $el.closest('div.cond-container');
var cond = getConditionIndex()[ $row.attr( 'id' ) ];
cond.operator = val;
setUpConditionOpFields( $row, cond );
configModified = true;
updateConditionRow( $row, $el );
}
/**
* Handler for device change
*/
function handleDeviceChange( ev ) {
var $el = $( ev.currentTarget );
var newDev = $el.val();
var $row = $el.closest( 'div.cond-container' );
var condId = $row.attr( 'id' );
var cond = getConditionIndex()[condId];
cond.device = parseInt( newDev );
if ( -1 === cond.device ) {
cond.devicename = "(self)";
} else {
var dobj = api.getDeviceObject( cond.device );
cond.devicename = ( dobj || {}).name;
}
delete cond.deviceName; /* remove old form */
configModified = true;
/* Make a new service/variable menu and replace it on the row. */
var newMenu = makeVariableMenu( cond.device, cond.service, cond.variable );
$("select.varmenu", $row).replaceWith( newMenu );
$("select.varmenu", $row).off( 'change.reactor' ).on( 'change.reactor', handleConditionVarChange );
newMenu = makeEventMenu( cond, $row );
$( 'div.eventlist', $row ).replaceWith( newMenu );
updateCurrentServiceValue( $row );
updateConditionRow( $row ); /* pass it on */
}
function handleExpandOptionsClick( ev ) {
var $el = $( ev.currentTarget );
var $row = $el.closest( 'div.cond-container' );
var isGroup = $row.hasClass( 'cond-group' );
var cond = getConditionIndex()[ $row.attr( "id" ) ];
/* If the options container already exists, just show it. */
var $container = $( isGroup ? 'div.condopts' : 'div.cond-body > div.condopts', $row );
if ( $container.length > 0 ) {
/* Container exists and is open, close it, remove it. */
$container.slideUp({
complete: function() {
$container.remove();
}
});
$( 'i', $el ).text( 'expand_more' );
$el.attr( 'title', msgOptionsShow );
if ( $row.hasClass( 'tbautohidden' ) ) {
$( '.cond-group-title button.re-expand', $row ).click();
$row.removeClass( 'tbautohidden' );
}
return;
}
/* Doesn't exist. Create the options container and add options */
$( 'i', $el ).text( 'expand_less' );
$el.attr( 'title', msgOptionsHide );
$container = $( '' ).hide();
var displayed = condOptions[ cond.type || "group" ] || {};
var condOpts = cond.options || {};
/* Options now fall into two general groups: output control, and restrictions. */
/* Output Control */
var out = $( '', { "id": "outputopt", "class": "tboptgroup" } ).appendTo( $container );
$( '
Output Control
' ).append( getWiki( 'Condition-Options' ) ).appendTo( out );
var fs = $( '' ).appendTo( out );
var rid = "output" + getUID();
getRadio( rid, 1, "", "Follow (default) - output remains true while condition matches", "opt-output" )
.appendTo( fs );
if ( false !== displayed.hold ) {
fs.append( '; ' );
$( '' )
.appendTo( fs );
}
/* Pulse group is not displayed for update and change operators; always display if configured, though,
do any legacy configs prior to this restriction being added are still editable. */
if ( ( false !== displayed.pulse && !(cond.operator || "=").match( /^(update|change)/ ) ) ||
condOpts.pulsetime ) {
fs = $( '' ).appendTo( out );
getRadio( rid, 2, "P", "Pulse - output goes true for", "opt-output" )
.appendTo( fs );
$( ' seconds' )
.appendTo( fs );
$( '
' )
.appendTo( fs );
}
if ( false !== displayed.latch ) {
fs = $( '' ).appendTo( out );
getRadio( rid, 3, "L", "Latch - output is held true until external reset", "opt-output" )
.appendTo( fs );
}
/* Restore/configure */
if ( ( condOpts.pulsetime || 0 ) > 0 ) {
$( '.pulseopts', out ).prop( 'disabled', false );
$( 'input#' + idSelector(rid + '2'), out ).prop( 'checked', true );
$( 'input.re-pulsetime', out ).val( condOpts.pulsetime || 15 );
$( 'input.re-pulsebreak', out ).val( condOpts.pulsebreak || "" );
$( 'input.re-pulsecount', out ).val( condOpts.pulsecount || "" );
var pbo = (condOpts.pulsebreak || 0) > 0;
$( 'select.re-pulsemode', out ).val( pbo ? "repeat" : "" );
$( 'div.re-pulsebreakopts', out ).toggle( pbo );
$( '.followopts,.latchopts', out ).prop( 'disabled', true );
} else if ( 0 !== ( condOpts.latch || 0 ) ) {
$( '.latchopts', out ).prop( 'disabled', false );
$( 'input#' + idSelector(rid + '3'), out ).prop( 'checked', true );
$( '.followopts,.pulseopts', out ).prop( 'disabled', true );
$( 'div.re-pulsebreakopts', out ).toggle( false );
} else {
$( '.followopts', out ).prop( 'disabled', false );
$( 'input#' + idSelector(rid + '1'), out ).prop( 'checked', true );
$( '.latchopts,.pulseopts', out ).prop( 'disabled', true );
$( 'div.re-pulsebreakopts', out ).toggle( false );
$( 'input.re-holdtime', out ).val( 0 !== (condOpts.holdtime || 0) ? condOpts.holdtime : "" );
}
/* Restrictions */
if ( displayed.sequence || displayed.duration || displayed.repeat ) {
var rst = $( '', { "id": "restrictopt", "class": "tboptgroup" } ).appendTo( $container );
$( '
Restrictions
' ).append( getWiki( 'Condition-Options' ) ).appendTo( rst );
/* Sequence (predecessor condition) */
if ( displayed.sequence ) {
fs = $( '' ).appendTo( rst );
var $preds = $('');
/* Add groups that are not ancestor of condition */
DOtraverse( (getConditionIndex()).root, function( node ) {
$preds.append( $( '' ).val( node.id ).text( makeConditionDescription( node ) ) );
}, false, function( node ) {
/* If node is not ancestor (line to root) or descendent of cond, allow as predecessor */
return "comment" !== node.type && cond.id !== node.id && !isAncestor( node.id, cond.id ) && !isDescendent( node.id, cond.id );
});
$( '' )
.append( $preds )
.appendTo( fs );
fs.append(' ');
fs.append( getCheckbox( getUID("check"), "0",
"Predecessor must still be true for this condition to go true", "predmode" ) );
$('select.re-predecessor', fs).val( condOpts.after || "" );
$('input.re-predtime', fs).val( condOpts.aftertime || 0 )
.prop( 'disabled', "" === ( condOpts.after || "" ) );
$('input.predmode', fs)
.prop( 'checked', 0 === ( condOpts.aftermode || 0 ) )
.prop( 'disabled', "" === ( condOpts.after || "" ) );
}
/* Duration */
if ( displayed.duration ) {
fs = $( '' ).appendTo( rst );
fs.append('');
}
/* Repeat */
if ( displayed.repeat ) {
fs = $( '' ).appendTo( rst );
fs.append('');
}
if ( ( condOpts.duration || 0 ) > 0 ) {
$('input.re-repeatcount,input.re-repeatspan', rst).prop('disabled', true);
$('input.re-duration', rst).val( condOpts.duration );
$('select.re-durop', rst).val( condOpts.duration_op || "ge" );
} else {
var rc = condOpts.repeatcount || "";
$('input.re-duration', rst).prop('disabled', rc != "");
$('select.re-durop', rst).prop('disabled', rc != "");
$('input.re-repeatcount', rst).val( rc );
$('input.re-repeatspan', rst).prop('disabled', rc=="").val( rc == "" ? "" : ( condOpts.repeatwithin || "60" ) );
}
}
/* Handler for all fields */
$( 'input,select', $container ).on( 'change.reactor', handleConditionRowChange );
/* Add the options container (specific immediate child of this row selection) */
if ( isGroup ) {
$row.append( $container );
if ( 1 === $( '.cond-group-title button.re-collapse', $row ).length ) {
$( '.cond-group-title button.re-collapse', $row ).click();
$row.addClass('tbautohidden');
}
} else {
$row.children( 'div.cond-body' ).append( $container );
}
$container.slideDown();
}
/**
* Update location selector to show correct locations for selected user.
*/
function updateGeofenceLocations( row, loc ) {
var user = $( 'select.re-userid', row ).val() || "";
var mm = $( 'select.re-location', row );
mm.empty();
if ( "" !== user ) {
var ud = api.getUserData();
var l = ud.usergeofences ? ud.usergeofences.length : 0;
for ( var k=0; k' ).val( "" ).text( '--choose location--' ) );
$.each( ud.usergeofences[k].geotags || [], function( ix, v ) {
mm.append( $( '' ).val( v.id ).text( v.name ) );
});
var el = $( 'option[value="' + (loc || "") + '"]' );
if ( el.length == 0 ) {
mm.append( $( '' ).val( loc )
.text( "Deleted location " + String(loc) )
);
}
mm.val( loc || "" );
break;
}
}
}
}
/**
* Handle user selector changed event.
*/
function handleGeofenceUserChange( ev ) {
var row = $( ev.currentTarget ).closest( 'div.cond-container' );
updateGeofenceLocations( row, "" );
handleConditionRowChange( ev );
}
/**
* Handle geofence operator change event.
*/
function handleGeofenceOperatorChange( ev ) {
var el = $( ev.currentTarget );
var row = el.closest( 'div.cond-container' );
var val = el.val() || "is";
if ( "at" === val || "notat" === val ) {
$( 'div.re-geolong', row ).show();
$( 'div.re-geoquick', row ).hide();
} else {
$( 'div.re-geolong', row ).hide();
$( 'div.re-geoquick', row ).show();
}
handleConditionRowChange( ev );
}
function firstKey( t ) {
for ( var d in t ) {
if ( t.hasOwnProperty( d ) ) return d;
}
return undefined;
}
/**
* Make event menu from static JSON eventlist for device
*/
function makeEventMenu( cond, $row ) {
var el = $( '' );
var btnid = getUID('btn');
el.append( '' );
$( 'button.device-triggers', el ).attr( 'id', btnid );
var mm = $( '' ).attr( 'aria-labelledby', btnid );
el.append( mm );
var myid = api.getCpanelDeviceId();
var myself = -1 === cond.device || cond.device === myid;
var events;
if ( ! myself ) {
if ( isALTUI ) {
/* AltUI doesn't implement getDeviceTemplate() as of 2019-06-09 */
var dobj = api.getDeviceObject( myself ? myid : cond.device );
var eobj = dobj ? api.getEventDefinition( dobj.device_type ) || {} : {};
/* AltUI returns object; reduce to array */
events = [];
for ( var ie=0; undefined !== eobj[String(ie)] ; ie++ ) {
events.push( eobj[String(ie)] );
}
} else {
var dtmp = api.getDeviceTemplate( myself ? myid : cond.device );
events = dtmp ? dtmp.eventList2 : false;
}
}
if ( events && events.length > 0 ) {
var wrapAction = function( eventinfo, cond, $row ) {
return function( ev ) {
var el = $( ev.target );
cond.service = el.data( 'service' ) || "?";
cond.variable = el.data( 'variable' ) || "?";
cond.operator = el.data( 'operator' ) || "=";
cond.value = el.data( 'value' ) || "";
delete cond.nocase;
var sk = cond.service + "/" + cond.variable;
menuSelectDefaultInsert( $( 'select.varmenu', $row ), sk );
$( 'select.opmenu', $row ).val( cond.operator );
setUpConditionOpFields( $row, cond );
$( 'input#' + idSelector( cond.id + '-value' ), $row ).val( cond.value );
configModified = true;
updateCurrentServiceValue( $row );
updateConditionRow( $row, $( ev ) );
$( 'select.varmenu', $row ).focus();
/* Hotfix 20103-01: Older UI/firmware (<=1040 at least) seems to need this
to prevent the button click from returning to the UI to the
dashboard, which is truly bizrre. */
ev.preventDefault();
};
};
var reptext = function( s ) {
return ( s || "?" ).replace( /_DEVICE_NAME_/g, "device" ).replace( /_ARGUMENT_VALUE_/g, "value" );
};
var lx = events.length;
for ( var ix=0; ix' );
item.attr( 'id', cx.id );
k = firstKey( cx.serviceStateTable );
item.data('service', cx.serviceId);
item.data('variable', k);
item.data('operator', (cx.serviceStateTable[k] || {}).comparisson || "=");
item.data('value', String( cx.serviceStateTable[k].value ) );
txt = reptext( (cx.label || {}).text || String(cx.id) );
item.html( txt );
mm.append( item );
item.on( 'click.reactor', wrapAction( cx, cond, $row ) );
} else { /* argumentList */
var ly = cx.argumentList ? cx.argumentList.length : 0;
for ( var iy=0; iy' );
item.attr( 'id', cx.id );
item.data('service', cx.serviceId);
item.data( 'variable', arg.name );
item.data( 'operator', arg.comparisson || "=" );
k = firstKey( av );
item.data( 'value', String( av[k] || "" ) );
item.attr( 'id', arg.id );
item.html( reptext( av.HumanFriendlyText.text || "(invalid device_json description)" ) );
mm.append( item );
item.on( 'click.reactor', wrapAction( cx, cond, $row ) );
}
} else {
item = $( '' );
item.data( 'id', cx.id );
item.data('service', cx.serviceId);
item.data( 'variable', arg.name );
item.data( 'operator', arg.comparisson || "=" );
item.data( 'value',
( undefined === arg.defaultValue || arg.optional ) ? "" : String( arg.defaultValue ) );
item.attr( 'id', arg.id );
item.html( reptext( arg.HumanFriendlyText.text || "(invalid device_json description)" ) );
mm.append( item );
item.on( 'click.reactor', wrapAction( cx, cond, $row ) );
}
}
}
}
}
if ( $( 'a', mm ).length > 0 ) {
mm.append( $( '' ) );
mm.append( $( '' )
.text( "In addition to the above device-defined events, you can select any state variable defined on the device and test its value." ) );
} else {
mm.append( $( '' ).text( "This device does not define any events." ) );
}
return el;
}
/**
* Set condition fields and data for type. This also replaces existing
* data from the passed condition. The condition must have at least
* id and type keys set (so new conditions may be safely be otherwise
* empty).
*/
function setConditionForType( cond, row ) {
var op, k, v, mm, fs, el, dobj;
if ( undefined === row ) {
row = $( 'div.cond-container#' + idSelector( cond.id ) );
}
var container = $('div.params', row).empty().addClass("form-inline");
container.closest( 'div.cond-body' ).addClass("form-inline");
row.children( 'button.re-condmore' ).prop( 'disabled', "comment" === cond.type );
switch (cond.type) {
case "":
break;
case 'comment':
container.removeClass("form-inline");
container.closest("div.cond-body").removeClass("form-inline");
container.append('');
$('input', container).on( 'change.reactor', handleConditionRowChange ).val( cond.comment || "" );
if ( "cond0" === cond.id && ( cond.comment || "").match( /^Welcome to your new Reactor/i ) ) {
$( '