From 3909d6a9f7e04e5bec67cf55ee7546a9873bdd76 Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Wed, 31 Aug 2016 19:10:28 -0400 Subject: [PATCH 01/49] Initial implementation for dimmer value and scene support --- I_DeusExMachinaII1.xml | 197 ++++++++++++++++++------ J_DeusExMachinaII1_UI7.js | 316 +++++++++++++++++++++++++++++++------- 2 files changed, 415 insertions(+), 98 deletions(-) diff --git a/I_DeusExMachinaII1.xml b/I_DeusExMachinaII1.xml index fc55c05..f9fada2 100644 --- a/I_DeusExMachinaII1.xml +++ b/I_DeusExMachinaII1.xml @@ -13,7 +13,7 @@ SWITCH_SID = "urn:upnp-org:serviceId:SwitchPower1" DIMMER_TYPE = "urn:schemas-upnp-org:device:DimmableLight:1" DIMMER_SID = "urn:upnp-org:serviceId:Dimming1" - DEMVERSION = 20300 + DEMVERSION = 20302 STATE_STANDBY = 0 STATE_IDLE = 1 @@ -62,19 +62,23 @@ end -- DEM cycles lights between sunset and the user-specified off time. This function returns 0 - -- if the current time is between sunset and off; otherwise 1. + -- if the current time is between sunset and off; otherwise 1. Note that all times are reduced + -- to minutes-since-midnight units. local function isBedtime() local testing = getVarNumeric("TestMode", 0) if (testing ~= 0) then luup.log('DeusExMachinaII::isBedtime(): TestMode is on') end - if (not (testing or luup.is_night())) then - return 1 - end + + -- Establish the lights-out time local bedtime = 1439 -- that's 23:59 in minutes since midnight (default) - local bedtime_tmp = luup.variable_get(SID, "LightsOut", lul_device) + local bedtime_tmp = luup.variable_get(SID, "LightsOut", lul_device) if (bedtime_tmp ~= nil) then bedtime_tmp = tonumber(bedtime_tmp,10) if (bedtime_tmp >= 0 and bedtime_tmp < 1440) then bedtime = bedtime_tmp end end + + -- Figure out our sunset time. Note that if we make this inquiry after sunset, MiOS + -- returns the time of tomorrow's sunset. But, that's not different enough from today's + -- that it really matters to us, so go with it. local date = os.date('*t', luup.sunset()) local sunset = date['hour'] * 60 + date['min'] if (testing ~= 0) then @@ -82,12 +86,16 @@ if (s ~= nil) then sunset = s end luup.log('DeusExMachinaII::isBedtime(): testing mode sunset override time is ' .. tostring(sunset)) end + + -- And the current time. date = os.date('*t') local time = date['hour'] * 60 + date['min'] + + -- Figure out if we're betweeen sunset and lightout (ret=0) or not (ret=1) if (testing ~= 0) then luup.log('DeusExMachinaII:isBedtime(): times (mins since midnight) are now=' .. tostring(time) .. ', sunset=' .. tostring(sunset) .. ', bedtime=' .. tostring(bedtime)) end - local ret = 1; + local ret = 1 -- guilty until proven innocent if (bedtime > sunset) then -- Case 1: bedtime is after sunset (i.e. between sunset and midnight) if (time >= sunset and time < bedtime) then @@ -103,14 +111,17 @@ return ret end - -- Get the list of controlled devices from our device state, parse to table of device IDs. - local function getDeviceList() - local s = luup.variable_get(SID, "Devices", lul_device) or "" + -- Take a string and split it around sep, returning table (indexed) of substrings + -- For example abc,def,ghi becomes t[1]=abc, t[2]=def, t[3]=ghi + -- Returns: table of values, count of values (integer >= 0) + local function split(s, sep) local t = {} - local k = 1 local n = 0 + if (#s == 0) then return t,n end -- empty string returns nothing + local i,j + local k = 1 repeat - local i = string.find(s, ',', k) + i, j = string.find(s, sep or "%s*,%s*", k) if (i == nil) then table.insert(t, string.sub(s, k, -1)) n = n + 1 @@ -118,36 +129,134 @@ else table.insert(t, string.sub(s, k, i-1)) n = n + 1 - k = i + 1 + k = j + 1 end until k > string.len(s) return t, n end + -- Return true if a specified scene has been run (i.e. on the list) + local function isSceneOn(spec) + local stateList = luup.variable_get(SID, "ScenesRunning", lul_device) or "" + for i in string.gfind(stateList, "[^,]+") do + if (i == spec) then return true end + end + return false + end + + -- Mark or unmark a scene as having been run + local function updateSceneState(spec, isOn) + local stateList = luup.variable_get(SID, "ScenesRunning", lul_device) or "" + local i + local t = {} + for i in string.gfind(stateList, "[^,]+") do + t[i] = 1 + end + if (isOn) then + t[spec] = 1 + else + t[spec] = nil + end + stateList = "" + for i in pairs(t) do stateList = stateList .. "," .. tostring(i) end + luup.variable_set(SID, "ScenesRunning", string.sub(stateList, 2, -1), lul_device) + end + + -- Run "final" scene, if defined. This scene is run after all other devices have been + -- turned off. + local function runFinalScene() + local scene = getVarNumeric("FinalScene", nil) + if (scene ~= nil) then + luup.log("DeusExMachina::runFinalScene(): running final scene " .. tostring(scene)) + luup.call_action("urn:micasaverde-com:serviceId:HomeAutomationGateway1", "RunScene", { SceneNum=scene }, 0) + end + end + + -- Get the list of controlled devices from our device state, parse to table of device IDs. + local function getDeviceList() + local s = luup.variable_get(SID, "Devices", lul_device) or "" + return split(s) + end + -- Light on or off? Returns boolean local function isDeviceOn(devid) - local t = luup.devices[devid].device_type - local val = 0 - if (t == SWITCH_TYPE) then - val = luup.variable_get(SWITCH_SID, "Status", devid) - elseif (t == DIMMER_TYPE) then - val = luup.variable_get(DIMMER_SID, "LoadLevelStatus", devid) + local first = string.upper(string.sub(devid, 1, 1)) + if (first == "S") then +luup.log("DEMII::isDeviceOn(): handling scene spec " .. devid) + return isSceneOn(devid) + end + + -- Handle as switch or dimmer +luup.log("DEMII::isDeviceOn(): handling device spec " .. devid) + local r = tonumber(string.match(devid, '%d+'), 10) +luup.log("DEMII::isDeviceOn(): device number seems to be " .. tostring(r)) + local val = "0" + if (luup.devices[r] ~= nil) then + local t = luup.devices[r].device_type + if (t == SWITCH_TYPE) then + val = luup.variable_get(SWITCH_SID, "Status", r) + elseif (t == DIMMER_TYPE) then + val = luup.variable_get(DIMMER_SID, "LoadLevelStatus", r) + else + luup.log("DeusExMachinaII::isDeviceOn(): device " .. tostring(devid) .. " unknown device_type " .. tostring(t)) + return false + end else - luup.log("DeusExMachinaII::isDeviceOn(): device " .. tostring(devid) .. " unknown device_type " .. tostring(t)) + luup.log("DeusExMachinaII::isDeviceOn(): device spec " .. tostring(devid) .. " device " .. tostring(r) .. ", device not found in luup.devices") + return false end return val ~= "0" end - -- Control device + -- Control device. Device is a string, expected to be a pure integer (in which case the device is assumed to be a switch or dimmer), + -- or a string in the form Sxx:yy, in which case xx is an "on" scene to run, and yy is an "off" scene to run. local function deviceControl(devid, turnOn) - local t = luup.devices[devid].device_type - local lvl = 0 - if (t == DIMMER_TYPE) then - if (turnOn) then lvl = 100 end - luup.call_action(DIMMER_SID, "SetLoadLevelTarget", {newLoadlevelTarget=lvl}, devid) -- note odd case inconsistency - elseif (t == SWITCH_TYPE) then - if (turnOn) then lvl = 1 end - luup.call_action(SWITCH_SID, "SetTarget", {newTargetValue=lvl}, devid) + luup.log("DeusExMachinaII::deviceControl(): devid=" .. tostring(devid) .. ", turnOn=" .. tostring(turnOn)) + local first = string.upper(string.sub(devid, 1, 1)) + if first == "S" then +luup.log("DEMII::deviceControl(): handling scene spec " .. devid) + i, j, onScene, offScene = string.find(string.sub(devid, 2), "(%d+)-(%d+)") + if (i == nil) then + luup.log("DeusExMachina::deviceControl(): malformed scene spec=" .. devid) + return + end + onScene = tonumber(onScene, 10) + offScene = tonumber(offScene, 10) + if luup.scenes[onScene] == nil or luup.scenes[offScene] == nil then + -- Both on scene and off scene must exist (defensive). + luup.log("DeusExMachinaII::deviceControl(): one or both of the scenes in " .. tostring(devid) .. " not found in luup.scenes!") + return + end +luup.log("DEMII::deviceControl(): on scene is " .. tostring(onScene) .. ", off scene is " .. tostring(offScene)) + local targetScene + if (turnOn) then targetScene = onScene else targetScene = offScene end + luup.call_action("urn:micasaverde-com:serviceId:HomeAutomationGateway1", "RunScene", { SceneNum=targetScene }, 0) + updateSceneState(devid, turnOn) + else + local lvl = 100 + local k = string.find(devid, '=') + if k ~= nil then + _, _, devid, lvl = string.find(devid, "(%d+)=(%d+)") + lvl = tonumber(lvl, 10) + end + devid = tonumber(devid, 10) +luup.log("DEMII::deviceControl(): handling device " .. tostring(devid) .. ", level " .. tostring(lvl)) + if luup.devices[devid] == nil then + luup.log("DeusExMachinaII::deviceControl(): device " .. tostring(devid) .. " not found in luup.devices"); + return + end + local t = luup.devices[devid].device_type + if not turnOn then + lvl = 0 + end + if (t == DIMMER_TYPE) then + luup.call_action(DIMMER_SID, "SetLoadLevelTarget", {newLoadlevelTarget=lvl}, devid) -- note odd case inconsistency + elseif (t == SWITCH_TYPE) then + if turnOn then lvl = 1 end + luup.call_action(SWITCH_SID, "SetTarget", {newTargetValue=lvl}, devid) + else + luup.log("DeusExMachinaII: deviceControl(): can't determine device type of devspec=" .. devid) + end end end @@ -159,17 +268,16 @@ local i local on = {} local n = 0 - for i = 1, max, 1 do - local deviceId = tonumber(devs[i],10) - if (isDeviceOn(deviceId)) then - table.insert(on, deviceId) + for i = 1,max do + if (isDeviceOn(devs[i])) then + table.insert(on, devs[i]) n = n + 1 end end if (n > 0) then i = math.random(1, n) deviceControl(on[i], false) - luup.log("DeusExMachinaII::turnOffLight(): set " .. on[i] .. " to OFF; " .. tostring(n-1) .. " devices still on.") + luup.log("DeusExMachinaII::turnOffLight(): turned " .. tostring(on[i]) .. " OFF, " .. tostring(n-1) .. " devices still on.") if (n > 1) then return 1 end @@ -259,6 +367,7 @@ -- Enable DEM by setting a new cycle stamp and calling an initial cycle directly. function deusEnable() luup.log("DeusExMachinaII::deusEnable(): enabling...") + luup.variable_set(SID, "ScenesRunning", "", lul_device) -- start with a clean slate runStamp = os.time() deusStep(runStamp) end @@ -272,7 +381,7 @@ local devs, count devs, count = getDeviceList() while count > 0 do - deviceControl(tonumber(devs[count],10), false) + deviceControl(devs[count], false) count = count - 1 end end @@ -370,7 +479,7 @@ local t = tonumber(s, 10) local hh = math.floor(t / 60) local mm = math.mod(t, 60) - t = os.date('*t'); + t = os.date('*t') t['hour'] = hh t['min'] = mm t['sec'] = 0 @@ -388,6 +497,7 @@ maxdelay = getVarNumeric("MaxOffDelay", 300) if (turnOffLight() == 0) then -- No more lights to turn off, arm for next sunset + runFinalScene() luup.variable_set(SID, "State", STATE_IDLE, lul_device) local delay = sunset - os.time() + math.random(mindelay,maxdelay) luup.log("DeusExMachina::deusStep(): all lights out; waiting for next sunset in " .. delay) @@ -403,15 +513,14 @@ devs, max = getDeviceList() if (max > 0) then local change = math.random(1, max) - local deviceId = tonumber(devs[change],10) - if (deviceId ~= nil) then - local status = luup.variable_get(SWITCH_SID, "Status", deviceId) - if (isDeviceOn(deviceId)) then - deviceControl(deviceId, false) - luup.log("DeusExMachinaII::deusStep(): set " .. deviceId .. " to OFF") + local devspec = devs[change] + if (devspec ~= nil) then + if (isDeviceOn(devspec)) then + deviceControl(devspec, false) + luup.log("DeusExMachinaII::deusStep(): set " .. devspec .. " to OFF") else - deviceControl(deviceId, true) - luup.log("DeusExMachinaII::deusStep(): set " .. deviceId .. " to ON") + deviceControl(devspec, true) + luup.log("DeusExMachinaII::deusStep(): set " .. devspec .. " to ON") end end else diff --git a/J_DeusExMachinaII1_UI7.js b/J_DeusExMachinaII1_UI7.js index 487d57a..f2c171a 100644 --- a/J_DeusExMachinaII1_UI7.js +++ b/J_DeusExMachinaII1_UI7.js @@ -8,7 +8,8 @@ var DeusExMachinaII = (function(api) { var myModule = {}; var deusDevice = api.getCpanelDeviceId(); - var controlled; + var controlled = []; + var sceneNamesById = []; function onBeforeCpanelClose(args) { console.log('handler for before cpanel close'); @@ -18,6 +19,12 @@ var DeusExMachinaII = (function(api) { api.registerEventHandler('on_ui_cpanel_before_close', myModule, 'onBeforeCpanelClose'); } + function safe(obj) { + if (obj === undefined) return "undefined" + else if (obj instanceof jQuery || obj.constructor.prototype.jquery) return "jQuery[" + obj.length + "]"; + else return '(' + typeof(obj) + ')' + obj.toString(); + } + function isLight(device) { switch(device.device_type) { case "urn:schemas-upnp-org:device:BinaryLight:1": @@ -29,15 +36,85 @@ var DeusExMachinaII = (function(api) { } } - function getControlled() { + function getControlledList() { var list = get_device_state(deusDevice, serviceId, "Devices", 0); if (typeof(list) == "undefined" || list.match(/^\s*$/)) { return []; } - var res = list.split(','); - return res; + return list.split(','); } - + + function updateControlledList() { + controlled = []; + jQuery('input.controlled-device:checked').each( function( ix, obj ) { + var devid = jQuery(obj).attr('id').substr(6); + var level = 100; + var ds = jQuery('div#slider' + devid); + if (ds.length == 1) + level = ds.slider('option','value'); + if (level < 100) + devid += '=' + level; + controlled.push(devid); + }); + jQuery('.controlled-scenes').each( function( ix, obj ) { + var devid = jQuery(obj).attr('id'); + console.log('updateControlledList: handling scene pair ' + devid); + controlled.push(devid); + }); + + var s = controlled.join(','); + console.log('Updating controlled list to ' + s); + api.setDeviceStatePersistent(deusDevice, serviceId, "Devices", s, 0); + } + + // Find a controlled device in the Devices list + function findControlledDevice(deviceId) + { + for (var k=0; k= controlled.length) return undefined; + var ret = {}; + var c = controlled[ix]; + ret.index = ix; + ret.raw = c; + if (c.charAt(0) == 'S') { + ret.type = "scene"; + var l = c.indexOf('-'); + ret.onScene = c.substr(1,l-1); // portion after S and before - + ret.offScene = c.substr(l+1); // after - + } else { + ret.type = "device"; + var l = c.indexOf('='); + var d,v + if (l < 0) { + d = c; + v = 100; + } else { + d = c.substr(0,l); + v = c.substr(l+1); + } + ret.device = d; + ret.value = v; + } + return ret; + } + + function findControlledSceneSpec(sceneSpec) + { + return jQuery.inArray(sceneSpec, controlled); + } + function timeMinsToStr(totalMinutes) { var hours = Math.floor(totalMinutes / 60); @@ -55,18 +132,83 @@ var DeusExMachinaII = (function(api) { { api.setDeviceStatePersistent(deusDevice, serviceId, "LightsOut", timeMins, 0); } - + + function saveFinalScene(uiObj) + { + var scene = ""; + if (uiObj.selectedIndex > 0) + scene = uiObj.options[uiObj.selectedIndex].value; + api.setDeviceStatePersistent(deusDevice, serviceId, "FinalScene", scene, 0); + } + + function clean(name, dflt) + { + if (dflt === undefined) dflt = '(undefined)'; + if (name === undefined) name = dflt; + return name; + } + + function getScenePairDisplay(onScene, offScene) + { + var html = ""; + var divid = 'S' + onScene.toString() + '-' + offScene.toString(); + html += '
  • '; + html += 'remove_circle_outline'; + html += ' On: ' + clean(sceneNamesById[onScene], '(missing scene)') + '; Off: ' + clean(sceneNamesById[offScene], '(missing scene)'); + html += '
  • '; // controlled + return html; + } + + function addScenePair(onScene, offScene) + { + var sceneSpec = 'S' + onScene.toString() + '-' + offScene.toString(); + var index = findControlledSceneSpec(sceneSpec); + if (index < 0) { + var html = getScenePairDisplay(onScene, offScene); + jQuery('ul#scenepairs').append(html); + updateControlledList(); + + jQuery("select#addonscene").prop("selectedIndex", 0); + jQuery("select#addoffscene").prop("selectedIndex", 0); + } + } + + function removeScenePair(spec) + { + var index = findControlledSceneSpec(spec); + if (index >= 0) { + jQuery('li#' + spec).remove(); + updateControlledList(); + } + } + function updateDeusControl(deviceId) { - var index = jQuery.inArray(deviceId.toString(), controlled); // PHR 03 + var index = findControlledDevice(deviceId); + // console.log('checkbox ' + deviceId + ' in controlled at ' + index); if (index >= 0) { - controlled.splice(index, 1); - + // Remove device + jQuery("input#device" + deviceId).prop("checked", false); + jQuery("div#slider" + deviceId).slider("option", "disabled", true); + jQuery("div#slider" + deviceId).slider("option", "value", 1); } else { - controlled.push(deviceId); + // Add device + jQuery("input#device" + deviceId).prop("checked", true); + jQuery("div#slider" + deviceId).slider("option", "disabled", false); + jQuery("div#slider" + deviceId).slider("option", "value", 100); + } + updateControlledList(); + } + + function changeDimmerSlider( obj, val ) + { + // console.log('changeDimmerSlider(' + obj.attr('id') + ', ' + val + ')'); + var deviceId = obj.attr('id').substr(6); + var ix = findControlledDevice(deviceId); + if (ix >= 0) { + controlled[ix] = deviceId + (val < 100 ? '=' + val : ""); // 100% is assumed if not specified + updateControlledList(); } - - api.setDeviceStatePersistent(deusDevice, serviceId, "Devices", controlled.join(','), 0); // PHR 01 } function changeHouseModeSelector( eventObject ) @@ -101,16 +243,30 @@ var DeusExMachinaII = (function(api) { init(); var i, j, roomObj, roomid, html = ""; - html += "
    "; - html += " (HH:MM)"; - html += "

    "; - html += "
    "; + html += ''; + html += ''; + html += ''; + html += "

    Lights-Out Time


    "; + html += " (HH:MM)"; + + html += "

    House Modes

    "; + html += "
    "; html += ' Home'; html += '   Away'; html += '   Night'; html += '   Vacation'; - html += "

    "; var devices = api.getListOfDevices(); var rooms = []; @@ -139,45 +295,112 @@ var DeusExMachinaII = (function(api) { } ); - html += "

     

    "; - controlled = getControlled(); + html += "

    Controlled Devices

    "; + controlled = getControlledList(); for (j=0; j"; + html += '

    ' + roomObj.name + "

    "; for (i=0; i= 0) { // PHR 03 - html += " checked=\"true\""; - } + html += '= 0) + html += ' checked="true"'; html += " onChange=\"DeusExMachinaII.updateDeusControl('" + roomObj.devices[i].id + "')\""; html += " /> "; html += "#" + roomObj.devices[i].id + " "; html += roomObj.devices[i].name; + if (roomObj.devices[i].device_type == "urn:schemas-upnp-org:device:DimmableLight:1") html += '
    '; html += "
    \n"; } } - html += "
    "; - - // Finish up + html += "
    "; // devs + // Handle scene pairs + html += '

    Scene Control

    '; + html += 'In addition to controlling individual devices, DeusExMachinaII can also run scenes. Scenes are specified in pairs: a scene to do something (the "on" scene), and a scene to undo it (the "off" scene). To add a scene pair, select an "on" scene and an "off" scene and click the green plus. To remove a configured scene pair, click the red minus next to it.'; + html += '
    '; + + // Final scene + html += '

    Final Scene

    The final scene, if specified, is run after all other devices have been turned off during a lights-out cycle.
    '; + + html += '

    More Information

    If you need more information about configuring DeusExMachinaII, please see the README in our GitHub repository.'; + + // Push generated HTML to page api.setCpanelContent(html); // Restore time field var time = "23:59"; var timeMins = parseInt(api.getDeviceState(deusDevice, serviceId, "LightsOut")); if (!isNaN(timeMins)) - { time = timeMinsToStr(timeMins); - } jQuery("#deusExTime").val(time); // Restore house modes var houseModes = parseInt(api.getDeviceState(deusDevice, serviceId, "HouseModes")); for (var k=1; k<=4; ++k) { - if (houseModes & (1<= 0) { + var info = DeusExMachinaII.getControlled(ix); +console.log('found for ' + info.type + ' ' + info.device + ' with value=' + info.value + ', raw=' + info.raw + ", restoring slider value..."); + jQuery(obj).slider("option", "value", info.type == "device" ? info.value : 100); + } + }); + }); + + // Load sdata to get scene list. Populate menus, load controlled scene pairs, final scene. + jQuery.ajax({ + url: api.getDataRequestURL(), + data: { 'id' : 'sdata' }, + dataType: 'json', + success: function( data, status ) { + var menu = ""; + /* global */ sceneNamesById = []; + jQuery.each( data.scenes, function( ix, obj ) { + menu += ''; + sceneNamesById[obj.id] = obj.name; + }); + jQuery('select#addonscene').append(menu); + jQuery('select#addoffscene').append(menu); + validateScene(); + + for (var k=0; k Date: Sun, 25 Sep 2016 13:20:22 -0400 Subject: [PATCH 02/49] List any device that would respond to SwitchPower1 or Dimming1 basics. This should give us reach to control Philips Hue, virtual switches, and other devices that can be connected. It would be nice if the API provided a call to return the service list/tree, but alas, we have to probe various status various that would consistent with those services being present. This leads to some oddities in the list, like some motion sensors. We may need to deal with that later. --- J_DeusExMachinaII1_UI7.js | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/J_DeusExMachinaII1_UI7.js b/J_DeusExMachinaII1_UI7.js index f2c171a..63b5eaf 100644 --- a/J_DeusExMachinaII1_UI7.js +++ b/J_DeusExMachinaII1_UI7.js @@ -25,15 +25,17 @@ var DeusExMachinaII = (function(api) { else return '(' + typeof(obj) + ')' + obj.toString(); } - function isLight(device) { - switch(device.device_type) { - case "urn:schemas-upnp-org:device:BinaryLight:1": - case "urn:schemas-upnp-org:device:DimmableLight:1": - return true; - - default: - return false; - } + function isDimmer(devid) { + var v = api.getDeviceState( devid, "urn:upnp-org:serviceId:Dimming1", "LoadLevelStatus" ); + if (v === undefined || v === false) return false; + return true; + } + + function isControllable(devid) { + if (isDimmer(devid)) return true; /* a dimmer is a light */ + var v = api.getDeviceState( devid, "urn:upnp-org:serviceId:SwitchPower1", "Status" ); + if (v === undefined || v === false) return false; + return true; } function getControlledList() { @@ -273,7 +275,7 @@ var DeusExMachinaII = (function(api) { var noroom = { "id": "0", "name": "No Room", "devices": [] }; rooms[noroom.id] = noroom; for (i=0; i'; + if (isDimmer(roomObj.devices[i].id)) html += '
    '; html += "
    \n"; } } From b956811ee3865ad185042c402e4c50c42205e807 Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Sun, 25 Sep 2016 13:25:33 -0400 Subject: [PATCH 03/49] Interim development check-in. Support for more devices by probing service variables (why can't I find a list or tree of services the device supports?). Improve handling of house mode transitions during our active period. We now turn off all lights immediately, as we would if we were disabled, and then wait for another house mode transition (pretty much by polling) throughout the active time period in case it changes back to an active state for us. --- I_DeusExMachinaII1.xml | 390 ++++++++++++++++++++++++----------------- 1 file changed, 226 insertions(+), 164 deletions(-) diff --git a/I_DeusExMachinaII1.xml b/I_DeusExMachinaII1.xml index f9fada2..7f0dee7 100644 --- a/I_DeusExMachinaII1.xml +++ b/I_DeusExMachinaII1.xml @@ -7,21 +7,21 @@ -- Original code and releases 1.x by Andy Lintner (beowulfe) Version 2.0 and beyond by Patrick Rigney (rigpapa/toggledbits). -- A big thanks to Andy for passing the torch so that this great plug-in can live on. -- ------------------------------------------------------------------------------------------------------------------------- - + SID = "urn:toggledbits-com:serviceId:DeusExMachinaII1" SWITCH_TYPE = "urn:schemas-upnp-org:device:BinaryLight:1" SWITCH_SID = "urn:upnp-org:serviceId:SwitchPower1" DIMMER_TYPE = "urn:schemas-upnp-org:device:DimmableLight:1" DIMMER_SID = "urn:upnp-org:serviceId:Dimming1" DEMVERSION = 20302 - + STATE_STANDBY = 0 STATE_IDLE = 1 STATE_CYCLE = 2 STATE_SHUTDOWN = 3 - + runStamp = 0 - + local function checkVersion() local ui7Check = luup.variable_get(SID, "UI7Check", lul_device) or "" if ui7Check == "" then @@ -34,7 +34,7 @@ luup.reload() end end - + -- Get numeric variable, or return default value if not set or blank local function getVarNumeric( name, dflt ) local s = luup.variable_get(SID, name, lul_device) @@ -43,11 +43,11 @@ if (s == nil) then return dflt end return s end - + -- Delete a variable (if we can... read on...) local function deleteVar( name, devid ) if (devid == nil) then devid = luup.device end - -- Interestingly, setting a variable to nil with luup.variable_set does nothing interesting; too bad, it + -- Interestingly, setting a variable to nil with luup.variable_set does nothing interesting; too bad, it -- could have been used to delete variables, since a later get would yield nil anyway. But it turns out -- that using the variableget Luup request with no value WILL delete the variable. local req = "http://127.0.0.1:3480/data_request?id=variableset&DeviceNum=" .. tostring(devid) .. "&serviceId=" .. SID .. "&Variable=" .. name .. "&Value=" @@ -55,12 +55,80 @@ local status, result = luup.inet.wget(req) -- luup.log("DeusExMachinaII::deleteVar(" .. name .. "): status=" .. tostring(status) .. ", result=" .. tostring(result)) end - + -- Shortcut function to return state of Enabled variable - local function isEnabled() - return getVarNumeric("Enabled", 0) + local function isEnabled() + return ( getVarNumeric("Enabled", 0) ~= 0 ) + end + + local function isActiveHouseMode() + -- Fetch our mask bits that tell us what modes we operate in. If 0, we're not checking house mode. + local modebits = getVarNumeric("HouseModes", 0) + if (modebits ~= 0) then + -- Get the current house mode. There seems to be some disharmony in the correct way to go + -- about this, but the method (uncommented) below works. + -- local currentMode = luup.attr_get("Mode") -- alternate method + local currentMode + local status + status, currentMode = luup.inet.wget("http://127.0.0.1:3480/data_request?id=variableget&Variable=Mode", 2) + if status ~= 0 then + luup.log("DeusExMachinaII::isActiveHouseMode(): can't get current house mode, status=" .. tostring(status)) + currentMode = 0 + end + + -- Check to see if house mode bits are non-zero, and if so, apply current mode as mask. + -- If bit is set (current mode is in the bitset), we can run, otherwise skip. + local bit = require("bit") + -- Get the current house mode (1=Home,2=Away,3=Night,4=Vacation) + currentMode = math.pow(2, tonumber(currentMode,10)) + if (bit.band(modebits, currentMode) == 0) then + luup.log('DeusExMachinaII::isActiveHouseMode(): Current mode bit ' .. string.format("0x%x", currentMode) .. ' not in set ' .. string.format("0x%x", modebits)) + return false -- not active in this mode + end + end + return true -- default is we're active in the current house mode + end + + -- Get a random delay from two state variables. Error check. + function getRandomDelay(minStateName,maxStateName,defMin,defMax) + if defMin == nil then defMin = 300 end + if defMax == nil then defMax = 1800 end + local mind = getVarNumeric(minStateName, defMin) + if mind < 1 then mind = 1 elseif mind > 7200 then mind = 7200 end + local maxd = getVarNumeric(maxStateName, defMax) + if maxd < 1 then maxd = 1 elseif maxd > 7200 then maxd = 7200 end + if maxd < mind then maxd = mind end + return math.random( mind, maxd ) + end + + -- Get sunset time in minutes since midnight. May override for test mode value. + function getSunset() + -- Figure out our sunset time. Note that if we make this inquiry after sunset, MiOS + -- returns the time of tomorrow's sunset. But, that's not different enough from today's + -- that it really matters to us, so go with it. + local sunset = luup.sunset() + local testing = getVarNumeric("TestMode", 0) + if (testing ~= 0) then + local m = getVarNumeric( "TestSunset", nil ) -- units are minutes since midnight + if (m ~= nil) then + -- Sub in our test sunset time + local t = os.date('*t', sunset) + t['hour'] = math.floor(m / 60) + t['min'] = math.floor(m % 60) + t['sec'] = 0 + sunset = os.time(t) + end + luup.log('DeusExMachinaII::getSunset(): testing mode sunset override ' .. tostring(m) .. ', as time is ' .. tostring(sunset)) + end + if (sunset <= os.time()) then sunset = sunset + 86400 end + return sunset end - + + function getSunsetMSM() + local t = os.date('*t', getSunset()); + return t['hour']*60 + t['min'] + end + -- DEM cycles lights between sunset and the user-specified off time. This function returns 0 -- if the current time is between sunset and off; otherwise 1. Note that all times are reduced -- to minutes-since-midnight units. @@ -70,30 +138,22 @@ -- Establish the lights-out time local bedtime = 1439 -- that's 23:59 in minutes since midnight (default) - local bedtime_tmp = luup.variable_get(SID, "LightsOut", lul_device) + local bedtime_tmp = luup.variable_get(SID, "LightsOut", lul_device) if (bedtime_tmp ~= nil) then bedtime_tmp = tonumber(bedtime_tmp,10) if (bedtime_tmp >= 0 and bedtime_tmp < 1440) then bedtime = bedtime_tmp end end - - -- Figure out our sunset time. Note that if we make this inquiry after sunset, MiOS - -- returns the time of tomorrow's sunset. But, that's not different enough from today's - -- that it really matters to us, so go with it. - local date = os.date('*t', luup.sunset()) - local sunset = date['hour'] * 60 + date['min'] - if (testing ~= 0) then - local s = getVarNumeric( "TestSunset", nil ) - if (s ~= nil) then sunset = s end - luup.log('DeusExMachinaII::isBedtime(): testing mode sunset override time is ' .. tostring(sunset)) - end - + + -- Figure out our sunset time. + local sunset = getSunsetMSM() + -- And the current time. - date = os.date('*t') + local date = os.date('*t') local time = date['hour'] * 60 + date['min'] - + -- Figure out if we're betweeen sunset and lightout (ret=0) or not (ret=1) - if (testing ~= 0) then - luup.log('DeusExMachinaII:isBedtime(): times (mins since midnight) are now=' .. tostring(time) .. ', sunset=' .. tostring(sunset) .. ', bedtime=' .. tostring(bedtime)) + if (testing ~= 0) then + luup.log('DeusExMachinaII::isBedtime(): times (mins since midnight) are now=' .. tostring(time) .. ', sunset=' .. tostring(sunset) .. ', bedtime=' .. tostring(bedtime)) end local ret = 1 -- guilty until proven innocent if (bedtime > sunset) then @@ -113,7 +173,7 @@ -- Take a string and split it around sep, returning table (indexed) of substrings -- For example abc,def,ghi becomes t[1]=abc, t[2]=def, t[3]=ghi - -- Returns: table of values, count of values (integer >= 0) + -- Returns: table of values, count of values (integer ge 0) local function split(s, sep) local t = {} local n = 0 @@ -134,7 +194,7 @@ until k > string.len(s) return t, n end - + -- Return true if a specified scene has been run (i.e. on the list) local function isSceneOn(spec) local stateList = luup.variable_get(SID, "ScenesRunning", lul_device) or "" @@ -143,7 +203,7 @@ end return false end - + -- Mark or unmark a scene as having been run local function updateSceneState(spec, isOn) local stateList = luup.variable_get(SID, "ScenesRunning", lul_device) or "" @@ -161,7 +221,7 @@ for i in pairs(t) do stateList = stateList .. "," .. tostring(i) end luup.variable_set(SID, "ScenesRunning", string.sub(stateList, 2, -1), lul_device) end - + -- Run "final" scene, if defined. This scene is run after all other devices have been -- turned off. local function runFinalScene() @@ -171,35 +231,38 @@ luup.call_action("urn:micasaverde-com:serviceId:HomeAutomationGateway1", "RunScene", { SceneNum=scene }, 0) end end - + -- Get the list of controlled devices from our device state, parse to table of device IDs. local function getDeviceList() local s = luup.variable_get(SID, "Devices", lul_device) or "" return split(s) end - + -- Light on or off? Returns boolean local function isDeviceOn(devid) local first = string.upper(string.sub(devid, 1, 1)) if (first == "S") then -luup.log("DEMII::isDeviceOn(): handling scene spec " .. devid) +luup.log("DeusExMachinaII::isDeviceOn(): handling scene spec " .. devid) return isSceneOn(devid) end - + -- Handle as switch or dimmer -luup.log("DEMII::isDeviceOn(): handling device spec " .. devid) - local r = tonumber(string.match(devid, '%d+'), 10) -luup.log("DEMII::isDeviceOn(): device number seems to be " .. tostring(r)) +luup.log("DeusExMachinaII::isDeviceOn(): handling device spec " .. devid) + local r = tonumber(string.match(devid, '^%d+'), 10) +luup.log("DeusExMachinaII::isDeviceOn(): device number seems to be " .. tostring(r)) local val = "0" if (luup.devices[r] ~= nil) then - local t = luup.devices[r].device_type - if (t == SWITCH_TYPE) then - val = luup.variable_get(SWITCH_SID, "Status", r) - elseif (t == DIMMER_TYPE) then - val = luup.variable_get(DIMMER_SID, "LoadLevelStatus", r) + local t = luup.variable_get(DIMMER_SID, "LoadLevelStatus", r) + if (t == nil) then + t = luup.variable_get(SWITCH_SID, "Status", r) + if (t == nil) then + luup.log("DeusExMachinaII::isDeviceOn(): device " .. tostring(devid) .. " unknown device type") + return nil + else + val = luup.variable_get(SWITCH_SID, "Status", r) + end else - luup.log("DeusExMachinaII::isDeviceOn(): device " .. tostring(devid) .. " unknown device_type " .. tostring(t)) - return false + val = luup.variable_get(DIMMER_SID, "LoadLevelStatus", r) end else luup.log("DeusExMachinaII::isDeviceOn(): device spec " .. tostring(devid) .. " device " .. tostring(r) .. ", device not found in luup.devices") @@ -207,14 +270,14 @@ luup.log("DEMII::isDeviceOn(): device number seems to be " .. tostring(r)) end return val ~= "0" end - - -- Control device. Device is a string, expected to be a pure integer (in which case the device is assumed to be a switch or dimmer), + + -- Control device. Device is a string, expected to be a pure integer (in which case the device is assumed to be a switch or dimmer), -- or a string in the form Sxx:yy, in which case xx is an "on" scene to run, and yy is an "off" scene to run. local function deviceControl(devid, turnOn) luup.log("DeusExMachinaII::deviceControl(): devid=" .. tostring(devid) .. ", turnOn=" .. tostring(turnOn)) local first = string.upper(string.sub(devid, 1, 1)) if first == "S" then -luup.log("DEMII::deviceControl(): handling scene spec " .. devid) +luup.log("DeusExMachinaII::deviceControl(): handling scene spec " .. devid) i, j, onScene, offScene = string.find(string.sub(devid, 2), "(%d+)-(%d+)") if (i == nil) then luup.log("DeusExMachina::deviceControl(): malformed scene spec=" .. devid) @@ -227,7 +290,7 @@ luup.log("DEMII::deviceControl(): handling scene spec " .. devid) luup.log("DeusExMachinaII::deviceControl(): one or both of the scenes in " .. tostring(devid) .. " not found in luup.scenes!") return end -luup.log("DEMII::deviceControl(): on scene is " .. tostring(onScene) .. ", off scene is " .. tostring(offScene)) +luup.log("DeusExMachinaII::deviceControl(): on scene is " .. tostring(onScene) .. ", off scene is " .. tostring(offScene)) local targetScene if (turnOn) then targetScene = onScene else targetScene = offScene end luup.call_action("urn:micasaverde-com:serviceId:HomeAutomationGateway1", "RunScene", { SceneNum=targetScene }, 0) @@ -240,23 +303,29 @@ luup.log("DEMII::deviceControl(): on scene is " .. tostring(onScene) .. ", off s lvl = tonumber(lvl, 10) end devid = tonumber(devid, 10) -luup.log("DEMII::deviceControl(): handling device " .. tostring(devid) .. ", level " .. tostring(lvl)) +luup.log("DeusExMachinaII::deviceControl(): handling device " .. tostring(devid) .. ", level " .. tostring(lvl)) if luup.devices[devid] == nil then luup.log("DeusExMachinaII::deviceControl(): device " .. tostring(devid) .. " not found in luup.devices"); return end - local t = luup.devices[devid].device_type - if not turnOn then - lvl = 0 - end - if (t == DIMMER_TYPE) then - luup.call_action(DIMMER_SID, "SetLoadLevelTarget", {newLoadlevelTarget=lvl}, devid) -- note odd case inconsistency - elseif (t == SWITCH_TYPE) then - if turnOn then lvl = 1 end - luup.call_action(SWITCH_SID, "SetTarget", {newTargetValue=lvl}, devid) + -- Level for all types is 0 if turning device off + if not turnOn then lvl = 0 end + local t = luup.variable_get(DIMMER_SID, "LoadLevelTarget", devid) + if (t == nil) then + t = luup.variable_get(SWITCH_SID, "Status", devid) + if (t == nil) then + luup.log("DeusExMachinaII: deviceControl(): can't determine device type of devspec=" .. devid) + else + -- Handle as SwitchPower1 + if turnOn then lvl = 1 end +luup.log("DeusExMachinaII: deviceControl(): handling " .. devid .. " as binary light, setting target to " .. lvl) + luup.call_action(SWITCH_SID, "SetTarget", {newTargetValue=lvl}, devid) + end else - luup.log("DeusExMachinaII: deviceControl(): can't determine device type of devspec=" .. devid) - end + -- Handle as Dimming1 +luup.log("DeusExMachinaII: deviceControl(): handling " .. devid .. " as dimmer, setting load level to " .. lvl) + luup.call_action(DIMMER_SID, "SetLoadLevelTarget", {newLoadlevelTarget=lvl}, devid) -- note odd case inconsistency + end end end @@ -269,7 +338,8 @@ luup.log("DEMII::deviceControl(): handling device " .. tostring(devid) .. ", lev local on = {} local n = 0 for i = 1,max do - if (isDeviceOn(devs[i])) then + local devOn = isDeviceOn(devs[i]) + if (devOn ~= nil and devOn) then table.insert(on, devs[i]) n = n + 1 end @@ -279,15 +349,29 @@ luup.log("DEMII::deviceControl(): handling device " .. tostring(devid) .. ", lev deviceControl(on[i], false) luup.log("DeusExMachinaII::turnOffLight(): turned " .. tostring(on[i]) .. " OFF, " .. tostring(n-1) .. " devices still on.") if (n > 1) then - return 1 + return true -- there are still lights to turn off end end end - return 0 + return false -- no more lights to turn off + end + + -- Turn off all lights as fast as we can. Transition through SHUTDOWN state during, + -- in case user has any triggers connected to that state. The caller must immediately + -- set the next state when this function returns (expected would be STANDBY or IDLE). + function clearLights() + local devs, count + devs, count = getDeviceList() + luup.variable_set(SID, "State", STATE_SHUTDOWN, lul_device) + while count > 0 do + deviceControl(devs[count], false) + count = count - 1 + end + runFinalScene() end - + -- runOnce() looks to see if a core state variable exists; if not, a one-time initialization - -- takes place. For us, that means looking to see if an older version of Deus is still + -- takes place. For us, that means looking to see if an older version of Deus is still -- installed, and copying its config into our new config. Then disable the old Deus. local function runOnce() local s = luup.variable_get(SID, "Enabled", lul_device) @@ -328,11 +412,11 @@ luup.log("DEMII::deviceControl(): handling device " .. tostring(devid) .. ", lev luup.variable_set(SID, "Devices", s, lul_device) deleteVar("controlCount", lul_device) end - + -- Finally, turn off old Deus luup.call_action(oldsid, "SetEnabled", { NewEnabledValue = "0" }, olddev) end - + -- Set up some other default config luup.variable_set(SID, "MinCycleDelay", "300", lul_device) luup.variable_set(SID, "MaxCycleDelay", "1800", lul_device) @@ -358,12 +442,12 @@ luup.log("DEMII::deviceControl(): handling device " .. tostring(devid) .. ", lev end end deleteVar("LightsOutTime", lul_device) - + -- Update version last. luup.variable_set(SID, "Version", DEMVERSION, lul_device) end end - + -- Enable DEM by setting a new cycle stamp and calling an initial cycle directly. function deusEnable() luup.log("DeusExMachinaII::deusEnable(): enabling...") @@ -375,37 +459,32 @@ luup.log("DEMII::deviceControl(): handling device " .. tostring(devid) .. ", lev -- Disable DEM and go to standby state. If we are currently cycling (as opposed to idle/waiting for sunset), -- turn off any controlled lights that are on. function deusDisable() - local state = getVarNumeric("State", STATE_STANDBY) + local s = getVarNumeric("State", STATE_STANDBY) luup.log("DeusExMachinaII::deusDisable(): disabling...") - if ( state == STATE_CYCLE or state == STATE_SHUTDOWN ) then - local devs, count - devs, count = getDeviceList() - while count > 0 do - deviceControl(devs[count], false) - count = count - 1 - end + if ( s == STATE_CYCLE or s == STATE_SHUTDOWN ) then + clearLights() end luup.variable_set(SID, "State", STATE_STANDBY, lul_device) end - + -- Initialize. function deusInit(deusDevice) luup.log("DeusExMachinaII::deusInit(): Version 2.3 (2016-08-20), starting up...") - + -- One-time stuff runOnce() - + --check UI version checkVersion() - + -- Start up if we're enabled - if (isEnabled() == 1) then + if (isEnabled()) then deusEnable() else deusDisable() end end - + -- Run a cycle. If we're in "bedtime" (i.e. not between our cycle period between sunset and stop), -- then we'll shut off any lights we've turned on and queue another run for the next sunset. Otherwise, -- we'll toggled one of our controlled lights, and queue (random delay, but soon) for another cycle. @@ -415,124 +494,107 @@ luup.log("DEMII::deviceControl(): handling device " .. tostring(devid) .. ", lev local stepStamp = tonumber(stepStampCheck) luup.log("DeusExMachinaII::deusStep(): wakeup, stamp " .. stepStampCheck) if (stepStamp ~= runStamp) then - luup.log("DeusExMachinaII::deusStep(): another thread running, skipping") + luup.log("DeusExMachinaII::deusStep(): stamp mismatch, another thread running. Bye!") return end - if (isEnabled() ~= 1) then - luup.log("DeusExMachinaII::deusStep(): not enabled, skipping") + if (not isEnabled()) then + luup.log("DeusExMachinaII::deusStep(): not enabled, no more work for this thread...") return end - local modebits = getVarNumeric("HouseModes", 0) - -- local currentMode = luup.attr_get("Mode") - local currentMode - local status - status, currentMode = luup.inet.wget("http://127.0.0.1:3480/data_request?id=variableget&Variable=Mode",0) - + -- Get next sunset as seconds since midnight (approx) + local sunset = getSunset() + local currentState = getVarNumeric("State", 0) - if (currentState == 0 or currentState == 1) then - luup.log("DeusExMachinaII:deusStep(): run in state " + if (currentState == STATE_STANDBY or currentState == STATE_IDLE) then + luup.log("DeusExMachinaII::deusStep(): run in state " .. tostring(currentState) - .. ", modebits=" .. tostring(modebits) - .. ", currentMode=" .. tostring(currentMode) .. ", lightsout=" .. tostring(luup.variable_get(SID, "LightsOut", lul_device)) - .. ", luup.sunset=" .. tostring(luup.sunset()) + .. ", sunset=" .. tostring(sunset) .. ", os.time=" .. tostring(os.time()) ) - luup.log("+++ longitude=" + luup.log("+ long=" .. tostring(luup.longitude) - .. ", latitude=" .. tostring(luup.latitude) - .. ", timezone=" .. tostring(luup.timezone) + .. ", lat=" .. tostring(luup.latitude) + .. ", tz=" .. tostring(luup.timezone) .. ", city=" .. tostring(luup.city) + .. ", luup.sunset=" .. tostring(luup.sunset()) .. ", version=" .. tostring(luup.version) ) end - local runCycle = 1 - - -- Check to see if house mode bits are non-zero, and if so, apply current mode as mask. - -- If bit is set (current mode is in the bitset), we can run, otherwise skip. - if (modebits ~= 0) then - local bit = require("bit") - -- Get the current house mode (1=Home,2=Away,3=Night,4=Vacation) - currentMode = math.pow(2, tonumber(currentMode,10)) - if (bit.band(modebits, currentMode) == 0) then - luup.log('DeusExMachinaII::deusStep(): Current mode bit ' .. string.format("0x%x", currentMode) .. ' not in set ' .. string.format("0x%x", modebits)) - runCycle = 0 - end - end - - if (runCycle and isBedtime() ~= 0) then - luup.log("DeusExMachinaII::deusStep(): lights out time") - runCycle = 0 - end - - -- See if we're in test mode. If so, compute possible sunset override (debug) - local mindelay, maxdelay - local sunset = luup.sunset() - local now = os.time() - local testing = getVarNumeric("TestMode", 0) - if (testing ~= 0) then - -- Note that TestSunset is expressed in MINUTES since midnight - local s = getVarNumeric("TestSunset", nil) - if (s ~= nil) then - local t = tonumber(s, 10) - local hh = math.floor(t / 60) - local mm = math.mod(t, 60) - t = os.date('*t') - t['hour'] = hh - t['min'] = mm - t['sec'] = 0 - sunset = os.time(t) - if ( sunset <= now ) then sunset = sunset + 86400 end - luup.log('DeusExMachinaII::deusStep(): TestMode is on, next sunset is ' .. tostring(sunset) .. " (" .. tostring(sunset-os.time()) .. " from now)") - end + local inActiveTimePeriod = true + if (isBedtime() ~= 0) then + luup.log("DeusExMachinaII::deusStep(): in lights out time") + inActiveTimePeriod = false end - -- See if we've crossed the lights-out time - if (runCycle == 0) then + -- Get going... + local nextCycleDelay = 300 -- a default value to keep us out of hot water + if (not isActiveHouseMode()) then + -- Not in an active house mode. If we're not STANDBY or IDLE, turn everything back off and go to IDLE. + if (currentState ~= STATE_IDLE) then + luup.log("DeusExMachinaII::deusStep(): transitioning to IDLE, not in an active house mode."); + if (currentState ~= STATE_STANDBY) then clearLights() end -- turn off lights quickly unless transitioning from STANDBY + luup.variable_set(SID, "State", STATE_IDLE, lul_device) + else + luup.log("DeusExMachinaII::deusStep(): IDLE in an inactive house mode; waiting for mode change."); + end + + -- Figure out how long to delay. If we're lights-out, delay to next sunset. Otherwise, short delay + -- to re-check house mode, which could change at any time, so we must deal with it. + if (inActiveTimePeriod) then + nextCycleDelay = 300 + else + nextCycleDelay = sunset - os.time() + getRandomDelay("MinOffDelay", "MaxOffDelay", 60, 300) + end + elseif (not inActiveTimePeriod) then luup.log("DeusExMachinaII::deusStep(): running off cycle") luup.variable_set(SID, "State", STATE_SHUTDOWN, lul_device) - mindelay = getVarNumeric("MinOffDelay", 60) - maxdelay = getVarNumeric("MaxOffDelay", 300) - if (turnOffLight() == 0) then - -- No more lights to turn off, arm for next sunset + if (not turnOffLight()) then + -- No more lights to turn off runFinalScene() luup.variable_set(SID, "State", STATE_IDLE, lul_device) - local delay = sunset - os.time() + math.random(mindelay,maxdelay) - luup.log("DeusExMachina::deusStep(): all lights out; waiting for next sunset in " .. delay) - luup.call_delay("deusStep", delay, stepStamp, 1) - return + nextCycleDelay = sunset - os.time() + getRandomDelay("MinOffDelay", "MaxOffDelay", 60, 300) + luup.log("DeusExMachina::deusStep(): all lights out; now IDLE, setting delay to restart cycling at next sunset") + else + nextCycleDelay = getRandomDelay("MinOffDelay", "MaxOffDelay", 60, 300) end else + -- Fully active. Find a random device to control and control it. luup.log("DeusExMachinaII::deusStep(): running toggle cycle") luup.variable_set(SID, "State", STATE_CYCLE, lul_device) - mindelay = getVarNumeric("MinCycleDelay", 300) - maxdelay = getVarNumeric("MaxCycleDelay", 1800) + nextCycleDelay = getRandomDelay("MinCycleDelay", "MaxCycleDelay") local devs, max devs, max = getDeviceList() if (max > 0) then local change = math.random(1, max) local devspec = devs[change] if (devspec ~= nil) then - if (isDeviceOn(devspec)) then - deviceControl(devspec, false) - luup.log("DeusExMachinaII::deusStep(): set " .. devspec .. " to OFF") - else - deviceControl(devspec, true) - luup.log("DeusExMachinaII::deusStep(): set " .. devspec .. " to ON") + local s = isDeviceOn(devspec) + if (s ~= nil) then + if (s) then + deviceControl(devspec, false) + luup.log("DeusExMachinaII::deusStep(): set " .. devspec .. " to OFF") + else + deviceControl(devspec, true) + luup.log("DeusExMachinaII::deusStep(): set " .. devspec .. " to ON") + end end end else luup.log("DeusExMachinaII:deusStep(): no devices to control") end end - + -- Arm for next cycle - -- PHR??? Should we sure delay doesn't roll past LightsOut? Does it matter? - local delay = math.random(mindelay, maxdelay) - luup.call_delay("deusStep", delay, stepStamp, 1) - luup.log("DeusExMachinaII::deusStep(): cycle finished, next in " .. delay .. " seconds") + if nextCycleDelay ~= nil then + luup.log("DeusExMachinaII::deusStep(): cycle finished, next in " .. nextCycleDelay .. " seconds") + if nextCycleDelay < 1 then nextCycleDelay = 60 end + luup.call_delay("deusStep", nextCycleDelay, stepStamp, 1) + else + luup.log("DeusExMachinaII::deusStep(): nil nextCycleDelay, next cycle not scheduled!"); + end end deusInit From b65cdd90cbbed9b386c61e5b629bc7f7490dd854 Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Sun, 25 Sep 2016 13:36:12 -0400 Subject: [PATCH 04/49] Notes about new device list --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 50e4dac..349c8c5 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,9 @@ If no modes are chosen, it is the same as choosing all modes (Deus operates in a Selecting the lights to be controlled is a simple matter of clicking the check boxes. Lights on dimmers cannot be set to values less than 100% in the current version of the plugin. Because the operating cycle of the plug-in is random, any controlled light may be turned on and off several times during the cycling period (between sunset and Lights Out time). +As of version 2.4, all devices are listed that implement the SwitchPower1 and Dimming1 services. This leads to some oddities, +like some motion sensors and thermostats being listed. It may not be entirely obvious (or standard) what a thermostat, for example, might do when you try to turn it off and on like a light, so be careful selecting these devices. + #### Control by Scene #### As of version 2.0 and on UI7, DeusExMachina can be enabled or disabled like a light switch in scenes, through the regular graphical interface (no Lua required). From 2b2e394069d5e36524e8c39dffd09c3f94b2feab Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Sun, 25 Sep 2016 13:47:13 -0400 Subject: [PATCH 05/49] More about scene control in 2.4 --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 349c8c5..76cc514 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,14 @@ the plug-in is random, any controlled light may be turned on and off several tim As of version 2.4, all devices are listed that implement the SwitchPower1 and Dimming1 services. This leads to some oddities, like some motion sensors and thermostats being listed. It may not be entirely obvious (or standard) what a thermostat, for example, might do when you try to turn it off and on like a light, so be careful selecting these devices. +Also new for version 2.4 is the ability to run scenes during the random cycling period. Scenes must be specified in pairs, with +one being the "on" scene and the other being an "off" scene. This not allows more patterned use of lights, but also gives user +the ability to handle device-specific capabilities that would be difficult to track in DEMII. For example, while DEMII can now +turn Philips Hue lights on and off (to dimming levels, even), it cannot control their color because there's no UI for that in +DEMII. But a scene could be used to control that light or a group of lights, with their color. + +Finally, 2.4 adds the ability for a "final scene" to run when DEMII is disabled or turns off the last light after the "lights out" time. This could be used for any purpose. I personally use it to make sure a whole-house off is run, but you could use it to ensure your alarm system is armed, or your garage door is closed, etc. + #### Control by Scene #### As of version 2.0 and on UI7, DeusExMachina can be enabled or disabled like a light switch in scenes, through the regular graphical interface (no Lua required). From 33a699e001edcf45d9913269d8e2e8657afebd72 Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Sun, 25 Sep 2016 14:16:32 -0400 Subject: [PATCH 06/49] Update version number for release candidate --- I_DeusExMachinaII1.xml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/I_DeusExMachinaII1.xml b/I_DeusExMachinaII1.xml index 7f0dee7..101d8b0 100644 --- a/I_DeusExMachinaII1.xml +++ b/I_DeusExMachinaII1.xml @@ -13,7 +13,7 @@ SWITCH_SID = "urn:upnp-org:serviceId:SwitchPower1" DIMMER_TYPE = "urn:schemas-upnp-org:device:DimmableLight:1" DIMMER_SID = "urn:upnp-org:serviceId:Dimming1" - DEMVERSION = 20302 + DEMVERSION = 20400 STATE_STANDBY = 0 STATE_IDLE = 1 @@ -428,13 +428,13 @@ luup.log("DeusExMachinaII: deviceControl(): handling " .. devid .. " as dimmer, end -- Consider per-version changes. - -- v2.3: LightsOutTime (in milliseconds) deprecated, now using LightsOut (in minutes since midnight) s = getVarNumeric("Version", 0) - if (s < DEMVERSION) then - luup.log("DeusExMachinaII::runOnce(): updating config, version " .. tostring(s) .. " < " .. DEMVERSION) + if (s < 20300) then + -- v2.3: LightsOutTime (in milliseconds) deprecated, now using LightsOut (in minutes since midnight) + luup.log("DeusExMachinaII::runOnce(): updating config, version " .. tostring(s) .. " < 20300") s = luup.variable_get(SID, "LightsOut", lul_device) if (s == nil) then - s = getVarNumeric("LightsOutTime") -- get pre-2.2 variable + s = getVarNumeric("LightsOutTime") -- get pre-2.3 variable if (s == nil) then luup.variable_set(SID, "LightsOut", 1439, lul_device) -- default 23:59 else @@ -442,10 +442,10 @@ luup.log("DeusExMachinaII: deviceControl(): handling " .. devid .. " as dimmer, end end deleteVar("LightsOutTime", lul_device) - - -- Update version last. - luup.variable_set(SID, "Version", DEMVERSION, lul_device) end + + -- Update version last. + luup.variable_set(SID, "Version", DEMVERSION, lul_device) end -- Enable DEM by setting a new cycle stamp and calling an initial cycle directly. @@ -469,7 +469,7 @@ luup.log("DeusExMachinaII: deviceControl(): handling " .. devid .. " as dimmer, -- Initialize. function deusInit(deusDevice) - luup.log("DeusExMachinaII::deusInit(): Version 2.3 (2016-08-20), starting up...") + luup.log("DeusExMachinaII::deusInit(): Version 2.4RC1 (2016-09-25), starting up...") -- One-time stuff runOnce() From 4b897d4ee03153749935204cf0db1f3b1841d3d5 Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Sun, 25 Sep 2016 14:16:48 -0400 Subject: [PATCH 07/49] Update version number for release candidate --- D_DeusExMachinaII1_UI7.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/D_DeusExMachinaII1_UI7.json b/D_DeusExMachinaII1_UI7.json index 7bb8eb8..4a4b0fc 100644 --- a/D_DeusExMachinaII1_UI7.json +++ b/D_DeusExMachinaII1_UI7.json @@ -103,7 +103,7 @@ "left": "0", "Label": { "lang_tag": "dem_about", - "text": "DeusExMachina II ver 2.3 2016-08-20
    For documentation or to report bugs, please go to the DeusExMachina Github repository." + "text": "DeusExMachina II ver 2.4RC1 2016-09-25
    For documentation or to report bugs, please go to the DeusExMachina Github repository
    This plugin is offered for use as-is and without warranties of any kind. By using this plugin, you agree to assume all risks of use without limitation.
    ." }, "Display": { "Top": "80", From ba767a1994fda99069043604be0a35b737303c64 Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Wed, 28 Sep 2016 09:38:04 -0400 Subject: [PATCH 08/49] Fix what Deus does when we enable in inactive period. It was just cycling lights off as if it had been running all along. Now touch nothing, but go to idle state and delay to next sunset + random. --- I_DeusExMachinaII1.xml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/I_DeusExMachinaII1.xml b/I_DeusExMachinaII1.xml index 101d8b0..d02c70c 100644 --- a/I_DeusExMachinaII1.xml +++ b/I_DeusExMachinaII1.xml @@ -528,10 +528,16 @@ luup.log("DeusExMachinaII: deviceControl(): handling " .. devid .. " as dimmer, luup.log("DeusExMachinaII::deusStep(): in lights out time") inActiveTimePeriod = false end - + -- Get going... local nextCycleDelay = 300 -- a default value to keep us out of hot water - if (not isActiveHouseMode()) then + if (currentState == STATE_STANDBY and not inActiveTimePeriod) then + -- Transition from STATE_STANDBY (i.e. we're enabling) in the inactive period. + -- Go to IDLE and delay for next sunset. + luup.log("DeusExMachinaII::deusStep(): transitioning to IDLE from STANDBY, waiting for next sunset..."); + nextCycleDelay = sunset - os.time() + getRandomDelay("MinOffDelay", "MaxOffDelay", 60, 300) + luup.variable_set(SID, "State", STATE_IDLE, lul_device) + elseif (not isActiveHouseMode()) then -- Not in an active house mode. If we're not STANDBY or IDLE, turn everything back off and go to IDLE. if (currentState ~= STATE_IDLE) then luup.log("DeusExMachinaII::deusStep(): transitioning to IDLE, not in an active house mode."); From 49fa9fde1684af8e4b153500ce8233dc15dd47d4 Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Thu, 15 Dec 2016 19:10:24 -0500 Subject: [PATCH 09/49] UI support for max number of targets allowed to be "on" at once. --- J_DeusExMachinaII1_UI7.js | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/J_DeusExMachinaII1_UI7.js b/J_DeusExMachinaII1_UI7.js index 63b5eaf..a80f5c8 100644 --- a/J_DeusExMachinaII1_UI7.js +++ b/J_DeusExMachinaII1_UI7.js @@ -237,6 +237,17 @@ var DeusExMachinaII = (function(api) { } alert("Time must be in the format HH:MM (i.e. 22:30)"); } + + function checkMaxTargets() + { + var maxt = jQuery("#maxtargets").val(); + var re = new RegExp("^[0-9]+$"); + if (re.exec(maxt)) { + api.setDeviceStatePersistent(deusDevice, serviceId, "MaxTargetsOn", maxt, 0); + return; + } + alert("Max On Targets must be an integer and >= 0"); + } //////////////////////////// function configureDeus() @@ -259,7 +270,9 @@ var DeusExMachinaII = (function(api) { html += '.color-red { color: #ff0000; }'; html += '.color-green { color: #12805b; }'; html += 'input#deusExTime { text-align: center; }'; + html += 'input#maxtargets { text-align: center; }'; html += ''; + html += "

    Lights-Out Time


    "; html += " (HH:MM)"; @@ -320,16 +333,20 @@ var DeusExMachinaII = (function(api) { // Handle scene pairs html += '

    Scene Control

    '; - html += 'In addition to controlling individual devices, DeusExMachinaII can also run scenes. Scenes are specified in pairs: a scene to do something (the "on" scene), and a scene to undo it (the "off" scene). To add a scene pair, select an "on" scene and an "off" scene and click the green plus. To remove a configured scene pair, click the red minus next to it.'; + html += 'In addition to controlling individual devices, DeusExMachinaII can run scenes. Scenes are specified in pairs: a scene to do something (the "on" scene), and a scene to undo it (the "off" scene). To add a scene pair, select an "on" scene and an "off" scene and click the green plus. To remove a configured scene pair, click the red minus next to it.'; html += '
    '; - - // Final scene - html += '

    Final Scene

    The final scene, if specified, is run after all other devices have been turned off during a lights-out cycle.
     (0=no limit)"; + + // Final scene (the scene that is run when everything has been turned off and DEM is going idle). + html += '

    Final Scene

    The final scene, if specified, is run after all other targets have been turned off during a lights-out cycle.
    '; html += '

    More Information

    If you need more information about configuring DeusExMachinaII, please see the README in our GitHub repository.'; @@ -343,6 +360,12 @@ var DeusExMachinaII = (function(api) { if (!isNaN(timeMins)) time = timeMinsToStr(timeMins); jQuery("#deusExTime").val(time); + + // Restore maxtargets + var maxt = parseInt(api.getDeviceState(deusDevice, serviceId, "MaxTargetsOn")); + if (isNaN(maxt) || maxt < 0) + maxt = 0; + jQuery("#maxtargets").val(maxt); // Restore house modes var houseModes = parseInt(api.getDeviceState(deusDevice, serviceId, "HouseModes")); @@ -416,6 +439,7 @@ console.log('found for ' + info.type + ' ' + info.device + ' with value=' + info onBeforeCpanelClose: onBeforeCpanelClose, changeHouseModeSelector: changeHouseModeSelector, checkTime: checkTime, + checkMaxTargets: checkMaxTargets, updateDeusControl: updateDeusControl, configureDeus: configureDeus, addScenePair: addScenePair, From fe77d0bb9071f5c573598342386d75130543461b Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Thu, 15 Dec 2016 19:10:49 -0500 Subject: [PATCH 10/49] Support for max number of targets allowed on at once. Now RC2. --- I_DeusExMachinaII1.xml | 134 ++++++++++++++++++++++++----------------- 1 file changed, 78 insertions(+), 56 deletions(-) diff --git a/I_DeusExMachinaII1.xml b/I_DeusExMachinaII1.xml index d02c70c..a753ac2 100644 --- a/I_DeusExMachinaII1.xml +++ b/I_DeusExMachinaII1.xml @@ -222,7 +222,7 @@ luup.variable_set(SID, "ScenesRunning", string.sub(stateList, 2, -1), lul_device) end - -- Run "final" scene, if defined. This scene is run after all other devices have been + -- Run "final" scene, if defined. This scene is run after all other targets have been -- turned off. local function runFinalScene() local scene = getVarNumeric("FinalScene", nil) @@ -232,31 +232,31 @@ end end - -- Get the list of controlled devices from our device state, parse to table of device IDs. - local function getDeviceList() + -- Get the list of targets from our device state, parse to table of targets. + local function getTargetList() local s = luup.variable_get(SID, "Devices", lul_device) or "" return split(s) end -- Light on or off? Returns boolean - local function isDeviceOn(devid) - local first = string.upper(string.sub(devid, 1, 1)) + local function isDeviceOn(targetid) + local first = string.upper(string.sub(targetid, 1, 1)) if (first == "S") then -luup.log("DeusExMachinaII::isDeviceOn(): handling scene spec " .. devid) - return isSceneOn(devid) +luup.log("DeusExMachinaII::isDeviceOn(): handling scene spec " .. targetid) + return isSceneOn(targetid) end -- Handle as switch or dimmer -luup.log("DeusExMachinaII::isDeviceOn(): handling device spec " .. devid) - local r = tonumber(string.match(devid, '^%d+'), 10) -luup.log("DeusExMachinaII::isDeviceOn(): device number seems to be " .. tostring(r)) +luup.log("DeusExMachinaII::isDeviceOn(): handling target spec " .. targetid) + local r = tonumber(string.match(targetid, '^%d+'), 10) +luup.log("DeusExMachinaII::isDeviceOn(): target number seems to be " .. tostring(r)) local val = "0" if (luup.devices[r] ~= nil) then local t = luup.variable_get(DIMMER_SID, "LoadLevelStatus", r) if (t == nil) then t = luup.variable_get(SWITCH_SID, "Status", r) if (t == nil) then - luup.log("DeusExMachinaII::isDeviceOn(): device " .. tostring(devid) .. " unknown device type") + luup.log("DeusExMachinaII::isDeviceOn(): target " .. tostring(targetid) .. " unknown device type") return nil else val = luup.variable_get(SWITCH_SID, "Status", r) @@ -265,78 +265,78 @@ luup.log("DeusExMachinaII::isDeviceOn(): device number seems to be " .. tostring val = luup.variable_get(DIMMER_SID, "LoadLevelStatus", r) end else - luup.log("DeusExMachinaII::isDeviceOn(): device spec " .. tostring(devid) .. " device " .. tostring(r) .. ", device not found in luup.devices") + luup.log("DeusExMachinaII::isDeviceOn(): target spec " .. tostring(targetid) .. " device " .. tostring(r) .. ", device not found in luup.devices") return false end return val ~= "0" end - -- Control device. Device is a string, expected to be a pure integer (in which case the device is assumed to be a switch or dimmer), + -- Control target. Target is a string, expected to be a pure integer (in which case the target is assumed to be a switch or dimmer), -- or a string in the form Sxx:yy, in which case xx is an "on" scene to run, and yy is an "off" scene to run. - local function deviceControl(devid, turnOn) - luup.log("DeusExMachinaII::deviceControl(): devid=" .. tostring(devid) .. ", turnOn=" .. tostring(turnOn)) - local first = string.upper(string.sub(devid, 1, 1)) + local function targetControl(targetid, turnOn) + luup.log("DeusExMachinaII::targetControl(): targetid=" .. tostring(targetid) .. ", turnOn=" .. tostring(turnOn)) + local first = string.upper(string.sub(targetid, 1, 1)) if first == "S" then -luup.log("DeusExMachinaII::deviceControl(): handling scene spec " .. devid) - i, j, onScene, offScene = string.find(string.sub(devid, 2), "(%d+)-(%d+)") +luup.log("DeusExMachinaII::targetControl(): handling scene spec " .. targetid) + i, j, onScene, offScene = string.find(string.sub(targetid, 2), "(%d+)-(%d+)") if (i == nil) then - luup.log("DeusExMachina::deviceControl(): malformed scene spec=" .. devid) + luup.log("DeusExMachina::targetControl(): malformed scene spec=" .. targetid) return end onScene = tonumber(onScene, 10) offScene = tonumber(offScene, 10) if luup.scenes[onScene] == nil or luup.scenes[offScene] == nil then -- Both on scene and off scene must exist (defensive). - luup.log("DeusExMachinaII::deviceControl(): one or both of the scenes in " .. tostring(devid) .. " not found in luup.scenes!") + luup.log("DeusExMachinaII::targetControl(): one or both of the scenes in " .. tostring(targetid) .. " not found in luup.scenes!") return end -luup.log("DeusExMachinaII::deviceControl(): on scene is " .. tostring(onScene) .. ", off scene is " .. tostring(offScene)) +luup.log("DeusExMachinaII::targetControl(): on scene is " .. tostring(onScene) .. ", off scene is " .. tostring(offScene)) local targetScene if (turnOn) then targetScene = onScene else targetScene = offScene end luup.call_action("urn:micasaverde-com:serviceId:HomeAutomationGateway1", "RunScene", { SceneNum=targetScene }, 0) - updateSceneState(devid, turnOn) + updateSceneState(targetid, turnOn) else local lvl = 100 - local k = string.find(devid, '=') + local k = string.find(targetid, '=') if k ~= nil then - _, _, devid, lvl = string.find(devid, "(%d+)=(%d+)") + _, _, targetid, lvl = string.find(targetid, "(%d+)=(%d+)") lvl = tonumber(lvl, 10) end - devid = tonumber(devid, 10) -luup.log("DeusExMachinaII::deviceControl(): handling device " .. tostring(devid) .. ", level " .. tostring(lvl)) - if luup.devices[devid] == nil then - luup.log("DeusExMachinaII::deviceControl(): device " .. tostring(devid) .. " not found in luup.devices"); + targetid = tonumber(targetid, 10) +luup.log("DeusExMachinaII::targetControl(): handling device " .. tostring(targetid) .. ", level " .. tostring(lvl)) + if luup.devices[targetid] == nil then + luup.log("DeusExMachinaII::targetControl(): device " .. tostring(targetid) .. " not found in luup.devices"); return end -- Level for all types is 0 if turning device off if not turnOn then lvl = 0 end - local t = luup.variable_get(DIMMER_SID, "LoadLevelTarget", devid) + local t = luup.variable_get(DIMMER_SID, "LoadLevelTarget", targetid) if (t == nil) then - t = luup.variable_get(SWITCH_SID, "Status", devid) + t = luup.variable_get(SWITCH_SID, "Status", targetid) if (t == nil) then - luup.log("DeusExMachinaII: deviceControl(): can't determine device type of devspec=" .. devid) + luup.log("DeusExMachinaII: targetControl(): can't determine device type of devspec=" .. targetid) else -- Handle as SwitchPower1 if turnOn then lvl = 1 end -luup.log("DeusExMachinaII: deviceControl(): handling " .. devid .. " as binary light, setting target to " .. lvl) - luup.call_action(SWITCH_SID, "SetTarget", {newTargetValue=lvl}, devid) +luup.log("DeusExMachinaII: targetControl(): handling " .. targetid .. " as binary light, setting target to " .. lvl) + luup.call_action(SWITCH_SID, "SetTarget", {newTargetValue=lvl}, targetid) end else -- Handle as Dimming1 -luup.log("DeusExMachinaII: deviceControl(): handling " .. devid .. " as dimmer, setting load level to " .. lvl) - luup.call_action(DIMMER_SID, "SetLoadLevelTarget", {newLoadlevelTarget=lvl}, devid) -- note odd case inconsistency +luup.log("DeusExMachinaII: targetControl(): handling " .. targetid .. " as dimmer, setting load level to " .. lvl) + luup.call_action(DIMMER_SID, "SetLoadLevelTarget", {newLoadlevelTarget=lvl}, targetid) -- note odd case inconsistency end end end - - -- Turn off a light, if any is on. Returns 1 if there are more lights to turn off; otherwise 0. - local function turnOffLight() + + -- Get list of targets that are on + local function getTargetsOn() local devs, max - devs, max = getDeviceList() + local on = {} + local n = 0 + devs,max = getTargetList() if (max > 0) then local i - local on = {} - local n = 0 for i = 1,max do local devOn = isDeviceOn(devs[i]) if (devOn ~= nil and devOn) then @@ -344,13 +344,21 @@ luup.log("DeusExMachinaII: deviceControl(): handling " .. devid .. " as dimmer, n = n + 1 end end - if (n > 0) then - i = math.random(1, n) - deviceControl(on[i], false) - luup.log("DeusExMachinaII::turnOffLight(): turned " .. tostring(on[i]) .. " OFF, " .. tostring(n-1) .. " devices still on.") - if (n > 1) then - return true -- there are still lights to turn off - end + end + return on,n + end + + -- Turn off a light, if any is on. Returns 1 if there are more lights to turn off; otherwise 0. + local function turnOffLight() + local on + local n + on, n = getTargetsOn() + if (n > 0) then + local i = math.random(1, n) + targetControl(on[i], false) + luup.log("DeusExMachinaII::turnOffLight(): turned " .. tostring(on[i]) .. " OFF, " .. tostring(n-1) .. " targets still on.") + if (n > 1) then + return true -- there are still lights to turn off end end return false -- no more lights to turn off @@ -361,10 +369,10 @@ luup.log("DeusExMachinaII: deviceControl(): handling " .. devid .. " as dimmer, -- set the next state when this function returns (expected would be STANDBY or IDLE). function clearLights() local devs, count - devs, count = getDeviceList() + devs, count = getTargetList() luup.variable_set(SID, "State", STATE_SHUTDOWN, lul_device) while count > 0 do - deviceControl(devs[count], false) + targetControl(devs[count], false) count = count - 1 end runFinalScene() @@ -423,6 +431,7 @@ luup.log("DeusExMachinaII: deviceControl(): handling " .. devid .. " as dimmer, luup.variable_set(SID, "MinOffDelay", "60", lul_device) luup.variable_set(SID, "MaxOffDelay", "300", lul_device) luup.variable_set(SID, "LightsOut", 1439, lul_device) + luup.variable_set(SID, "MaxTargetsOn", 0, lul_device) luup.variable_set(SID, "Enabled", "0", lul_device) luup.variable_set(SID, "Version", DEMVERSION, lul_device) end @@ -469,7 +478,7 @@ luup.log("DeusExMachinaII: deviceControl(): handling " .. devid .. " as dimmer, -- Initialize. function deusInit(deusDevice) - luup.log("DeusExMachinaII::deusInit(): Version 2.4RC1 (2016-09-25), starting up...") + luup.log("DeusExMachinaII::deusInit(): Version 2.4RC2 (2016-12-15), starting up...") -- One-time stuff runOnce() @@ -567,12 +576,12 @@ luup.log("DeusExMachinaII: deviceControl(): handling " .. devid .. " as dimmer, nextCycleDelay = getRandomDelay("MinOffDelay", "MaxOffDelay", 60, 300) end else - -- Fully active. Find a random device to control and control it. + -- Fully active. Find a random target to control and control it. luup.log("DeusExMachinaII::deusStep(): running toggle cycle") luup.variable_set(SID, "State", STATE_CYCLE, lul_device) nextCycleDelay = getRandomDelay("MinCycleDelay", "MaxCycleDelay") local devs, max - devs, max = getDeviceList() + devs, max = getTargetList() if (max > 0) then local change = math.random(1, max) local devspec = devs[change] @@ -580,16 +589,29 @@ luup.log("DeusExMachinaII: deviceControl(): handling " .. devid .. " as dimmer, local s = isDeviceOn(devspec) if (s ~= nil) then if (s) then - deviceControl(devspec, false) + -- Turn something off. + targetControl(devspec, false) luup.log("DeusExMachinaII::deusStep(): set " .. devspec .. " to OFF") else - deviceControl(devspec, true) + -- Turn something on. If we're at the max number of targets we're allowed to turn on, + -- turn targets off first. + local maxOn = getVarNumeric("MaxTargetsOn") + if (maxOn == nil) then maxOn = 0 else maxOn = tonumber(maxOn,10) end + if (maxOn > 0) then + local on, n + on, n = getTargetsOn() + while ( n >= maxOn ) do + turnOffLight() + on, n = getTargetsOn() + end + end + targetControl(devspec, true) luup.log("DeusExMachinaII::deusStep(): set " .. devspec .. " to ON") end end end else - luup.log("DeusExMachinaII:deusStep(): no devices to control") + luup.log("DeusExMachinaII:deusStep(): no targets to control") end end From 8ec4f5c5f1ea59862aaa5fe3adb3a042ac2c49ab Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Thu, 15 Dec 2016 19:11:43 -0500 Subject: [PATCH 11/49] Now RC2 --- D_DeusExMachinaII1_UI7.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/D_DeusExMachinaII1_UI7.json b/D_DeusExMachinaII1_UI7.json index 4a4b0fc..bf2f81e 100644 --- a/D_DeusExMachinaII1_UI7.json +++ b/D_DeusExMachinaII1_UI7.json @@ -103,7 +103,7 @@ "left": "0", "Label": { "lang_tag": "dem_about", - "text": "DeusExMachina II ver 2.4RC1 2016-09-25
    For documentation or to report bugs, please go to the DeusExMachina Github repository
    This plugin is offered for use as-is and without warranties of any kind. By using this plugin, you agree to assume all risks of use without limitation.
    ." + "text": "DeusExMachina II ver 2.4RC2 2016-12-15
    For documentation or to report bugs, please go to the DeusExMachina Github repository
    This plugin is offered for use as-is and without warranties of any kind. By using this plugin, you agree to assume all risks of use without limitation.
    ." }, "Display": { "Top": "80", From 08d86c97f3571058895c08435c7d24a4e5496d4d Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Thu, 15 Dec 2016 20:10:03 -0500 Subject: [PATCH 12/49] Additional features for 2.4RC2. Prep for release. --- README.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 76cc514..f64d92b 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ There are currently two versions of Deus Ex Machina available: * Deus Ex Machina -- version 1.1, for UI5; this is the legacy version and although it installs for UI6 and UI7, it does not work properly on those platforms. -* Deus Ex Machina II -- version 2.0, for UI7; this is the new plugin. It is for all versions of firmware, but has only been tested under UI7. Testing and bug reports for UI5 and UI6 would be appreciated. +* Deus Ex Machina II -- version 2.4, for UI7 (only). This version was developed and tested on firmware version 1.7.855, but should work for any full release of UI7 provided by MiCasaVerde. ### History ### @@ -59,8 +59,9 @@ UI7 introduced the concept of "House Modes." Version 2.3 and beyond of Deus Ex M house is in one or more selected house modes. A set of checkboxes is used to selected which modes allow Deus Ex Machina to run. If no modes are chosen, it is the same as choosing all modes (Deus operates in any house mode). -Selecting the lights to be controlled is a simple matter of clicking the check boxes. Lights on dimmers cannot be set to values less than 100% in the current version of the plugin. Because the operating cycle of -the plug-in is random, any controlled light may be turned on and off several times during the cycling period (between sunset and Lights Out time). +Selecting the devices to be controlled is a simple matter of clicking the check boxes. Because the operating cycle of +the plug-in is random, any controlled device may be turned on and off several times during the cycling period (between sunset and Lights Out time). +As of version 2.4, lights on dimmers can be set to any level by setting the slider that appears to the right of the device name. Non-dimming devices are simply turned on and off. As of version 2.4, all devices are listed that implement the SwitchPower1 and Dimming1 services. This leads to some oddities, like some motion sensors and thermostats being listed. It may not be entirely obvious (or standard) what a thermostat, for example, might do when you try to turn it off and on like a light, so be careful selecting these devices. @@ -71,6 +72,8 @@ the ability to handle device-specific capabilities that would be difficult to tr turn Philips Hue lights on and off (to dimming levels, even), it cannot control their color because there's no UI for that in DEMII. But a scene could be used to control that light or a group of lights, with their color. +Version 2.4 also adds the ability to limit the number of targets (devices or scenes) that DEMII can have "on" simultaneously. + Finally, 2.4 adds the ability for a "final scene" to run when DEMII is disabled or turns off the last light after the "lights out" time. This could be used for any purpose. I personally use it to make sure a whole-house off is run, but you could use it to ensure your alarm system is armed, or your garage door is closed, etc. #### Control by Scene #### @@ -89,8 +92,8 @@ luup.call_action("urn:futzle-com:serviceId:DeusExMachina1", "SetEnabled", { NewE Of course, only one of either "0" or "1" should be specified. -Note that when disabling Deus Ex Machina from a scene or the user interface, versions 1.1 and 2.0 operate differently. Version 1.1 will simply stop cycling lights, leaving on any controlled lights it may have turned on. Version 2, however, -will turn off all controlled lights _if it was in the cycling period (between sunset and lights out time) at the time it was disabled_. +Note that when disabling Deus Ex Machina from a scene or the user interface, versions 1.1 and 2.0 operate differently. Version 1.1 will simply stop cycling lights, leaving on any controlled lights it may have turned on. +Version 2, however, will turn off all controlled lights _if it was in the cycling period (between sunset and lights out time) at the time it was disabled_. Version 2.0 also added the ability for a change of DeusExMachina's Enabled state to be used as trigger in scenes and other places where events can be watched (e.g. Program Logic plugins, etc.). This also works on UI7 only. From a77924478ffcddd636d4796cd694655dae3a1ae0 Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Thu, 15 Dec 2016 20:13:48 -0500 Subject: [PATCH 13/49] Text tweaks. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f64d92b..2b66910 100644 --- a/README.md +++ b/README.md @@ -67,12 +67,12 @@ As of version 2.4, all devices are listed that implement the SwitchPower1 and Di like some motion sensors and thermostats being listed. It may not be entirely obvious (or standard) what a thermostat, for example, might do when you try to turn it off and on like a light, so be careful selecting these devices. Also new for version 2.4 is the ability to run scenes during the random cycling period. Scenes must be specified in pairs, with -one being the "on" scene and the other being an "off" scene. This not allows more patterned use of lights, but also gives user -the ability to handle device-specific capabilities that would be difficult to track in DEMII. For example, while DEMII can now +one being the "on" scene and the other being an "off" scene. This not only allows more patterned use of lights, but also gives the user +the ability to handle device-specific capabilities that would be difficult to implement in DEMII. For example, while DEMII can now turn Philips Hue lights on and off (to dimming levels, even), it cannot control their color because there's no UI for that in DEMII. But a scene could be used to control that light or a group of lights, with their color. -Version 2.4 also adds the ability to limit the number of targets (devices or scenes) that DEMII can have "on" simultaneously. +Version 2.4 also adds the ability to limit the number of targets (devices or scenes) that DEMII can have "on" simultaneously. If this limit is 0, there is no limit enforced. Finally, 2.4 adds the ability for a "final scene" to run when DEMII is disabled or turns off the last light after the "lights out" time. This could be used for any purpose. I personally use it to make sure a whole-house off is run, but you could use it to ensure your alarm system is armed, or your garage door is closed, etc. From b2d9b33910372b679660842289074e2098ac7f04 Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Sat, 17 Dec 2016 09:15:16 -0500 Subject: [PATCH 14/49] WIP before change to device type sensing --- I_DeusExMachinaII1.xml | 46 +++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/I_DeusExMachinaII1.xml b/I_DeusExMachinaII1.xml index a753ac2..56a3e70 100644 --- a/I_DeusExMachinaII1.xml +++ b/I_DeusExMachinaII1.xml @@ -242,14 +242,14 @@ local function isDeviceOn(targetid) local first = string.upper(string.sub(targetid, 1, 1)) if (first == "S") then -luup.log("DeusExMachinaII::isDeviceOn(): handling scene spec " .. targetid) + -- luup.log("DeusExMachinaII::isDeviceOn(): handling scene spec " .. targetid) return isSceneOn(targetid) end -- Handle as switch or dimmer -luup.log("DeusExMachinaII::isDeviceOn(): handling target spec " .. targetid) + -- luup.log("DeusExMachinaII::isDeviceOn(): handling target spec " .. targetid) local r = tonumber(string.match(targetid, '^%d+'), 10) -luup.log("DeusExMachinaII::isDeviceOn(): target number seems to be " .. tostring(r)) + -- luup.log("DeusExMachinaII::isDeviceOn(): target number seems to be " .. tostring(r)) local val = "0" if (luup.devices[r] ~= nil) then local t = luup.variable_get(DIMMER_SID, "LoadLevelStatus", r) @@ -303,13 +303,13 @@ luup.log("DeusExMachinaII::targetControl(): on scene is " .. tostring(onScene) . lvl = tonumber(lvl, 10) end targetid = tonumber(targetid, 10) -luup.log("DeusExMachinaII::targetControl(): handling device " .. tostring(targetid) .. ", level " .. tostring(lvl)) + -- Level for all types is 0 if turning device off + if not turnOn then lvl = 0 end + -- luup.log("DeusExMachinaII::targetControl(): handling device " .. tostring(targetid) .. ", level " .. tostring(lvl)) if luup.devices[targetid] == nil then luup.log("DeusExMachinaII::targetControl(): device " .. tostring(targetid) .. " not found in luup.devices"); return end - -- Level for all types is 0 if turning device off - if not turnOn then lvl = 0 end local t = luup.variable_get(DIMMER_SID, "LoadLevelTarget", targetid) if (t == nil) then t = luup.variable_get(SWITCH_SID, "Status", targetid) @@ -318,12 +318,12 @@ luup.log("DeusExMachinaII::targetControl(): handling device " .. tostring(target else -- Handle as SwitchPower1 if turnOn then lvl = 1 end -luup.log("DeusExMachinaII: targetControl(): handling " .. targetid .. " as binary light, setting target to " .. lvl) + luup.log("DeusExMachinaII: targetControl(): handling " .. targetid .. " as binary light, setting target to " .. lvl) luup.call_action(SWITCH_SID, "SetTarget", {newTargetValue=lvl}, targetid) end else -- Handle as Dimming1 -luup.log("DeusExMachinaII: targetControl(): handling " .. targetid .. " as dimmer, setting load level to " .. lvl) + luup.log("DeusExMachinaII: targetControl(): handling " .. targetid .. " as dimmer, setting load level to " .. lvl) luup.call_action(DIMMER_SID, "SetLoadLevelTarget", {newLoadlevelTarget=lvl}, targetid) -- note odd case inconsistency end end @@ -349,19 +349,22 @@ luup.log("DeusExMachinaII: targetControl(): handling " .. targetid .. " as dimme end -- Turn off a light, if any is on. Returns 1 if there are more lights to turn off; otherwise 0. - local function turnOffLight() - local on + local function turnOffLight(on) local n - on, n = getTargetsOn() + if on == nil then + on, n = getTargetsOn() + else + n = table.getn(on) + end if (n > 0) then local i = math.random(1, n) - targetControl(on[i], false) - luup.log("DeusExMachinaII::turnOffLight(): turned " .. tostring(on[i]) .. " OFF, " .. tostring(n-1) .. " targets still on.") - if (n > 1) then - return true -- there are still lights to turn off - end + local target = on[i] + targetControl(target, false) + table.remove(on, i) + n = n - 1 + luup.log("DeusExMachinaII::turnOffLight(): turned " .. tostring(target) .. " OFF, " .. tostring(n) .. " targets still on.") end - return false -- no more lights to turn off + return (n > 0), on, n end -- Turn off all lights as fast as we can. Transition through SHUTDOWN state during, @@ -590,8 +593,8 @@ luup.log("DeusExMachinaII: targetControl(): handling " .. targetid .. " as dimme if (s ~= nil) then if (s) then -- Turn something off. + luup.log("DeusExMachinaII::deusStep(): turn " .. devspec .. " OFF") targetControl(devspec, false) - luup.log("DeusExMachinaII::deusStep(): set " .. devspec .. " to OFF") else -- Turn something on. If we're at the max number of targets we're allowed to turn on, -- turn targets off first. @@ -601,12 +604,13 @@ luup.log("DeusExMachinaII: targetControl(): handling " .. targetid .. " as dimme local on, n on, n = getTargetsOn() while ( n >= maxOn ) do - turnOffLight() - on, n = getTargetsOn() + luup.log("DeusExMachinaII:deusStep(): too many targets on, max is " .. tostring(maxOn) + .. ", have " .. tostring(n) .. ", let's turn one off.") + _, on, n = turnOffLight(on) end end + luup.log("DeusExMachinaII::deusStep(): turn " .. devspec .. " ON") targetControl(devspec, true) - luup.log("DeusExMachinaII::deusStep(): set " .. devspec .. " to ON") end end end From aeb8f98be1555423b46c8337a974d363625fbcb1 Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Sat, 17 Dec 2016 10:01:43 -0500 Subject: [PATCH 15/49] Cleaner handling of device type detection. Attempt to remove targets from Devices list if they're no longer around. --- I_DeusExMachinaII1.xml | 78 ++++++++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 29 deletions(-) diff --git a/I_DeusExMachinaII1.xml b/I_DeusExMachinaII1.xml index 56a3e70..35aee6c 100644 --- a/I_DeusExMachinaII1.xml +++ b/I_DeusExMachinaII1.xml @@ -9,12 +9,13 @@ -- ------------------------------------------------------------------------------------------------------------------------- SID = "urn:toggledbits-com:serviceId:DeusExMachinaII1" + DEMVERSION = 20400 + SWITCH_TYPE = "urn:schemas-upnp-org:device:BinaryLight:1" SWITCH_SID = "urn:upnp-org:serviceId:SwitchPower1" DIMMER_TYPE = "urn:schemas-upnp-org:device:DimmableLight:1" DIMMER_SID = "urn:upnp-org:serviceId:Dimming1" - DEMVERSION = 20400 - + STATE_STANDBY = 0 STATE_IDLE = 1 STATE_CYCLE = 2 @@ -28,7 +29,7 @@ luup.variable_set(SID, "UI7Check", "false", lul_device) ui7Check = "false" end - if( luup.version_branch == 1 and luup.version_major == 7 and ui7Check == "false") then + if ( luup.version_branch == 1 and luup.version_major == 7 and ui7Check == "false" ) then luup.variable_set(SID, "UI7Check", "true", lul_device) luup.attr_set("device_json", "D_DeusExMachinaII1_UI7.json", lul_device) luup.reload() @@ -194,6 +195,18 @@ until k > string.len(s) return t, n end + + local function join(sep, t) + local i + if (table.getn(t) < 1) then + return "" + end + local s = tostring(t[1]) + for i=2,table.getn(t) do + s = s .. sep .. tostring(t[i]) + end + return s + end -- Return true if a specified scene has been run (i.e. on the list) local function isSceneOn(spec) @@ -237,6 +250,20 @@ local s = luup.variable_get(SID, "Devices", lul_device) or "" return split(s) end + + -- Remove a target from the target list. Used when the target no longer exists. Linear, poor, but short list and rarely used. + local function removeTarget(target, tlist) + if tlist == nil then tlist = getTargetList() end + local i + for i = 1,table.getn(tlist) do + if tostring(target) == tlist[i] then + table.remove(tlist, i) + luup.variable_set(SID, "Devices", join(",", tlist), lul_device) + return true + end + end + return false + end -- Light on or off? Returns boolean local function isDeviceOn(targetid) @@ -252,20 +279,14 @@ -- luup.log("DeusExMachinaII::isDeviceOn(): target number seems to be " .. tostring(r)) local val = "0" if (luup.devices[r] ~= nil) then - local t = luup.variable_get(DIMMER_SID, "LoadLevelStatus", r) - if (t == nil) then - t = luup.variable_get(SWITCH_SID, "Status", r) - if (t == nil) then - luup.log("DeusExMachinaII::isDeviceOn(): target " .. tostring(targetid) .. " unknown device type") - return nil - else - val = luup.variable_get(SWITCH_SID, "Status", r) - end - else + if luup.device_supports_service(DIMMER_SID) then val = luup.variable_get(DIMMER_SID, "LoadLevelStatus", r) + elseif luup.device_supports_service(SWITCH_SID) then + val = luup.variable_get(SWITCH_SID, "Status", r) end else luup.log("DeusExMachinaII::isDeviceOn(): target spec " .. tostring(targetid) .. " device " .. tostring(r) .. ", device not found in luup.devices") + removeTarget(targetid) return false end return val ~= "0" @@ -292,10 +313,11 @@ luup.log("DeusExMachinaII::targetControl(): handling scene spec " .. targetid) end luup.log("DeusExMachinaII::targetControl(): on scene is " .. tostring(onScene) .. ", off scene is " .. tostring(offScene)) local targetScene - if (turnOn) then targetScene = onScene else targetScene = offScene end + if turnOn then targetScene = onScene else targetScene = offScene end luup.call_action("urn:micasaverde-com:serviceId:HomeAutomationGateway1", "RunScene", { SceneNum=targetScene }, 0) updateSceneState(targetid, turnOn) else + -- Parse the level if this is a dimming target spec local lvl = 100 local k = string.find(targetid, '=') if k ~= nil then @@ -307,24 +329,22 @@ luup.log("DeusExMachinaII::targetControl(): on scene is " .. tostring(onScene) . if not turnOn then lvl = 0 end -- luup.log("DeusExMachinaII::targetControl(): handling device " .. tostring(targetid) .. ", level " .. tostring(lvl)) if luup.devices[targetid] == nil then + -- Device doesn't exist (user deleted, etc.). PHR??? Should remove from Devices state variable. luup.log("DeusExMachinaII::targetControl(): device " .. tostring(targetid) .. " not found in luup.devices"); + removeTarget(targetid) return end - local t = luup.variable_get(DIMMER_SID, "LoadLevelTarget", targetid) - if (t == nil) then - t = luup.variable_get(SWITCH_SID, "Status", targetid) - if (t == nil) then - luup.log("DeusExMachinaII: targetControl(): can't determine device type of devspec=" .. targetid) - else - -- Handle as SwitchPower1 - if turnOn then lvl = 1 end - luup.log("DeusExMachinaII: targetControl(): handling " .. targetid .. " as binary light, setting target to " .. lvl) - luup.call_action(SWITCH_SID, "SetTarget", {newTargetValue=lvl}, targetid) - end - else + if luup.device_supports_service(DIMMER_SID) then -- Handle as Dimming1 luup.log("DeusExMachinaII: targetControl(): handling " .. targetid .. " as dimmer, setting load level to " .. lvl) luup.call_action(DIMMER_SID, "SetLoadLevelTarget", {newLoadlevelTarget=lvl}, targetid) -- note odd case inconsistency + elseif luup.device_supports_service(SWITCH_SID) then + -- Handle as SwitchPower1 + if turnOn then lvl = 1 end + luup.log("DeusExMachinaII: targetControl(): handling " .. targetid .. " as binary light, setting target to " .. lvl) + luup.call_action(SWITCH_SID, "SetTarget", {newTargetValue=lvl}, targetid) + else + luup.log("DeusExMachinaII: targetControl(): don't know how to control target " .. targetid) end end end @@ -460,12 +480,12 @@ luup.log("DeusExMachinaII::targetControl(): on scene is " .. tostring(onScene) . luup.variable_set(SID, "Version", DEMVERSION, lul_device) end - -- Enable DEM by setting a new cycle stamp and calling an initial cycle directly. + -- Enable DEM by setting a new cycle stamp and scheduling our first cycle step. function deusEnable() luup.log("DeusExMachinaII::deusEnable(): enabling...") luup.variable_set(SID, "ScenesRunning", "", lul_device) -- start with a clean slate runStamp = os.time() - deusStep(runStamp) + luup.call_delay("deusStep", 1, runStamp, 1) end -- Disable DEM and go to standby state. If we are currently cycling (as opposed to idle/waiting for sunset), @@ -486,7 +506,7 @@ luup.log("DeusExMachinaII::targetControl(): on scene is " .. tostring(onScene) . -- One-time stuff runOnce() - --check UI version + -- Check UI version checkVersion() -- Start up if we're enabled From 2aa890e42c6e6c945ef460b9f8f0b859dc6eeb7d Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Sun, 18 Dec 2016 09:51:21 -0500 Subject: [PATCH 16/49] I_DeusExMachinaII1.xml --- D_DeusExMachinaII1.xml | 10 +++++ D_DeusExMachinaII1_UI7.json | 88 ++++++++++++++++++------------------- S_DeusExMachinaII1.xml | 2 +- 3 files changed, 55 insertions(+), 45 deletions(-) diff --git a/D_DeusExMachinaII1.xml b/D_DeusExMachinaII1.xml index 988efed..db87869 100644 --- a/D_DeusExMachinaII1.xml +++ b/D_DeusExMachinaII1.xml @@ -13,6 +13,16 @@ urn:toggledbits-com:serviceId:DeusExMachinaII1 S_DeusExMachinaII1.xml + + urn:schemas-upnp-org:service:SwitchPower:1 + urn:upnp-org:serviceId:SwitchPower1 + S_SwitchPower1.xml + + + urn:schemas-micasaverde-com:service:HaDevice:1 + urn:micasaverde-com:serviceId:HaDevice1 + S_HaDevice1.xml + diff --git a/D_DeusExMachinaII1_UI7.json b/D_DeusExMachinaII1_UI7.json index bf2f81e..36f6bdf 100644 --- a/D_DeusExMachinaII1_UI7.json +++ b/D_DeusExMachinaII1_UI7.json @@ -10,8 +10,8 @@ "state_icons": [{ "img": "https://dtabq7xg0g1t1.cloudfront.net/deus-red.png", "conditions": [{ - "service": "urn:toggledbits-com:serviceId:DeusExMachinaII1", - "variable": "Enabled", + "service": "urn:upnp-org:serviceId:SwitchPower1", + "variable": "Status", "operator": "==", "value": 0, "subcategory_num": 0 @@ -19,8 +19,8 @@ }, { "img": "https://dtabq7xg0g1t1.cloudfront.net/deus-green.png", "conditions": [{ - "service": "urn:toggledbits-com:serviceId:DeusExMachinaII1", - "variable": "Enabled", + "service": "urn:upnp-org:serviceId:SwitchPower1", + "variable": "Status", "operator": "==", "value": 1, "subcategory_num": 0 @@ -57,44 +57,44 @@ "left": "1", "states": [{ "Label": { - "lang_tag": "dem_enable", - "text": "Enabled" + "lang_tag": "ui7_cmd_on", + "text": "On" }, "ControlGroup": 1, "Display": { - "Service": "urn:toggledbits-com:serviceId:DeusExMachinaII1", - "Variable": "Enabled", + "Service": "urn:upnp-org:serviceId:SwitchPower1", + "Variable": "Status", "Value": "1" }, "Command": { - "Service": "urn:toggledbits-com:serviceId:DeusExMachinaII1", - "Action": "SetEnabled", + "Service": "urn:upnp-org:serviceId:SwitchPower1", + "Action": "SetTarget", "Parameters": [{ - "Name": "NewEnabledValue", + "Name": "newTargetValue", "Value": "1" }] }, - "ControlCode": "dem_enable" + "ControlCode": "power_on" }, { "Label": { - "lang_tag": "dem_disable", - "text": "Disabled" + "lang_tag": "ui7_cmd_off", + "text": "Off" }, "ControlGroup": 1, "Display": { - "Service": "urn:toggledbits-com:serviceId:DeusExMachinaII1", - "Variable": "Enabled", + "Service": "urn:upnp-org:serviceId:SwitchPower1", + "Variable": "Status", "Value": "0" }, "Command": { - "Service": "urn:toggledbits-com:serviceId:DeusExMachinaII1", - "Action": "SetEnabled", + "Service": "urn:upnp-org:serviceId:SwitchPower1", + "Action": "SetTarget", "Parameters": [{ - "Name": "NewEnabledValue", + "Name": "newTargetValue", "Value": "0" }] }, - "ControlCode": "dem_disable" + "ControlCode": "power_off" }], "ControlCode": "dem_statecontrol" }, { @@ -103,7 +103,7 @@ "left": "0", "Label": { "lang_tag": "dem_about", - "text": "DeusExMachina II ver 2.4RC2 2016-12-15
    For documentation or to report bugs, please go to the DeusExMachina Github repository
    This plugin is offered for use as-is and without warranties of any kind. By using this plugin, you agree to assume all risks of use without limitation.
    ." + "text": "DeusExMachina II ver 2.4RC3 2016-12-17
    For documentation or to report bugs, please go to the DeusExMachina Github repository
    This plugin is offered for use as-is and without warranties of any kind. By using this plugin, you agree to assume all risks in connection with its use without limitation.." }, "Display": { "Top": "80", @@ -117,16 +117,16 @@ "lang_tag": "configure", "text": "Configure" }, - "Position": "2", + "Position": "1", "TabType": "javascript", "ScriptName": "J_DeusExMachinaII1_UI7.js", "Function": "DeusExMachinaII.configureDeus" }, { "Label": { - "lang_tag": "advanced", + "lang_tag": "ui7_advanced", "text": "Advanced" }, - "Position": "3", + "Position": "2", "TabType": "javascript", "ScriptName": "shared.js", "Function": "advanced_device" @@ -135,27 +135,27 @@ "group_1": { "cmd_1": { "label": "ON", - "serviceId": "urn:toggledbits-com:serviceId:DeusExMachinaII1", - "action": "SetEnabled", + "serviceId": "urn:upnp-org:serviceId:SwitchPower1", + "action": "SetTarget", "arguments": { - "NewEnabledValue": "1" + "newTargetValue": "1" }, "display": { - "service": "urn:toggledbits-com:serviceId:DeusExMachinaII1", - "variable": "Enabled", + "service": "urn:upnp-org:serviceId:SwitchPower1", + "variable": "Status", "value": "1" } }, "cmd_2": { "label": "OFF", - "serviceId": "urn:toggledbits-com:serviceId:DeusExMachinaII1", - "action": "SetEnabled", + "serviceId": "urn:upnp-org:serviceId:SwitchPower1", + "action": "SetTarget", "arguments": { - "NewEnabledValue": "0" + "newTargetValue": "0" }, "display": { - "service": "urn:toggledbits-com:serviceId:DeusExMachinaII1", - "variable": "Enabled", + "service": "urn:upnp-org:serviceId:SwitchPower1", + "variable": "Status", "value": "0" } } @@ -165,27 +165,27 @@ "id": 1, "label": { "lang_tag": "dem_enabledisable", - "text": "The device is enabled or disabled" + "text": "Is enabled or disabled" }, - "serviceId": "urn:toggledbits-com:serviceId:DeusExMachinaII1", + "serviceId": "urn:upnp-org:serviceId:SwitchPower1", "argumentList": [{ "id": 1, "dataType": "boolean", "defaultValue": "0", "allowedValueList": [{ - "value": "0", + "Disabled": "0", "HumanFriendlyText": { "lang_tag": "dem_disabled", "text": "_DEVICE_NAME_ is disabled" } }, { - "value": "1", + "Enabled": "1", "HumanFriendlyText": { "lang_tag": "dem_enabled", "text": "_DEVICE_NAME_ is enabled" } }], - "name": "Enabled", + "name": "Status", "comparisson": "=", "prefix": { "lang_tag": "dem_when", @@ -208,32 +208,32 @@ "value": "0", "HumanFriendlyText": { "lang_tag": "dem_standby", - "text": "_DEVICE_NAME_ mode is standby" + "text": "Standby" } }, { "value": "1", "HumanFriendlyText": { "lang_tag": "dem_ready", - "text": "_DEVICE_NAME_ mode is ready" + "text": "Ready" } }, { "value": "2", "HumanFriendlyText": { "lang_tag": "dem_cycle", - "text": "_DEVICE_NAME_ mode is cycling" + "text": "Cycling" } }, { "value": "3", "HumanFriendlyText": { "lang_tag": "dem_shutoff", - "text": "_DEVICE_NAME_ mode is shut-off" + "text": "Shut-off" } }], "name": "State", "comparisson": "=", "prefix": { "lang_tag": "dem_when", - "text": "When" + "text": "to" }, "suffix": {} }] diff --git a/S_DeusExMachinaII1.xml b/S_DeusExMachinaII1.xml index 2125a2d..9a8b1de 100644 --- a/S_DeusExMachinaII1.xml +++ b/S_DeusExMachinaII1.xml @@ -46,4 +46,4 @@ - + \ No newline at end of file From 2f85ca61487211d31da4afef3a8f3f17ef1922f4 Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Sun, 18 Dec 2016 09:51:32 -0500 Subject: [PATCH 17/49] First pass at SwitchPower1 SetTarget as focus for enable/disable --- I_DeusExMachinaII1.xml | 63 ++++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/I_DeusExMachinaII1.xml b/I_DeusExMachinaII1.xml index 35aee6c..4a7224e 100644 --- a/I_DeusExMachinaII1.xml +++ b/I_DeusExMachinaII1.xml @@ -57,9 +57,11 @@ -- luup.log("DeusExMachinaII::deleteVar(" .. name .. "): status=" .. tostring(status) .. ", result=" .. tostring(result)) end - -- Shortcut function to return state of Enabled variable + -- Shortcut function to return state of SwitchPower1 Status variable local function isEnabled() - return ( getVarNumeric("Enabled", 0) ~= 0 ) + local s = luup.variable_get(SWITCH_SID, "Status", lul_device) + if (s == nil or s == "") then return false end + return (s ~= "0") end local function isActiveHouseMode() @@ -196,18 +198,6 @@ return t, n end - local function join(sep, t) - local i - if (table.getn(t) < 1) then - return "" - end - local s = tostring(t[1]) - for i=2,table.getn(t) do - s = s .. sep .. tostring(t[i]) - end - return s - end - -- Return true if a specified scene has been run (i.e. on the list) local function isSceneOn(spec) local stateList = luup.variable_get(SID, "ScenesRunning", lul_device) or "" @@ -258,7 +248,7 @@ for i = 1,table.getn(tlist) do if tostring(target) == tlist[i] then table.remove(tlist, i) - luup.variable_set(SID, "Devices", join(",", tlist), lul_device) + luup.variable_set(SID, "Devices", table.concat(tlist, ","), lul_device) return true end end @@ -405,11 +395,12 @@ luup.log("DeusExMachinaII::targetControl(): on scene is " .. tostring(onScene) . -- takes place. For us, that means looking to see if an older version of Deus is still -- installed, and copying its config into our new config. Then disable the old Deus. local function runOnce() - local s = luup.variable_get(SID, "Enabled", lul_device) + local s = luup.variable_get(SID, "Devices", lul_device) if (s == nil) then - luup.log("DeusExMachinaII::runOnce(): Enabled variable not found, running...") + luup.log("DeusExMachinaII::runOnce(): Devices variable not found, setting up new instance...") -- See if there are variables from older version of DEM -- Start by finding the old Deus device, if there is one... + local devList = "" local i, olddev olddev = -1 for i,v in pairs(luup.devices) do @@ -439,14 +430,14 @@ luup.log("DeusExMachinaII::targetControl(): on scene is " .. tostring(onScene) . table.insert(t, s) end end - s = table.concat(t, ",") - luup.variable_set(SID, "Devices", s, lul_device) + devList = table.concat(t, ",") deleteVar("controlCount", lul_device) end -- Finally, turn off old Deus luup.call_action(oldsid, "SetEnabled", { NewEnabledValue = "0" }, olddev) end + luup.variable_set(SID, "Devices", devList, lul_device) -- Set up some other default config luup.variable_set(SID, "MinCycleDelay", "300", lul_device) @@ -455,8 +446,10 @@ luup.log("DeusExMachinaII::targetControl(): on scene is " .. tostring(onScene) . luup.variable_set(SID, "MaxOffDelay", "300", lul_device) luup.variable_set(SID, "LightsOut", 1439, lul_device) luup.variable_set(SID, "MaxTargetsOn", 0, lul_device) - luup.variable_set(SID, "Enabled", "0", lul_device) + luup.variable_set(SID, "Enabled", "0", lul_device, true) luup.variable_set(SID, "Version", DEMVERSION, lul_device) + luup.variable_set(SWITCH_SID, "Status", "0", lul_device, true) + luup.variable_set(SWITCH_SID, "Target", "0", lul_device, true) end -- Consider per-version changes. @@ -475,6 +468,12 @@ luup.log("DeusExMachinaII::targetControl(): on scene is " .. tostring(onScene) . end deleteVar("LightsOutTime", lul_device) end + if (s < 20400) then + luup.variable_set(SID, "MaxTargetsOn", 0, lul_device) + local e = getVarNumeric("Enabled", 0) + luup.variable_set(SWITCH_SID, "Status", e, lul_device, true) + luup.variable_set(SWITCH_SID, "Target", e, lul_device, true) + end -- Update version last. luup.variable_set(SID, "Version", DEMVERSION, lul_device) @@ -486,6 +485,8 @@ luup.log("DeusExMachinaII::targetControl(): on scene is " .. tostring(onScene) . luup.variable_set(SID, "ScenesRunning", "", lul_device) -- start with a clean slate runStamp = os.time() luup.call_delay("deusStep", 1, runStamp, 1) + luup.variable_set(SID, "Enabled", 1, lul_device) + luup.variable_set(SWITCH_SID, "Status", "1", lul_device) end -- Disable DEM and go to standby state. If we are currently cycling (as opposed to idle/waiting for sunset), @@ -497,11 +498,13 @@ luup.log("DeusExMachinaII::targetControl(): on scene is " .. tostring(onScene) . clearLights() end luup.variable_set(SID, "State", STATE_STANDBY, lul_device) + luup.variable_set(SID, "Enabled", 0, lul_device) + luup.variable_set(SWITCH_SID, "Status", "0", lul_device) end -- Initialize. function deusInit(deusDevice) - luup.log("DeusExMachinaII::deusInit(): Version 2.4RC2 (2016-12-15), starting up...") + luup.log("DeusExMachinaII::deusInit(): Version 2.4RC3 (2016-12-17), initializing...") -- One-time stuff runOnce() @@ -651,16 +654,28 @@ luup.log("DeusExMachinaII::targetControl(): on scene is " .. tostring(onScene) . deusInit + + urn:upnp-org:serviceId:SwitchPower1 + SetTarget + + local newTargetValue = lul_settings.newTargetValue or "0" + luup.variable_set(SWITCH_SID, "Target", newTargetValue, lul_device) + if (newTargetValue == "1") then + deusEnable() + else + deusDisable() + end + + urn:toggledbits-com:serviceId:DeusExMachinaII1 SetEnabled - local newEnabledValue = lul_settings.NewEnabledValue + local newEnabledValue = lul_settings.NewEnabledValue or "0" + luup.variable_set(SWITCH_SID, "Target", newEnabledValue, lul_device) if (newEnabledValue == "1") then - luup.variable_set(SID, "Enabled", 1, lul_device) deusEnable() else - luup.variable_set(SID, "Enabled", 0, lul_device) deusDisable() end From 670854bf0e81eea90e512aef21639a5798ad3bf2 Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Sun, 18 Dec 2016 09:51:56 -0500 Subject: [PATCH 18/49] First pass at SwitchPower1 SetTarget as focus for enable/disable --- README.md | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 2b66910..13750f2 100644 --- a/README.md +++ b/README.md @@ -80,22 +80,23 @@ Finally, 2.4 adds the ability for a "final scene" to run when DEMII is disabled As of version 2.0 and on UI7, DeusExMachina can be enabled or disabled like a light switch in scenes, through the regular graphical interface (no Lua required). -A Lua interface is also supported since version 1.1 for both UI5 and UI7, via a luup.call_action() call: +A Lua interface is also supported since version 1.1 for both UI5 and UI7. All versions of support the SetEnabled action, although for DEMII versions 2.4 and higher, the use of SetTarget in the standard SwitchPower1 service is preferred. Examples (the "0|1" means use either 0 or 1 to disable or enable, respectively): ``` --- For the new Deus Ex Machina II plugin (v2.0 and higher), do this: +-- Preferred for DEMII versions 2.4 and higher: +luup.call_action("urn:upnp-org-serviceId:SwitchPower1", "SetTarget", { newTargetValue = "0|1" }, deviceID) + +-- Also works for all versions of DEMII: luup.call_action("urn:toggledbits-com:serviceId:DeusExMachinaII1", "SetEnabled", { NewEnabledValue = "0|1" }, deviceID) -- For the old Deus Ex Machina plugin (v1.1 and earlier) running on UI5 or UI7, do this: luup.call_action("urn:futzle-com:serviceId:DeusExMachina1", "SetEnabled", { NewEnabledValue = "0|1" }, deviceID) ``` -Of course, only one of either "0" or "1" should be specified. - -Note that when disabling Deus Ex Machina from a scene or the user interface, versions 1.1 and 2.0 operate differently. Version 1.1 will simply stop cycling lights, leaving on any controlled lights it may have turned on. -Version 2, however, will turn off all controlled lights _if it was in the cycling period (between sunset and lights out time) at the time it was disabled_. +Note that when disabling Deus Ex Machina from a scene or the user interface, versions 1.1 and 2.x (DEMII) operate differently. Version 1.1 will simply stop cycling lights, leaving on any controlled lights it may have turned on. +DEMII, however, will turn off all controlled lights _if it was in the cycling period (between sunset and lights out time) at the time it was disabled_. -Version 2.0 also added the ability for a change of DeusExMachina's Enabled state to be used as trigger in scenes and other places where events can be watched (e.g. Program Logic plugins, etc.). This also works on UI7 only. +DEMII version 2.0 also added the ability for a change of DeusExMachina's operating state to be used as trigger in scenes and other places where events can be watched (e.g. Program Logic plugins, etc.). This also works on UI7 only. #### Triggers #### @@ -107,17 +108,18 @@ For the "operating mode changes" event, the trigger fires when DEMII's operating * Standby - DEMII is disabled (this is equivalent to the "device is disabled" state event); -* Ready - DEMII is enabled and waiting for the next sunset; +* Ready - DEMII is enabled and waiting for the next sunset (and house mode, if applicable); -* Cycling - DEMII is enabled and cycling lights in the active period after sunset and before the "lights out" time; +* Cycling - DEMII is cycling lights, that is, it is enabled, in the period between sunset and the set "lights out" time, and correct house mode (if applicable); * Shut-off - DEMII is enabled and shutting off lights, having reached the "lights out" time. -When disabled, DEMII is always in Standby mode. When enabled, DEMII enters the Ready mode, then transitions to Cycling mode at sunset, then Shut-off mode at the "lights out" time, and then when all lights have -been shut off, returns to the Ready mode waiting for the next day's sunset. The transition between Ready, Cycling, and Shut-off continues until DEMII is disabled (at which point it goes to Standby). +When disabled, DEMII is always in Standby mode. When enabled, DEMII enters the Ready mode, then transitions to Cycling mode at sunset, then Shut-off mode at the "lights out" time, +and then when all lights have been shut off, returns to the Ready mode waiting for the next day's sunset. The transition between Ready, Cycling, and Shut-off continues until DEMII +is disabled (at which point it goes to Standby). -It should be noted that DEMII can enter Cycling or Shut-off mode immediately, without passing through Ready, if it is enabled after sunset or after the "lights out" time, respectively. -DEMII will also transition into or out of Standby mode immediately and from any other mode when disabled or enabled, respectively. +It should be noted that DEMII can enter Cycling or Shut-off mode immediately, without passing through Ready, if it is enabled after sunset or after the "lights out" time, +respectively. DEMII will also transition into or out of Standby mode immediately and from any other mode when disabled or enabled, respectively. #### Cycle Timing #### From 05d87e08872fa4b368ebc34d43b8a5a38b82abcf Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Sun, 18 Dec 2016 20:26:18 -0500 Subject: [PATCH 19/49] Fix minor broken-ness. Whoa. --- I_DeusExMachinaII1.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/I_DeusExMachinaII1.xml b/I_DeusExMachinaII1.xml index 4a7224e..6d2dcd6 100644 --- a/I_DeusExMachinaII1.xml +++ b/I_DeusExMachinaII1.xml @@ -269,9 +269,9 @@ -- luup.log("DeusExMachinaII::isDeviceOn(): target number seems to be " .. tostring(r)) local val = "0" if (luup.devices[r] ~= nil) then - if luup.device_supports_service(DIMMER_SID) then + if luup.device_supports_service(DIMMER_SID, r) then val = luup.variable_get(DIMMER_SID, "LoadLevelStatus", r) - elseif luup.device_supports_service(SWITCH_SID) then + elseif luup.device_supports_service(SWITCH_SID, r) then val = luup.variable_get(SWITCH_SID, "Status", r) end else @@ -324,11 +324,11 @@ luup.log("DeusExMachinaII::targetControl(): on scene is " .. tostring(onScene) . removeTarget(targetid) return end - if luup.device_supports_service(DIMMER_SID) then + if luup.device_supports_service(DIMMER_SID, targetid) then -- Handle as Dimming1 luup.log("DeusExMachinaII: targetControl(): handling " .. targetid .. " as dimmer, setting load level to " .. lvl) luup.call_action(DIMMER_SID, "SetLoadLevelTarget", {newLoadlevelTarget=lvl}, targetid) -- note odd case inconsistency - elseif luup.device_supports_service(SWITCH_SID) then + elseif luup.device_supports_service(SWITCH_SID, targetid) then -- Handle as SwitchPower1 if turnOn then lvl = 1 end luup.log("DeusExMachinaII: targetControl(): handling " .. targetid .. " as binary light, setting target to " .. lvl) From 7a9c603ed951d1ef440323dc94908394c81551d1 Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Wed, 21 Dec 2016 10:56:51 -0500 Subject: [PATCH 20/49] A little more debug checking house mode. Clean up delays in cycling loop; some places I was using OffDelay where I should have used CycleDelay (not harmful, just makes Vera test things a bit more often than generally necessary). --- I_DeusExMachinaII1.xml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/I_DeusExMachinaII1.xml b/I_DeusExMachinaII1.xml index 6d2dcd6..75e66a5 100644 --- a/I_DeusExMachinaII1.xml +++ b/I_DeusExMachinaII1.xml @@ -87,6 +87,8 @@ if (bit.band(modebits, currentMode) == 0) then luup.log('DeusExMachinaII::isActiveHouseMode(): Current mode bit ' .. string.format("0x%x", currentMode) .. ' not in set ' .. string.format("0x%x", modebits)) return false -- not active in this mode + else + luup.log('DeusExMachinaII::isActiveHouseMode(): Current mode bit ' .. string.format("0x%x", currentMode) .. ' SET in ' .. string.format("0x%x", modebits)) end end return true -- default is we're active in the current house mode @@ -277,7 +279,7 @@ else luup.log("DeusExMachinaII::isDeviceOn(): target spec " .. tostring(targetid) .. " device " .. tostring(r) .. ", device not found in luup.devices") removeTarget(targetid) - return false + return nil end return val ~= "0" end @@ -485,7 +487,7 @@ luup.log("DeusExMachinaII::targetControl(): on scene is " .. tostring(onScene) . luup.variable_set(SID, "ScenesRunning", "", lul_device) -- start with a clean slate runStamp = os.time() luup.call_delay("deusStep", 1, runStamp, 1) - luup.variable_set(SID, "Enabled", 1, lul_device) + luup.variable_set(SID, "Enabled", "1", lul_device) luup.variable_set(SWITCH_SID, "Status", "1", lul_device) end @@ -498,7 +500,7 @@ luup.log("DeusExMachinaII::targetControl(): on scene is " .. tostring(onScene) . clearLights() end luup.variable_set(SID, "State", STATE_STANDBY, lul_device) - luup.variable_set(SID, "Enabled", 0, lul_device) + luup.variable_set(SID, "Enabled", "0", lul_device) luup.variable_set(SWITCH_SID, "Status", "0", lul_device) end @@ -570,7 +572,7 @@ luup.log("DeusExMachinaII::targetControl(): on scene is " .. tostring(onScene) . -- Transition from STATE_STANDBY (i.e. we're enabling) in the inactive period. -- Go to IDLE and delay for next sunset. luup.log("DeusExMachinaII::deusStep(): transitioning to IDLE from STANDBY, waiting for next sunset..."); - nextCycleDelay = sunset - os.time() + getRandomDelay("MinOffDelay", "MaxOffDelay", 60, 300) + nextCycleDelay = sunset - os.time() + getRandomDelay("MinCycleDelay", "MaxCycleDelay") luup.variable_set(SID, "State", STATE_IDLE, lul_device) elseif (not isActiveHouseMode()) then -- Not in an active house mode. If we're not STANDBY or IDLE, turn everything back off and go to IDLE. @@ -585,9 +587,9 @@ luup.log("DeusExMachinaII::targetControl(): on scene is " .. tostring(onScene) . -- Figure out how long to delay. If we're lights-out, delay to next sunset. Otherwise, short delay -- to re-check house mode, which could change at any time, so we must deal with it. if (inActiveTimePeriod) then - nextCycleDelay = 300 + nextCycleDelay = getRandomDelay("MinCycleDelay", "MaxCycleDelay") else - nextCycleDelay = sunset - os.time() + getRandomDelay("MinOffDelay", "MaxOffDelay", 60, 300) + nextCycleDelay = sunset - os.time() + getRandomDelay("MinCycleDelay", "MaxCycleDelay") end elseif (not inActiveTimePeriod) then luup.log("DeusExMachinaII::deusStep(): running off cycle") @@ -596,7 +598,7 @@ luup.log("DeusExMachinaII::targetControl(): on scene is " .. tostring(onScene) . -- No more lights to turn off runFinalScene() luup.variable_set(SID, "State", STATE_IDLE, lul_device) - nextCycleDelay = sunset - os.time() + getRandomDelay("MinOffDelay", "MaxOffDelay", 60, 300) + nextCycleDelay = sunset - os.time() + getRandomDelay("MinCycleDelay", "MaxCycleDelay") luup.log("DeusExMachina::deusStep(): all lights out; now IDLE, setting delay to restart cycling at next sunset") else nextCycleDelay = getRandomDelay("MinOffDelay", "MaxOffDelay", 60, 300) @@ -615,7 +617,7 @@ luup.log("DeusExMachinaII::targetControl(): on scene is " .. tostring(onScene) . local s = isDeviceOn(devspec) if (s ~= nil) then if (s) then - -- Turn something off. + -- It's on; turn it off. luup.log("DeusExMachinaII::deusStep(): turn " .. devspec .. " OFF") targetControl(devspec, false) else From b16f80c9b84749b46196bbaf42d2b4da4de9bae7 Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Wed, 21 Dec 2016 10:59:24 -0500 Subject: [PATCH 21/49] Update RC3 date --- D_DeusExMachinaII1_UI7.json | 2 +- I_DeusExMachinaII1.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/D_DeusExMachinaII1_UI7.json b/D_DeusExMachinaII1_UI7.json index 36f6bdf..63784c5 100644 --- a/D_DeusExMachinaII1_UI7.json +++ b/D_DeusExMachinaII1_UI7.json @@ -103,7 +103,7 @@ "left": "0", "Label": { "lang_tag": "dem_about", - "text": "DeusExMachina II ver 2.4RC3 2016-12-17
    For documentation or to report bugs, please go to the DeusExMachina Github repository
    This plugin is offered for use as-is and without warranties of any kind. By using this plugin, you agree to assume all risks in connection with its use without limitation.." + "text": "DeusExMachina II ver 2.4RC3 2016-12-21
    For documentation or to report bugs, please go to the DeusExMachina Github repository
    This plugin is offered for use as-is and without warranties of any kind. By using this plugin, you agree to assume all risks in connection with its use without limitation.." }, "Display": { "Top": "80", diff --git a/I_DeusExMachinaII1.xml b/I_DeusExMachinaII1.xml index 75e66a5..b0b8a5b 100644 --- a/I_DeusExMachinaII1.xml +++ b/I_DeusExMachinaII1.xml @@ -506,7 +506,7 @@ luup.log("DeusExMachinaII::targetControl(): on scene is " .. tostring(onScene) . -- Initialize. function deusInit(deusDevice) - luup.log("DeusExMachinaII::deusInit(): Version 2.4RC3 (2016-12-17), initializing...") + luup.log("DeusExMachinaII::deusInit(): Version 2.4RC3 (2016-12-21), initializing...") -- One-time stuff runOnce() From c9a3a97f1bab57b9dbaa3f216f4657c7c95979a7 Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Fri, 23 Dec 2016 13:13:02 -0500 Subject: [PATCH 22/49] Move most code off to module --- I_DeusExMachinaII1.xml | 664 +---------------------------------------- L_DeusExMachinaII1.lua | 649 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 661 insertions(+), 652 deletions(-) create mode 100644 L_DeusExMachinaII1.lua diff --git a/I_DeusExMachinaII1.xml b/I_DeusExMachinaII1.xml index b0b8a5b..d647e1d 100644 --- a/I_DeusExMachinaII1.xml +++ b/I_DeusExMachinaII1.xml @@ -7,665 +7,25 @@ -- Original code and releases 1.x by Andy Lintner (beowulfe) Version 2.0 and beyond by Patrick Rigney (rigpapa/toggledbits). -- A big thanks to Andy for passing the torch so that this great plug-in can live on. -- ------------------------------------------------------------------------------------------------------------------------- - - SID = "urn:toggledbits-com:serviceId:DeusExMachinaII1" - DEMVERSION = 20400 - - SWITCH_TYPE = "urn:schemas-upnp-org:device:BinaryLight:1" - SWITCH_SID = "urn:upnp-org:serviceId:SwitchPower1" - DIMMER_TYPE = "urn:schemas-upnp-org:device:DimmableLight:1" - DIMMER_SID = "urn:upnp-org:serviceId:Dimming1" - - STATE_STANDBY = 0 - STATE_IDLE = 1 - STATE_CYCLE = 2 - STATE_SHUTDOWN = 3 - - runStamp = 0 - - local function checkVersion() - local ui7Check = luup.variable_get(SID, "UI7Check", lul_device) or "" - if ui7Check == "" then - luup.variable_set(SID, "UI7Check", "false", lul_device) - ui7Check = "false" - end - if ( luup.version_branch == 1 and luup.version_major == 7 and ui7Check == "false" ) then - luup.variable_set(SID, "UI7Check", "true", lul_device) - luup.attr_set("device_json", "D_DeusExMachinaII1_UI7.json", lul_device) - luup.reload() - end - end - - -- Get numeric variable, or return default value if not set or blank - local function getVarNumeric( name, dflt ) - local s = luup.variable_get(SID, name, lul_device) - if (s == nil or s == "") then return dflt end - s = tonumber(s, 10) - if (s == nil) then return dflt end - return s - end - - -- Delete a variable (if we can... read on...) - local function deleteVar( name, devid ) - if (devid == nil) then devid = luup.device end - -- Interestingly, setting a variable to nil with luup.variable_set does nothing interesting; too bad, it - -- could have been used to delete variables, since a later get would yield nil anyway. But it turns out - -- that using the variableget Luup request with no value WILL delete the variable. - local req = "http://127.0.0.1:3480/data_request?id=variableset&DeviceNum=" .. tostring(devid) .. "&serviceId=" .. SID .. "&Variable=" .. name .. "&Value=" - -- luup.log("DeusExMachinaII::deleteVar(" .. name .. "): req=" .. tostring(req)) - local status, result = luup.inet.wget(req) - -- luup.log("DeusExMachinaII::deleteVar(" .. name .. "): status=" .. tostring(status) .. ", result=" .. tostring(result)) - end - - -- Shortcut function to return state of SwitchPower1 Status variable - local function isEnabled() - local s = luup.variable_get(SWITCH_SID, "Status", lul_device) - if (s == nil or s == "") then return false end - return (s ~= "0") - end - - local function isActiveHouseMode() - -- Fetch our mask bits that tell us what modes we operate in. If 0, we're not checking house mode. - local modebits = getVarNumeric("HouseModes", 0) - if (modebits ~= 0) then - -- Get the current house mode. There seems to be some disharmony in the correct way to go - -- about this, but the method (uncommented) below works. - -- local currentMode = luup.attr_get("Mode") -- alternate method - local currentMode - local status - status, currentMode = luup.inet.wget("http://127.0.0.1:3480/data_request?id=variableget&Variable=Mode", 2) - if status ~= 0 then - luup.log("DeusExMachinaII::isActiveHouseMode(): can't get current house mode, status=" .. tostring(status)) - currentMode = 0 - end - - -- Check to see if house mode bits are non-zero, and if so, apply current mode as mask. - -- If bit is set (current mode is in the bitset), we can run, otherwise skip. - local bit = require("bit") - -- Get the current house mode (1=Home,2=Away,3=Night,4=Vacation) - currentMode = math.pow(2, tonumber(currentMode,10)) - if (bit.band(modebits, currentMode) == 0) then - luup.log('DeusExMachinaII::isActiveHouseMode(): Current mode bit ' .. string.format("0x%x", currentMode) .. ' not in set ' .. string.format("0x%x", modebits)) - return false -- not active in this mode - else - luup.log('DeusExMachinaII::isActiveHouseMode(): Current mode bit ' .. string.format("0x%x", currentMode) .. ' SET in ' .. string.format("0x%x", modebits)) - end - end - return true -- default is we're active in the current house mode - end - - -- Get a random delay from two state variables. Error check. - function getRandomDelay(minStateName,maxStateName,defMin,defMax) - if defMin == nil then defMin = 300 end - if defMax == nil then defMax = 1800 end - local mind = getVarNumeric(minStateName, defMin) - if mind < 1 then mind = 1 elseif mind > 7200 then mind = 7200 end - local maxd = getVarNumeric(maxStateName, defMax) - if maxd < 1 then maxd = 1 elseif maxd > 7200 then maxd = 7200 end - if maxd < mind then maxd = mind end - return math.random( mind, maxd ) - end - - -- Get sunset time in minutes since midnight. May override for test mode value. - function getSunset() - -- Figure out our sunset time. Note that if we make this inquiry after sunset, MiOS - -- returns the time of tomorrow's sunset. But, that's not different enough from today's - -- that it really matters to us, so go with it. - local sunset = luup.sunset() - local testing = getVarNumeric("TestMode", 0) - if (testing ~= 0) then - local m = getVarNumeric( "TestSunset", nil ) -- units are minutes since midnight - if (m ~= nil) then - -- Sub in our test sunset time - local t = os.date('*t', sunset) - t['hour'] = math.floor(m / 60) - t['min'] = math.floor(m % 60) - t['sec'] = 0 - sunset = os.time(t) - end - luup.log('DeusExMachinaII::getSunset(): testing mode sunset override ' .. tostring(m) .. ', as time is ' .. tostring(sunset)) - end - if (sunset <= os.time()) then sunset = sunset + 86400 end - return sunset - end - - function getSunsetMSM() - local t = os.date('*t', getSunset()); - return t['hour']*60 + t['min'] - end - - -- DEM cycles lights between sunset and the user-specified off time. This function returns 0 - -- if the current time is between sunset and off; otherwise 1. Note that all times are reduced - -- to minutes-since-midnight units. - local function isBedtime() - local testing = getVarNumeric("TestMode", 0) - if (testing ~= 0) then luup.log('DeusExMachinaII::isBedtime(): TestMode is on') end - - -- Establish the lights-out time - local bedtime = 1439 -- that's 23:59 in minutes since midnight (default) - local bedtime_tmp = luup.variable_get(SID, "LightsOut", lul_device) - if (bedtime_tmp ~= nil) then - bedtime_tmp = tonumber(bedtime_tmp,10) - if (bedtime_tmp >= 0 and bedtime_tmp < 1440) then bedtime = bedtime_tmp end - end - - -- Figure out our sunset time. - local sunset = getSunsetMSM() - - -- And the current time. - local date = os.date('*t') - local time = date['hour'] * 60 + date['min'] - - -- Figure out if we're betweeen sunset and lightout (ret=0) or not (ret=1) - if (testing ~= 0) then - luup.log('DeusExMachinaII::isBedtime(): times (mins since midnight) are now=' .. tostring(time) .. ', sunset=' .. tostring(sunset) .. ', bedtime=' .. tostring(bedtime)) - end - local ret = 1 -- guilty until proven innocent - if (bedtime > sunset) then - -- Case 1: bedtime is after sunset (i.e. between sunset and midnight) - if (time >= sunset and time < bedtime) then - ret = 0 - end - else - -- Case 2: bedtime is after midnight - if (time >= sunset or time < bedtime) then - ret = 0 - end - end - if (testing ~= 0) then luup.log("DeusExMachinaII::isBedtime(): returning " .. tostring(ret)) end - return ret - end - - -- Take a string and split it around sep, returning table (indexed) of substrings - -- For example abc,def,ghi becomes t[1]=abc, t[2]=def, t[3]=ghi - -- Returns: table of values, count of values (integer ge 0) - local function split(s, sep) - local t = {} - local n = 0 - if (#s == 0) then return t,n end -- empty string returns nothing - local i,j - local k = 1 - repeat - i, j = string.find(s, sep or "%s*,%s*", k) - if (i == nil) then - table.insert(t, string.sub(s, k, -1)) - n = n + 1 - break - else - table.insert(t, string.sub(s, k, i-1)) - n = n + 1 - k = j + 1 - end - until k > string.len(s) - return t, n - end - - -- Return true if a specified scene has been run (i.e. on the list) - local function isSceneOn(spec) - local stateList = luup.variable_get(SID, "ScenesRunning", lul_device) or "" - for i in string.gfind(stateList, "[^,]+") do - if (i == spec) then return true end - end - return false - end - - -- Mark or unmark a scene as having been run - local function updateSceneState(spec, isOn) - local stateList = luup.variable_get(SID, "ScenesRunning", lul_device) or "" - local i - local t = {} - for i in string.gfind(stateList, "[^,]+") do - t[i] = 1 - end - if (isOn) then - t[spec] = 1 - else - t[spec] = nil - end - stateList = "" - for i in pairs(t) do stateList = stateList .. "," .. tostring(i) end - luup.variable_set(SID, "ScenesRunning", string.sub(stateList, 2, -1), lul_device) - end - - -- Run "final" scene, if defined. This scene is run after all other targets have been - -- turned off. - local function runFinalScene() - local scene = getVarNumeric("FinalScene", nil) - if (scene ~= nil) then - luup.log("DeusExMachina::runFinalScene(): running final scene " .. tostring(scene)) - luup.call_action("urn:micasaverde-com:serviceId:HomeAutomationGateway1", "RunScene", { SceneNum=scene }, 0) - end - end - - -- Get the list of targets from our device state, parse to table of targets. - local function getTargetList() - local s = luup.variable_get(SID, "Devices", lul_device) or "" - return split(s) - end - - -- Remove a target from the target list. Used when the target no longer exists. Linear, poor, but short list and rarely used. - local function removeTarget(target, tlist) - if tlist == nil then tlist = getTargetList() end - local i - for i = 1,table.getn(tlist) do - if tostring(target) == tlist[i] then - table.remove(tlist, i) - luup.variable_set(SID, "Devices", table.concat(tlist, ","), lul_device) - return true - end - end - return false - end - - -- Light on or off? Returns boolean - local function isDeviceOn(targetid) - local first = string.upper(string.sub(targetid, 1, 1)) - if (first == "S") then - -- luup.log("DeusExMachinaII::isDeviceOn(): handling scene spec " .. targetid) - return isSceneOn(targetid) - end - - -- Handle as switch or dimmer - -- luup.log("DeusExMachinaII::isDeviceOn(): handling target spec " .. targetid) - local r = tonumber(string.match(targetid, '^%d+'), 10) - -- luup.log("DeusExMachinaII::isDeviceOn(): target number seems to be " .. tostring(r)) - local val = "0" - if (luup.devices[r] ~= nil) then - if luup.device_supports_service(DIMMER_SID, r) then - val = luup.variable_get(DIMMER_SID, "LoadLevelStatus", r) - elseif luup.device_supports_service(SWITCH_SID, r) then - val = luup.variable_get(SWITCH_SID, "Status", r) - end - else - luup.log("DeusExMachinaII::isDeviceOn(): target spec " .. tostring(targetid) .. " device " .. tostring(r) .. ", device not found in luup.devices") - removeTarget(targetid) - return nil - end - return val ~= "0" - end - - -- Control target. Target is a string, expected to be a pure integer (in which case the target is assumed to be a switch or dimmer), - -- or a string in the form Sxx:yy, in which case xx is an "on" scene to run, and yy is an "off" scene to run. - local function targetControl(targetid, turnOn) - luup.log("DeusExMachinaII::targetControl(): targetid=" .. tostring(targetid) .. ", turnOn=" .. tostring(turnOn)) - local first = string.upper(string.sub(targetid, 1, 1)) - if first == "S" then -luup.log("DeusExMachinaII::targetControl(): handling scene spec " .. targetid) - i, j, onScene, offScene = string.find(string.sub(targetid, 2), "(%d+)-(%d+)") - if (i == nil) then - luup.log("DeusExMachina::targetControl(): malformed scene spec=" .. targetid) - return - end - onScene = tonumber(onScene, 10) - offScene = tonumber(offScene, 10) - if luup.scenes[onScene] == nil or luup.scenes[offScene] == nil then - -- Both on scene and off scene must exist (defensive). - luup.log("DeusExMachinaII::targetControl(): one or both of the scenes in " .. tostring(targetid) .. " not found in luup.scenes!") - return - end -luup.log("DeusExMachinaII::targetControl(): on scene is " .. tostring(onScene) .. ", off scene is " .. tostring(offScene)) - local targetScene - if turnOn then targetScene = onScene else targetScene = offScene end - luup.call_action("urn:micasaverde-com:serviceId:HomeAutomationGateway1", "RunScene", { SceneNum=targetScene }, 0) - updateSceneState(targetid, turnOn) - else - -- Parse the level if this is a dimming target spec - local lvl = 100 - local k = string.find(targetid, '=') - if k ~= nil then - _, _, targetid, lvl = string.find(targetid, "(%d+)=(%d+)") - lvl = tonumber(lvl, 10) - end - targetid = tonumber(targetid, 10) - -- Level for all types is 0 if turning device off - if not turnOn then lvl = 0 end - -- luup.log("DeusExMachinaII::targetControl(): handling device " .. tostring(targetid) .. ", level " .. tostring(lvl)) - if luup.devices[targetid] == nil then - -- Device doesn't exist (user deleted, etc.). PHR??? Should remove from Devices state variable. - luup.log("DeusExMachinaII::targetControl(): device " .. tostring(targetid) .. " not found in luup.devices"); - removeTarget(targetid) - return - end - if luup.device_supports_service(DIMMER_SID, targetid) then - -- Handle as Dimming1 - luup.log("DeusExMachinaII: targetControl(): handling " .. targetid .. " as dimmer, setting load level to " .. lvl) - luup.call_action(DIMMER_SID, "SetLoadLevelTarget", {newLoadlevelTarget=lvl}, targetid) -- note odd case inconsistency - elseif luup.device_supports_service(SWITCH_SID, targetid) then - -- Handle as SwitchPower1 - if turnOn then lvl = 1 end - luup.log("DeusExMachinaII: targetControl(): handling " .. targetid .. " as binary light, setting target to " .. lvl) - luup.call_action(SWITCH_SID, "SetTarget", {newTargetValue=lvl}, targetid) - else - luup.log("DeusExMachinaII: targetControl(): don't know how to control target " .. targetid) - end - end - end - - -- Get list of targets that are on - local function getTargetsOn() - local devs, max - local on = {} - local n = 0 - devs,max = getTargetList() - if (max > 0) then - local i - for i = 1,max do - local devOn = isDeviceOn(devs[i]) - if (devOn ~= nil and devOn) then - table.insert(on, devs[i]) - n = n + 1 - end - end - end - return on,n - end - - -- Turn off a light, if any is on. Returns 1 if there are more lights to turn off; otherwise 0. - local function turnOffLight(on) - local n - if on == nil then - on, n = getTargetsOn() - else - n = table.getn(on) - end - if (n > 0) then - local i = math.random(1, n) - local target = on[i] - targetControl(target, false) - table.remove(on, i) - n = n - 1 - luup.log("DeusExMachinaII::turnOffLight(): turned " .. tostring(target) .. " OFF, " .. tostring(n) .. " targets still on.") - end - return (n > 0), on, n - end - - -- Turn off all lights as fast as we can. Transition through SHUTDOWN state during, - -- in case user has any triggers connected to that state. The caller must immediately - -- set the next state when this function returns (expected would be STANDBY or IDLE). - function clearLights() - local devs, count - devs, count = getTargetList() - luup.variable_set(SID, "State", STATE_SHUTDOWN, lul_device) - while count > 0 do - targetControl(devs[count], false) - count = count - 1 - end - runFinalScene() - end - - -- runOnce() looks to see if a core state variable exists; if not, a one-time initialization - -- takes place. For us, that means looking to see if an older version of Deus is still - -- installed, and copying its config into our new config. Then disable the old Deus. - local function runOnce() - local s = luup.variable_get(SID, "Devices", lul_device) - if (s == nil) then - luup.log("DeusExMachinaII::runOnce(): Devices variable not found, setting up new instance...") - -- See if there are variables from older version of DEM - -- Start by finding the old Deus device, if there is one... - local devList = "" - local i, olddev - olddev = -1 - for i,v in pairs(luup.devices) do - if (v.device_type == "urn:schemas-futzle-com:device:DeusExMachina:1") then - luup.log("DeusExMachinaII::runOnce(): Found old Deus Ex Machina device #" .. tostring(i)) - olddev = i - break - end - end - if (olddev > 0) then - -- We found an old Deus device, copy its config into our new state variables - local oldsid = "urn:futzle-com:serviceId:DeusExMachina1" - s = luup.variable_get(oldsid, "LightsOutTime", olddev) - if (s ~= nil) then - local n = tonumber(s,10) / 60000 - luup.variable_set(SID, "LightsOut", n, lul_device) - deleteVar("LightsOutTime", lul_device) - end - s = luup.variable_get(oldsid, "controlCount", olddev) - if (s ~= nil) then - local n = tonumber(s, 10) - local k - local t = {} - for k = 1,n do - s = luup.variable_get(oldsid, "control" .. tostring(k-1), olddev) - if (s ~= nil) then - table.insert(t, s) - end - end - devList = table.concat(t, ",") - deleteVar("controlCount", lul_device) - end - - -- Finally, turn off old Deus - luup.call_action(oldsid, "SetEnabled", { NewEnabledValue = "0" }, olddev) - end - luup.variable_set(SID, "Devices", devList, lul_device) - - -- Set up some other default config - luup.variable_set(SID, "MinCycleDelay", "300", lul_device) - luup.variable_set(SID, "MaxCycleDelay", "1800", lul_device) - luup.variable_set(SID, "MinOffDelay", "60", lul_device) - luup.variable_set(SID, "MaxOffDelay", "300", lul_device) - luup.variable_set(SID, "LightsOut", 1439, lul_device) - luup.variable_set(SID, "MaxTargetsOn", 0, lul_device) - luup.variable_set(SID, "Enabled", "0", lul_device, true) - luup.variable_set(SID, "Version", DEMVERSION, lul_device) - luup.variable_set(SWITCH_SID, "Status", "0", lul_device, true) - luup.variable_set(SWITCH_SID, "Target", "0", lul_device, true) - end - - -- Consider per-version changes. - s = getVarNumeric("Version", 0) - if (s < 20300) then - -- v2.3: LightsOutTime (in milliseconds) deprecated, now using LightsOut (in minutes since midnight) - luup.log("DeusExMachinaII::runOnce(): updating config, version " .. tostring(s) .. " < 20300") - s = luup.variable_get(SID, "LightsOut", lul_device) - if (s == nil) then - s = getVarNumeric("LightsOutTime") -- get pre-2.3 variable - if (s == nil) then - luup.variable_set(SID, "LightsOut", 1439, lul_device) -- default 23:59 - else - luup.variable_set(SID, "LightsOut", tonumber(s,10) / 60000, lul_device) -- conv ms to minutes - end - end - deleteVar("LightsOutTime", lul_device) - end - if (s < 20400) then - luup.variable_set(SID, "MaxTargetsOn", 0, lul_device) - local e = getVarNumeric("Enabled", 0) - luup.variable_set(SWITCH_SID, "Status", e, lul_device, true) - luup.variable_set(SWITCH_SID, "Target", e, lul_device, true) - end - - -- Update version last. - luup.variable_set(SID, "Version", DEMVERSION, lul_device) - end - - -- Enable DEM by setting a new cycle stamp and scheduling our first cycle step. - function deusEnable() - luup.log("DeusExMachinaII::deusEnable(): enabling...") - luup.variable_set(SID, "ScenesRunning", "", lul_device) -- start with a clean slate - runStamp = os.time() - luup.call_delay("deusStep", 1, runStamp, 1) - luup.variable_set(SID, "Enabled", "1", lul_device) - luup.variable_set(SWITCH_SID, "Status", "1", lul_device) - end - - -- Disable DEM and go to standby state. If we are currently cycling (as opposed to idle/waiting for sunset), - -- turn off any controlled lights that are on. - function deusDisable() - local s = getVarNumeric("State", STATE_STANDBY) - luup.log("DeusExMachinaII::deusDisable(): disabling...") - if ( s == STATE_CYCLE or s == STATE_SHUTDOWN ) then - clearLights() - end - luup.variable_set(SID, "State", STATE_STANDBY, lul_device) - luup.variable_set(SID, "Enabled", "0", lul_device) - luup.variable_set(SWITCH_SID, "Status", "0", lul_device) - end - - -- Initialize. - function deusInit(deusDevice) - luup.log("DeusExMachinaII::deusInit(): Version 2.4RC3 (2016-12-21), initializing...") - - -- One-time stuff - runOnce() - - -- Check UI version - checkVersion() - - -- Start up if we're enabled - if (isEnabled()) then - deusEnable() - else - deusDisable() - end - end - - -- Run a cycle. If we're in "bedtime" (i.e. not between our cycle period between sunset and stop), - -- then we'll shut off any lights we've turned on and queue another run for the next sunset. Otherwise, - -- we'll toggled one of our controlled lights, and queue (random delay, but soon) for another cycle. - -- The shutdown of lights also occurs randomly, but can (through device state/config) have different - -- delays, so the lights going off looks more "natural" (i.e. not all at once just slamming off). - function deusStep(stepStampCheck) - local stepStamp = tonumber(stepStampCheck) - luup.log("DeusExMachinaII::deusStep(): wakeup, stamp " .. stepStampCheck) - if (stepStamp ~= runStamp) then - luup.log("DeusExMachinaII::deusStep(): stamp mismatch, another thread running. Bye!") - return - end - if (not isEnabled()) then - luup.log("DeusExMachinaII::deusStep(): not enabled, no more work for this thread...") - return - end - - -- Get next sunset as seconds since midnight (approx) - local sunset = getSunset() - - local currentState = getVarNumeric("State", 0) - if (currentState == STATE_STANDBY or currentState == STATE_IDLE) then - luup.log("DeusExMachinaII::deusStep(): run in state " - .. tostring(currentState) - .. ", lightsout=" .. tostring(luup.variable_get(SID, "LightsOut", lul_device)) - .. ", sunset=" .. tostring(sunset) - .. ", os.time=" .. tostring(os.time()) - ) - luup.log("+ long=" - .. tostring(luup.longitude) - .. ", lat=" .. tostring(luup.latitude) - .. ", tz=" .. tostring(luup.timezone) - .. ", city=" .. tostring(luup.city) - .. ", luup.sunset=" .. tostring(luup.sunset()) - .. ", version=" .. tostring(luup.version) - ) - end - - local inActiveTimePeriod = true - if (isBedtime() ~= 0) then - luup.log("DeusExMachinaII::deusStep(): in lights out time") - inActiveTimePeriod = false - end - - -- Get going... - local nextCycleDelay = 300 -- a default value to keep us out of hot water - if (currentState == STATE_STANDBY and not inActiveTimePeriod) then - -- Transition from STATE_STANDBY (i.e. we're enabling) in the inactive period. - -- Go to IDLE and delay for next sunset. - luup.log("DeusExMachinaII::deusStep(): transitioning to IDLE from STANDBY, waiting for next sunset..."); - nextCycleDelay = sunset - os.time() + getRandomDelay("MinCycleDelay", "MaxCycleDelay") - luup.variable_set(SID, "State", STATE_IDLE, lul_device) - elseif (not isActiveHouseMode()) then - -- Not in an active house mode. If we're not STANDBY or IDLE, turn everything back off and go to IDLE. - if (currentState ~= STATE_IDLE) then - luup.log("DeusExMachinaII::deusStep(): transitioning to IDLE, not in an active house mode."); - if (currentState ~= STATE_STANDBY) then clearLights() end -- turn off lights quickly unless transitioning from STANDBY - luup.variable_set(SID, "State", STATE_IDLE, lul_device) - else - luup.log("DeusExMachinaII::deusStep(): IDLE in an inactive house mode; waiting for mode change."); - end - - -- Figure out how long to delay. If we're lights-out, delay to next sunset. Otherwise, short delay - -- to re-check house mode, which could change at any time, so we must deal with it. - if (inActiveTimePeriod) then - nextCycleDelay = getRandomDelay("MinCycleDelay", "MaxCycleDelay") - else - nextCycleDelay = sunset - os.time() + getRandomDelay("MinCycleDelay", "MaxCycleDelay") - end - elseif (not inActiveTimePeriod) then - luup.log("DeusExMachinaII::deusStep(): running off cycle") - luup.variable_set(SID, "State", STATE_SHUTDOWN, lul_device) - if (not turnOffLight()) then - -- No more lights to turn off - runFinalScene() - luup.variable_set(SID, "State", STATE_IDLE, lul_device) - nextCycleDelay = sunset - os.time() + getRandomDelay("MinCycleDelay", "MaxCycleDelay") - luup.log("DeusExMachina::deusStep(): all lights out; now IDLE, setting delay to restart cycling at next sunset") - else - nextCycleDelay = getRandomDelay("MinOffDelay", "MaxOffDelay", 60, 300) - end - else - -- Fully active. Find a random target to control and control it. - luup.log("DeusExMachinaII::deusStep(): running toggle cycle") - luup.variable_set(SID, "State", STATE_CYCLE, lul_device) - nextCycleDelay = getRandomDelay("MinCycleDelay", "MaxCycleDelay") - local devs, max - devs, max = getTargetList() - if (max > 0) then - local change = math.random(1, max) - local devspec = devs[change] - if (devspec ~= nil) then - local s = isDeviceOn(devspec) - if (s ~= nil) then - if (s) then - -- It's on; turn it off. - luup.log("DeusExMachinaII::deusStep(): turn " .. devspec .. " OFF") - targetControl(devspec, false) - else - -- Turn something on. If we're at the max number of targets we're allowed to turn on, - -- turn targets off first. - local maxOn = getVarNumeric("MaxTargetsOn") - if (maxOn == nil) then maxOn = 0 else maxOn = tonumber(maxOn,10) end - if (maxOn > 0) then - local on, n - on, n = getTargetsOn() - while ( n >= maxOn ) do - luup.log("DeusExMachinaII:deusStep(): too many targets on, max is " .. tostring(maxOn) - .. ", have " .. tostring(n) .. ", let's turn one off.") - _, on, n = turnOffLight(on) - end - end - luup.log("DeusExMachinaII::deusStep(): turn " .. devspec .. " ON") - targetControl(devspec, true) - end - end - end - else - luup.log("DeusExMachinaII:deusStep(): no targets to control") - end - end - - -- Arm for next cycle - if nextCycleDelay ~= nil then - luup.log("DeusExMachinaII::deusStep(): cycle finished, next in " .. nextCycleDelay .. " seconds") - if nextCycleDelay < 1 then nextCycleDelay = 60 end - luup.call_delay("deusStep", nextCycleDelay, stepStamp, 1) - else - luup.log("DeusExMachinaII::deusStep(): nil nextCycleDelay, next cycle not scheduled!"); - end + function startupDeusExMachinaII1() + luup.log("DeusExMachinaII STARTUP!") + demII = require("L_DeusExMachinaII1") + deusStep = demII.deusStep + demII.deusInit() end - deusInit + startupDeusExMachinaII1 urn:upnp-org:serviceId:SwitchPower1 SetTarget local newTargetValue = lul_settings.newTargetValue or "0" - luup.variable_set(SWITCH_SID, "Target", newTargetValue, lul_device) + luup.variable_set("urn:upnp-org:serviceId:SwitchPower1", "Target", newTargetValue, lul_device) if (newTargetValue == "1") then - deusEnable() + demII.deusEnable() else - deusDisable() + demII.deusDisable() end @@ -674,11 +34,11 @@ luup.log("DeusExMachinaII::targetControl(): on scene is " .. tostring(onScene) . SetEnabled local newEnabledValue = lul_settings.NewEnabledValue or "0" - luup.variable_set(SWITCH_SID, "Target", newEnabledValue, lul_device) + luup.variable_set("urn:upnp-org:serviceId:SwitchPower1", "Target", newEnabledValue, lul_device) if (newEnabledValue == "1") then - deusEnable() + demII.deusEnable() else - deusDisable() + demII.deusDisable() end
    diff --git a/L_DeusExMachinaII1.lua b/L_DeusExMachinaII1.lua new file mode 100644 index 0000000..9048e64 --- /dev/null +++ b/L_DeusExMachinaII1.lua @@ -0,0 +1,649 @@ +module("L_DeusExMachinaII1", package.seeall) + +local SID = "urn:toggledbits-com:serviceId:DeusExMachinaII1" +local DEMVERSION = 20400 + +local SWITCH_TYPE = "urn:schemas-upnp-org:device:BinaryLight:1" +local SWITCH_SID = "urn:upnp-org:serviceId:SwitchPower1" +local DIMMER_TYPE = "urn:schemas-upnp-org:device:DimmableLight:1" +local DIMMER_SID = "urn:upnp-org:serviceId:Dimming1" + +local STATE_STANDBY = 0 +local STATE_IDLE = 1 +local STATE_CYCLE = 2 +local STATE_SHUTDOWN = 3 + +local runStamp = 0 + +local debugMode = true + +local function debug(...) + if debugMode then + local str = "DeusExMachinaII1:" .. arg[1] + local ipos = 1 + while true do + local i, j, n + i, j, n = string.find(str, "%%(%d+)", ipos) + if i == nil then break end + n = tonumber(n, 10) + if n >= 1 and n < table.getn(arg) then + if i == 1 then + str = tostring(arg[n+1]) .. string.sub(str, j+1) + else + str = string.sub(str, 1, i-1) .. tostring(arg[n+1]) .. string.sub(str, j+1) + end + end + ipos = j + 1 + end + luup.log(str) + end +end + +local function checkVersion() + local ui7Check = luup.variable_get(SID, "UI7Check", lul_device) or "" + if ui7Check == "" then + luup.variable_set(SID, "UI7Check", "false", lul_device) + ui7Check = "false" + end + if ( luup.version_branch == 1 and luup.version_major == 7 and ui7Check == "false" ) then + luup.variable_set(SID, "UI7Check", "true", lul_device) + luup.attr_set("device_json", "D_DeusExMachinaII1_UI7.json", lul_device) + luup.reload() + end +end + +-- Get numeric variable, or return default value if not set or blank +local function getVarNumeric( name, dflt ) + local s = luup.variable_get(SID, name, lul_device) + if (s == nil or s == "") then return dflt end + s = tonumber(s, 10) + if (s == nil) then return dflt end + return s +end + +-- Delete a variable (if we can... read on...) +local function deleteVar( name, devid ) + if (devid == nil) then devid = luup.device end + -- Interestingly, setting a variable to nil with luup.variable_set does nothing interesting; too bad, it + -- could have been used to delete variables, since a later get would yield nil anyway. But it turns out + -- that using the variableget Luup request with no value WILL delete the variable. + local req = "http://127.0.0.1:3480/data_request?id=variableset&DeviceNum=" .. tostring(devid) .. "&serviceId=" .. SID .. "&Variable=" .. name .. "&Value=" + -- debug("DeusExMachinaII::deleteVar(" .. name .. "): req=" .. tostring(req)) + local status, result = luup.inet.wget(req) + -- debug("DeusExMachinaII::deleteVar(" .. name .. "): status=" .. tostring(status) .. ", result=" .. tostring(result)) +end + +-- Shortcut function to return state of SwitchPower1 Status variable +local function isEnabled() + local s = luup.variable_get(SWITCH_SID, "Status", lul_device) + if (s == nil or s == "") then return false end + return (s ~= "0") +end + +local function isActiveHouseMode() + -- Fetch our mask bits that tell us what modes we operate in. If 0, we're not checking house mode. + local modebits = getVarNumeric("HouseModes", 0) + if (modebits ~= 0) then + -- Get the current house mode. There seems to be some disharmony in the correct way to go + -- about this, but the method (uncommented) below works. + local currentMode = luup.attr_get("Mode", 0) -- alternate method + + -- Check to see if house mode bits are non-zero, and if so, apply current mode as mask. + -- If bit is set (current mode is in the bitset), we can run, otherwise skip. + local bit = require("bit") + -- Get the current house mode (1=Home,2=Away,3=Night,4=Vacation) + currentMode = math.pow(2, tonumber(currentMode,10)) + if (bit.band(modebits, currentMode) == 0) then + debug('DeusExMachinaII::isActiveHouseMode(): Current mode bit %1 not set in %2', string.format("0x%x", currentMode), string.format("0x%x", modebits)) + return false -- not active in this mode + else + debug('DeusExMachinaII::isActiveHouseMode(): Current mode bit %1 SET in %2', string.format("0x%x", currentMode), string.format("0x%x", modebits)) + end + end + return true -- default is we're active in the current house mode +end + +-- Get a random delay from two state variables. Error check. +local function getRandomDelay(minStateName,maxStateName,defMin,defMax) + if defMin == nil then defMin = 300 end + if defMax == nil then defMax = 1800 end + local mind = getVarNumeric(minStateName, defMin) + if mind < 1 then mind = 1 elseif mind > 7200 then mind = 7200 end + local maxd = getVarNumeric(maxStateName, defMax) + if maxd < 1 then maxd = 1 elseif maxd > 7200 then maxd = 7200 end + if maxd < mind then maxd = mind end + return math.random( mind, maxd ) +end + +-- Get sunset time in minutes since midnight. May override for test mode value. +local function getSunset() + -- Figure out our sunset time. Note that if we make this inquiry after sunset, MiOS + -- returns the time of tomorrow's sunset. But, that's not different enough from today's + -- that it really matters to us, so go with it. + local sunset = luup.sunset() + local testing = getVarNumeric("TestMode", 0) + if (testing ~= 0) then + local m = getVarNumeric( "TestSunset", nil ) -- units are minutes since midnight + if (m ~= nil) then + -- Sub in our test sunset time + local t = os.date('*t', sunset) + t['hour'] = math.floor(m / 60) + t['min'] = math.floor(m % 60) + t['sec'] = 0 + sunset = os.time(t) + end + debug('getSunset(): testing mode sunset override %1, as timeval is %2', m, sunset) + end + if (sunset <= os.time()) then sunset = sunset + 86400 end + return sunset +end + +local function getSunsetMSM() + local t = os.date('*t', getSunset()) + return t['hour']*60 + t['min'] +end + +-- DEM cycles lights between sunset and the user-specified off time. This function returns 0 +-- if the current time is between sunset and off; otherwise 1. Note that all times are reduced +-- to minutes-since-midnight units. +local function isBedtime() + local testing = getVarNumeric("TestMode", 0) + if (testing ~= 0) then luup.log('DeusExMachinaII::isBedtime(): TestMode is on') end + + -- Establish the lights-out time + local bedtime = 1439 -- that's 23:59 in minutes since midnight (default) + local bedtime_tmp = luup.variable_get(SID, "LightsOut", lul_device) + if (bedtime_tmp ~= nil) then + bedtime_tmp = tonumber(bedtime_tmp,10) + if (bedtime_tmp >= 0 and bedtime_tmp < 1440) then bedtime = bedtime_tmp end + end + + -- Figure out our sunset time. + local sunset = getSunsetMSM() + + -- And the current time. + local date = os.date('*t') + local time = date['hour'] * 60 + date['min'] + + -- Figure out if we're betweeen sunset and lightout (ret=0) or not (ret=1) + debug('isBedtime(): times (mins since midnight) are now=%1, sunset=%2, bedtime=%3', time, sunset, bedtime) + local ret = 1 -- guilty until proven innocent + if (bedtime > sunset) then + -- Case 1: bedtime is after sunset (i.e. between sunset and midnight) + if (time >= sunset and time < bedtime) then + ret = 0 + end + else + -- Case 2: bedtime is after midnight + if (time >= sunset or time < bedtime) then + ret = 0 + end + end + debug('isBedtime(): returning %1", ret) + return ret +end + +-- Take a string and split it around sep, returning table (indexed) of substrings +-- For example abc,def,ghi becomes t[1]=abc, t[2]=def, t[3]=ghi +-- Returns: table of values, count of values (integer ge 0) +local function split(s, sep) + local t = {} + local n = 0 + if (#s == 0) then return t,n end -- empty string returns nothing + local i,j + local k = 1 + repeat + i, j = string.find(s, sep or "%s*,%s*", k) + if (i == nil) then + table.insert(t, string.sub(s, k, -1)) + n = n + 1 + break + else + table.insert(t, string.sub(s, k, i-1)) + n = n + 1 + k = j + 1 + end + until k > string.len(s) + return t, n +end + +-- Return true if a specified scene has been run (i.e. on the list) +local function isSceneOn(spec) + local stateList = luup.variable_get(SID, "ScenesRunning", lul_device) or "" + for i in string.gfind(stateList, "[^,]+") do + if (i == spec) then return true end + end + return false +end + +-- Mark or unmark a scene as having been run +local function updateSceneState(spec, isOn) + local stateList = luup.variable_get(SID, "ScenesRunning", lul_device) or "" + local i + local t = {} + for i in string.gfind(stateList, "[^,]+") do + t[i] = 1 + end + if (isOn) then + t[spec] = 1 + else + t[spec] = nil + end + stateList = "" + for i in pairs(t) do stateList = stateList .. "," .. tostring(i) end + luup.variable_set(SID, "ScenesRunning", string.sub(stateList, 2, -1), lul_device) +end + +-- Run "final" scene, if defined. This scene is run after all other targets have been +-- turned off. +local function runFinalScene() + local scene = getVarNumeric("FinalScene", nil) + if (scene ~= nil) then + debug("runFinalScene(): running final scene %1", scene) + luup.call_action("urn:micasaverde-com:serviceId:HomeAutomationGateway1", "RunScene", { SceneNum=scene }, 0) + end +end + +-- Get the list of targets from our device state, parse to table of targets. +local function getTargetList() + local s = luup.variable_get(SID, "Devices", lul_device) or "" + return split(s) +end + +-- Remove a target from the target list. Used when the target no longer exists. Linear, poor, but short list and rarely used. +local function removeTarget(target, tlist) + if tlist == nil then tlist = getTargetList() end + local i + for i = 1,table.getn(tlist) do + if tostring(target) == tlist[i] then + table.remove(tlist, i) + luup.variable_set(SID, "Devices", table.concat(tlist, ","), lul_device) + return true + end + end + return false +end + +-- Light on or off? Returns boolean +local function isDeviceOn(targetid) + local first = string.upper(string.sub(targetid, 1, 1)) + if (first == "S") then + debug("isDeviceOn(): handling scene spec %1", targetid) + return isSceneOn(targetid) + end + + -- Handle as switch or dimmer + debug("isDeviceOn(): handling target spec %1", targetid) + local r = tonumber(string.match(targetid, '^%d+'), 10) + local val = "0" + if (luup.devices[r] ~= nil) then + if luup.device_supports_service(DIMMER_SID, r) then + val = luup.variable_get(DIMMER_SID, "LoadLevelStatus", r) + elseif luup.device_supports_service(SWITCH_SID, r) then + val = luup.variable_get(SWITCH_SID, "Status", r) + end + else + luup.log("DeusExMachinaII:isDeviceOn(): target spec " .. tostring(targetid) .. ", device " .. tostring(r) .. " not found in luup.devices") + removeTarget(targetid) + return nil + end + return val ~= "0" +end + +-- Control target. Target is a string, expected to be a pure integer (in which case the target is assumed to be a switch or dimmer), +-- or a string in the form Sxx:yy, in which case xx is an "on" scene to run, and yy is an "off" scene to run. +local function targetControl(targetid, turnOn) + debug("targetControl(): targetid=%1, turnOn=%2", targetid, turnOn) + local first = string.upper(string.sub(targetid, 1, 1)) + if first == "S" then + debug("targetControl(): handling as scene spec %1", targetid) + i, j, onScene, offScene = string.find(string.sub(targetid, 2), "(%d+)-(%d+)") + if (i == nil) then + luup.log("DeusExMachina:targetControl(): malformed scene spec=" .. tostring(targetid)) + return + end + onScene = tonumber(onScene, 10) + offScene = tonumber(offScene, 10) + if luup.scenes[onScene] == nil or luup.scenes[offScene] == nil then + -- Both on scene and off scene must exist (defensive). + luup.log("DeusExMachinaII:targetControl(): one or both of the scenes in " .. tostring(targetid) .. " not found in luup.scenes!") + return + end + debug("targetControl(): on scene is %1, off scene is %2", onScene, offScene) + local targetScene + if turnOn then targetScene = onScene else targetScene = offScene end + luup.call_action("urn:micasaverde-com:serviceId:HomeAutomationGateway1", "RunScene", { SceneNum=targetScene }, 0) + updateSceneState(targetid, turnOn) + else + -- Parse the level if this is a dimming target spec + local lvl = 100 + local k = string.find(targetid, '=') + if k ~= nil then + _, _, targetid, lvl = string.find(targetid, "(%d+)=(%d+)") + lvl = tonumber(lvl, 10) + end + targetid = tonumber(targetid, 10) + -- Level for all types is 0 if turning device off + if not turnOn then lvl = 0 end + if luup.devices[targetid] == nil then + -- Device doesn't exist (user deleted, etc.). PHR??? Should remove from Devices state variable. + luup.log("DeusExMachinaII:targetControl(): device " .. tostring(targetid) .. " not found in luup.devices") + removeTarget(targetid) + return + end + if luup.device_supports_service(DIMMER_SID, targetid) then + -- Handle as Dimming1 + debug("targetControl(): handling %1 as dimmmer, set load level to %2", targetid, lvl) + luup.call_action(DIMMER_SID, "SetLoadLevelTarget", {newLoadlevelTarget=lvl}, targetid) -- note odd case inconsistency + elseif luup.device_supports_service(SWITCH_SID, targetid) then + -- Handle as SwitchPower1 + if turnOn then lvl = 1 end + debug("targetControl(): handling %1 as switch, setting target to %2", targetid, lvl) + luup.call_action(SWITCH_SID, "SetTarget", {newTargetValue=lvl}, targetid) + else + luup.log("DeusExMachinaII:targetControl(): don't know how to control target " .. tostring(targetid)) + end + end +end + +-- Get list of targets that are on +local function getTargetsOn() + local devs, max + local on = {} + local n = 0 + devs,max = getTargetList() + if (max > 0) then + local i + for i = 1,max do + local devOn = isDeviceOn(devs[i]) + if (devOn ~= nil and devOn) then + table.insert(on, devs[i]) + n = n + 1 + end + end + end + return on,n +end + +-- Turn off a light, if any is on. Returns 1 if there are more lights to turn off; otherwise 0. +local function turnOffLight(on) + local n + if on == nil then + on, n = getTargetsOn() + else + n = table.getn(on) + end + if (n > 0) then + local i = math.random(1, n) + local target = on[i] + targetControl(target, false) + table.remove(on, i) + n = n - 1 + debug(":turnOffLight(): turned %1 OFF, still %2 targets on", target, n) + end + return (n > 0), on, n +end + +-- Turn off all lights as fast as we can. Transition through SHUTDOWN state during, +-- in case user has any triggers connected to that state. The caller must immediately +-- set the next state when this function returns (expected would be STANDBY or IDLE). +local function clearLights() + local devs, count + devs, count = getTargetList() + luup.variable_set(SID, "State", STATE_SHUTDOWN, lul_device) + while count > 0 do + targetControl(devs[count], false) + count = count - 1 + end + runFinalScene() +end + +-- runOnce() looks to see if a core state variable exists; if not, a one-time initialization +-- takes place. For us, that means looking to see if an older version of Deus is still +-- installed, and copying its config into our new config. Then disable the old Deus. +local function runOnce() + local s = luup.variable_get(SID, "Devices", lul_device) + if (s == nil) then + luup.log("DeusExMachinaII:runOnce(): Devices variable not found, setting up new instance...") + -- See if there are variables from older version of DEM + -- Start by finding the old Deus device, if there is one... + local devList = "" + local i, olddev + olddev = -1 + for i,v in pairs(luup.devices) do + if (v.device_type == "urn:schemas-futzle-com:device:DeusExMachina:1") then + luup.log("DeusExMachinaII:runOnce(): Found old Deus Ex Machina device #" .. tostring(i)) + olddev = i + break + end + end + if (olddev > 0) then + -- We found an old Deus device, copy its config into our new state variables + local oldsid = "urn:futzle-com:serviceId:DeusExMachina1" + s = luup.variable_get(oldsid, "LightsOutTime", olddev) + if (s ~= nil) then + local n = tonumber(s,10) / 60000 + luup.variable_set(SID, "LightsOut", n, lul_device) + deleteVar("LightsOutTime", lul_device) + end + s = luup.variable_get(oldsid, "controlCount", olddev) + if (s ~= nil) then + local n = tonumber(s, 10) + local k + local t = {} + for k = 1,n do + s = luup.variable_get(oldsid, "control" .. tostring(k-1), olddev) + if (s ~= nil) then + table.insert(t, s) + end + end + devList = table.concat(t, ",") + deleteVar("controlCount", lul_device) + end + + -- Finally, turn off old Deus + luup.call_action(oldsid, "SetEnabled", { NewEnabledValue = "0" }, olddev) + end + luup.variable_set(SID, "Devices", devList, lul_device) + + -- Set up some other default config + luup.variable_set(SID, "MinCycleDelay", "300", lul_device) + luup.variable_set(SID, "MaxCycleDelay", "1800", lul_device) + luup.variable_set(SID, "MinOffDelay", "60", lul_device) + luup.variable_set(SID, "MaxOffDelay", "300", lul_device) + luup.variable_set(SID, "LightsOut", 1439, lul_device) + luup.variable_set(SID, "MaxTargetsOn", 0, lul_device) + luup.variable_set(SID, "Enabled", "0", lul_device, true) + luup.variable_set(SID, "Version", DEMVERSION, lul_device) + luup.variable_set(SWITCH_SID, "Status", "0", lul_device, true) + luup.variable_set(SWITCH_SID, "Target", "0", lul_device, true) + end + + -- Consider per-version changes. + s = getVarNumeric("Version", 0) + if (s < 20300) then + -- v2.3: LightsOutTime (in milliseconds) deprecated, now using LightsOut (in minutes since midnight) + luup.log("DeusExMachinaII:runOnce(): updating config, version " .. tostring(s) .. " < 20300") + s = luup.variable_get(SID, "LightsOut", lul_device) + if (s == nil) then + s = getVarNumeric("LightsOutTime") -- get pre-2.3 variable + if (s == nil) then + luup.variable_set(SID, "LightsOut", 1439, lul_device) -- default 23:59 + else + luup.variable_set(SID, "LightsOut", tonumber(s,10) / 60000, lul_device) -- conv ms to minutes + end + end + deleteVar("LightsOutTime", lul_device) + end + if (s < 20400) then + luup.variable_set(SID, "MaxTargetsOn", 0, lul_device) + local e = getVarNumeric("Enabled", 0) + luup.variable_set(SWITCH_SID, "Status", e, lul_device, true) + luup.variable_set(SWITCH_SID, "Target", e, lul_device, true) + end + + -- Update version last. + luup.variable_set(SID, "Version", DEMVERSION, lul_device) +end + +-- Enable DEM by setting a new cycle stamp and scheduling our first cycle step. +function deusEnable() + luup.log("DeusExMachinaII:deusEnable(): enabling...") + luup.variable_set(SID, "ScenesRunning", "", lul_device) -- start with a clean slate + runStamp = os.time() + luup.call_delay("deusStep", 1, runStamp, 1) + luup.variable_set(SID, "Enabled", "1", lul_device) + luup.variable_set(SWITCH_SID, "Status", "1", lul_device) +end + +-- Disable DEM and go to standby state. If we are currently cycling (as opposed to idle/waiting for sunset), +-- turn off any controlled lights that are on. +function deusDisable() + local s = getVarNumeric("State", STATE_STANDBY) + luup.log("DeusExMachinaII:deusDisable(): disabling...") + if ( s == STATE_CYCLE or s == STATE_SHUTDOWN ) then + clearLights() + end + luup.variable_set(SID, "State", STATE_STANDBY, lul_device) + luup.variable_set(SID, "Enabled", "0", lul_device) + luup.variable_set(SWITCH_SID, "Status", "0", lul_device) +end + +-- Initialize. +function deusInit(deusDevice) + luup.log("DeusExMachinaII::deusInit(): Version 2.4RC3 (2016-12-21), initializing...") + + -- One-time stuff + runOnce() + + -- Check UI version + checkVersion() + + -- Start up if we're enabled + if (isEnabled()) then + deusEnable() + else + deusDisable() + end +end + +-- Run a cycle. If we're in "bedtime" (i.e. not between our cycle period between sunset and stop), +-- then we'll shut off any lights we've turned on and queue another run for the next sunset. Otherwise, +-- we'll toggled one of our controlled lights, and queue (random delay, but soon) for another cycle. +-- The shutdown of lights also occurs randomly, but can (through device state/config) have different +-- delays, so the lights going off looks more "natural" (i.e. not all at once just slamming off). +function deusStep(stepStampCheck) + local stepStamp = tonumber(stepStampCheck) + luup.log("DeusExMachinaII:deusStep(): wakeup, stamp " .. stepStampCheck) + if (stepStamp ~= runStamp) then + luup.log("DeusExMachinaII:deusStep(): stamp mismatch, another thread running. Bye!") + return + end + if (not isEnabled()) then + luup.log("DeusExMachinaII:deusStep(): not enabled, no more work for this thread...") + return + end + + -- Get next sunset as seconds since midnight (approx) + local sunset = getSunset() + + local currentState = getVarNumeric("State", 0) + if (currentState == STATE_STANDBY or currentState == STATE_IDLE) then + debug("deusStep(): run in state %1, lightsout=%2, sunset=%3, os.time=%4", currentState, + luup.variable_get(SID, "LightsOut", lul_device), sunset, os.time()) + debug("deusStep(): luup variables longitude=%1, latitude=%2, timezone=%3, city=%4, sunset=%5, version=%6", + luup.longitude, luup.latitude, luup.timezone, luup.city, luup.sunset(), luup.version) + end + + local inActiveTimePeriod = true + if (isBedtime() ~= 0) then + luup.log("DeusExMachinaII:deusStep(): in lights out time") + inActiveTimePeriod = false + end + + -- Get going... + local nextCycleDelay = 300 -- a default value to keep us out of hot water + if (currentState == STATE_STANDBY and not inActiveTimePeriod) then + -- Transition from STATE_STANDBY (i.e. we're enabling) in the inactive period. + -- Go to IDLE and delay for next sunset. + luup.log("DeusExMachinaII::deusStep(): transitioning to IDLE from STANDBY, waiting for next sunset...") + nextCycleDelay = sunset - os.time() + getRandomDelay("MinCycleDelay", "MaxCycleDelay") + luup.variable_set(SID, "State", STATE_IDLE, lul_device) + elseif (not isActiveHouseMode()) then + -- Not in an active house mode. If we're not STANDBY or IDLE, turn everything back off and go to IDLE. + if (currentState ~= STATE_IDLE) then + luup.log("DeusExMachinaII::deusStep(): transitioning to IDLE, not in an active house mode.") + if (currentState ~= STATE_STANDBY) then clearLights() end -- turn off lights quickly unless transitioning from STANDBY + luup.variable_set(SID, "State", STATE_IDLE, lul_device) + else + luup.log("DeusExMachinaII::deusStep(): IDLE in an inactive house mode; waiting for mode change.") + end + + -- Figure out how long to delay. If we're lights-out, delay to next sunset. Otherwise, short delay + -- to re-check house mode, which could change at any time, so we must deal with it. + if (inActiveTimePeriod) then + nextCycleDelay = getRandomDelay("MinCycleDelay", "MaxCycleDelay") + else + nextCycleDelay = sunset - os.time() + getRandomDelay("MinCycleDelay", "MaxCycleDelay") + end + elseif (not inActiveTimePeriod) then + luup.log("DeusExMachinaII::deusStep(): running off cycle") + luup.variable_set(SID, "State", STATE_SHUTDOWN, lul_device) + if (not turnOffLight()) then + -- No more lights to turn off + runFinalScene() + luup.variable_set(SID, "State", STATE_IDLE, lul_device) + nextCycleDelay = sunset - os.time() + getRandomDelay("MinCycleDelay", "MaxCycleDelay") + luup.log("DeusExMachina::deusStep(): all lights out; now IDLE, setting delay to restart cycling at next sunset") + else + nextCycleDelay = getRandomDelay("MinOffDelay", "MaxOffDelay", 60, 300) + end + else + -- Fully active. Find a random target to control and control it. + luup.log("DeusExMachinaII::deusStep(): running toggle cycle") + luup.variable_set(SID, "State", STATE_CYCLE, lul_device) + nextCycleDelay = getRandomDelay("MinCycleDelay", "MaxCycleDelay") + local devs, max + devs, max = getTargetList() + if (max > 0) then + local change = math.random(1, max) + local devspec = devs[change] + if (devspec ~= nil) then + local s = isDeviceOn(devspec) + if (s ~= nil) then + if (s) then + -- It's on; turn it off. + debug("deusStep(): turn %1 OFF", devspec) + targetControl(devspec, false) + else + -- Turn something on. If we're at the max number of targets we're allowed to turn on, + -- turn targets off first. + local maxOn = getVarNumeric("MaxTargetsOn") + if (maxOn == nil) then maxOn = 0 else maxOn = tonumber(maxOn,10) end + if (maxOn > 0) then + local on, n + on, n = getTargetsOn() + while ( n >= maxOn ) do + debug("deusStep(): too many targets on, max is %1, have %2, turning one off", maxOn, n) + _, on, n = turnOffLight(on) + end + end + debug("deusStep(): turn %1 ON", devspec) + targetControl(devspec, true) + end + end + end + else + luup.log("DeusExMachinaII:deusStep(): no targets to control") + end + end + + -- Arm for next cycle + if nextCycleDelay ~= nil then + luup.log("DeusExMachinaII:deusStep(): cycle finished, next in " .. nextCycleDelay .. " seconds") + if nextCycleDelay < 1 then nextCycleDelay = 60 end + luup.call_delay("deusStep", nextCycleDelay, stepStamp, 1) + else + luup.log("DeusExMachinaII:deusStep(): nil nextCycleDelay, next cycle not scheduled!") + end +end From d3e94f3b3aebf382c763c2961610ca18d27f735a Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Fri, 23 Dec 2016 19:13:29 -0500 Subject: [PATCH 23/49] Message field to say what we are doing --- D_DeusExMachinaII1_UI7.json | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/D_DeusExMachinaII1_UI7.json b/D_DeusExMachinaII1_UI7.json index 63784c5..a1081c6 100644 --- a/D_DeusExMachinaII1_UI7.json +++ b/D_DeusExMachinaII1_UI7.json @@ -103,7 +103,10 @@ "left": "0", "Label": { "lang_tag": "dem_about", - "text": "DeusExMachina II ver 2.4RC3 2016-12-21
    For documentation or to report bugs, please go to the DeusExMachina Github repository
    This plugin is offered for use as-is and without warranties of any kind. By using this plugin, you agree to assume all risks in connection with its use without limitation.." + "text": "DeusExMachina II ver 2.4RC4 2016-12-23
    + For documentation or to report bugs, please go to the DeusExMachina Github repository.
    + This plugin is offered for use as-is and without warranties of any kind. By using this plugin,
    + you agree to assume all risks in connection with its use without limitation." }, "Display": { "Top": "80", @@ -111,6 +114,18 @@ "Width": "200", "Height": "24" } + }, { + "ControlType": "variable", + "top": "0", + "left": "0", + "Display": { + "Service": "urn:toggledbits-com:serviceId:DeusExMachinaII1", + "Variable": "Message", + "Top": "16", + "Left": "120", + "Width": "200", + "Height": "20" + } }] }, { "Label": { From bbb566b74d3746df4fa2a7fe4d4c77968218d5ef Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Fri, 23 Dec 2016 19:13:59 -0500 Subject: [PATCH 24/49] Spring cleaning (in winter) --- README.md | 140 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 93 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 13750f2..263acc6 100644 --- a/README.md +++ b/README.md @@ -7,21 +7,17 @@ DeusExMachina is a plugin for the MiCasaVerde Vera home automation system. It ta There are currently two versions of Deus Ex Machina available: -* Deus Ex Machina -- version 1.1, for UI5; this is the legacy version and although it installs for UI6 and UI7, it does not work properly on those platforms. +* Deus Ex Machina -- for UI5 (only); this is the legacy version and although it installs for UI6 and UI7, it does not work properly on those platforms. This version is only available from the MiCasaVerde plugin library. -* Deus Ex Machina II -- version 2.4, for UI7 (only). This version was developed and tested on firmware version 1.7.855, but should work for any full release of UI7 provided by MiCasaVerde. +* Deus Ex Machina II -- for UI7 (only). This version was developed and tested on firmware version 1.7.855, but should work for any full release of UI7 provided by MiCasaVerde. The current release is available via both GitHub and the MiCasaVerde plugin library. Advanced builds are also available in the GitHub repository. ### History ### -DeusExMachina was originally written and published in 2012 by Andy Lintner (beowulfe), and maintained by Andy through the 1.x versions. In May 2016, Andy turned the project over to Patrick Rigney (toggledbits here on Github, rigpapa in the MCV/Mios world) for ongoing support (version 2.0 onward). - -For information about modifications, fixes and enhancements, please see the Changelog. +DeusExMachina was originally written and published in 2012 by Andy Lintner (beowulfe), and maintained by Andy through the 1.x versions. In May 2016, Andy turned the project over to Patrick Rigney (toggledbits here on Github, rigpapa in the MCV/MiOS world) for ongoing support (version 2.0 onward). At this point, the plugin became known as Deus Ex Machina II, or just DEMII. ### How It Works ### -When DeusExMachina is activated, it first waits until sunset. It then begins to toggle lights on the list of controlled devices (which is user-configured), with a random delay of between 5 and 30 minutes between each device. Then, when a user-configured "lights out" time is reached, the plug-in begins to turn randomly turn off any of the controlled lights that are on, until all are off. It then waits until the next day to resume its work at sunset, if it is still enabled. - -In the "lights out" mode, DeusExMachina prior to version 2.0 used the same 5-30 minute range of delays to turn off each light. If a large number of lights was configured, it could potentially take hours past the user-specified "lights out" time to turn them all off. As of version 2.0, the default range is between 1 and 5 minutes. This can be changed as further described below. +When Deus Ex Machina II (DEMII) is enabled, it first waits until two conditions are satisfied: the current time is at or after sunset, and the "house mode" is one of the selected modes in which DEMII is configured by the user to be active. If both conditions are met, DEMII enters a cycle of turning a set of user-selected lights on and off at random intervals. This continues until the user-configured "lights out" time, at which point DEMII begins its shutdown cycle, in which any of the user-selected lights that are on are turned off at random intervals until all lights are off. DEMII then waits until the next day's sunset. For more information, see Additional Documentation below. @@ -37,50 +33,66 @@ DeusExMachina is offered under GPL (the GNU Public License). #### Installation #### -The plugin is installed in the usual way: - -* On UI7, go to Apps in the left navigation, and click on "Install Apps". Search for "Deus Ex Machina II" (make sure you include the "II" at the end to get the UI7-compatible version), and then click the -"Details" button in its listing of the search results. From here, simply click "Install" and wait for the install to complete. A full refresh of the UI is necessary (e.g. Ctrl-F5 on Windows) after installation. - -* On UI5, click on APPS in the top navigation bar, then click "Install Apps". Search for "Deus Ex Machina". The search results will show "Deus Ex Machina" and "Deus Ex Machina II". At this time, it is recommended -that you install the legacy version only on UI5 (unless you want to help me test the new version), which is "Deus Ex Machina". Click on the "Details" button, and then click "Install". Once the install completes, -one or more full refreshes of the browser (e.g. Ctrl-F5 on Windows) will be necessary. +The plugin is installed in the usual way: go to Apps in the left navigation, and click on "Install Apps". +Search for "Deus Ex Machina II" (make sure you include the "II" at the end to get the UI7-compatible version), +and then click the "Details" button in its listing of the search results. From here, simply click "Install" +and wait for the install to complete. A full refresh of the UI is necessary (e.g. Ctrl-F5 on Windows) after installation. Once you have installed the plugin and refreshed the browser, you can proceed to device configuation. #### Simple Configuration #### -Deus Ex Machina's "Configure" tab allows you to set up the set of lights that should be controlled, and the time at which DEM should begin shutting lights off to simulate the house occupants going to sleep. +Deus Ex Machina's "Configure" tab gives you a set of simple controls to control the behavior of your vacation haunt. + +##### Lights-Out Time ##### + +The "Lights Out" time is a time, expressed in 24-hour HH:MM format, that is the time at which lights should begin +shutting off. This time should be after sunset. Keep in mind that sunset is a moving target, and +at certain times of year in some places can be quite late, so a Lights Out time of 20:15, for example, may not be +a good choice for the longest days of summer. The lights out time can be a time after midnight. + +##### House Modes ##### -The "Lights Out" time is a time, expressed in 24-hour HH:MM format, that is the time at which lights should begin shutting off. This time should be after sunset. Keep in mind that sunset is a moving target, and -at certain times of year in some places can be quite late, so a Lights Out time of 20:15, for example, may be too early. The lights out time can be a time after midnight. +The next group of controls is the House Modes in which DEMII should be active when enabled. If no house mode is selected, +DEMII will operate in _any_ house mode. -UI7 introduced the concept of "House Modes." Version 2.3 and beyond of Deus Ex Machina have the ability to run only when the -house is in one or more selected house modes. A set of checkboxes is used to selected which modes allow Deus Ex Machina to run. -If no modes are chosen, it is the same as choosing all modes (Deus operates in any house mode). +##### Controlled Devices ##### +Next is a set of checkboxes for each of the devices you'd like DEMII to control. Selecting the devices to be controlled is a simple matter of clicking the check boxes. Because the operating cycle of the plug-in is random, any controlled device may be turned on and off several times during the cycling period (between sunset and Lights Out time). -As of version 2.4, lights on dimmers can be set to any level by setting the slider that appears to the right of the device name. Non-dimming devices are simply turned on and off. +Dimming devices can be set to any level by setting the slider that appears to the right of the device name. +Non-dimming devices are simply turned on and off (no dimmer slider is shown for these devices). -As of version 2.4, all devices are listed that implement the SwitchPower1 and Dimming1 services. This leads to some oddities, -like some motion sensors and thermostats being listed. It may not be entirely obvious (or standard) what a thermostat, for example, might do when you try to turn it off and on like a light, so be careful selecting these devices. +> Note: all devices are listed that implement the SwitchPower1 and Dimming1 services. This leads to some oddities, +> like some motion sensors and thermostats being listed. It may not be entirely obvious (or standard) what a thermostat, for example, +> might do when you try to turn it off and on like a light, so be careful selecting these devices. -Also new for version 2.4 is the ability to run scenes during the random cycling period. Scenes must be specified in pairs, with +##### Scene Control ##### + +The next group of settings allows you to use scenes with DEMII. +Scenes must be specified in pairs, with one being the "on" scene and the other being an "off" scene. This not only allows more patterned use of lights, but also gives the user -the ability to handle device-specific capabilities that would be difficult to implement in DEMII. For example, while DEMII can now +the ability to handle device-specific capabilities that would be difficult to implement in DEMII. For example, while DEMII can turn Philips Hue lights on and off (to dimming levels, even), it cannot control their color because there's no UI for that in -DEMII. But a scene could be used to control that light or a group of lights, with their color. +DEMII. But a scene could be used to control that light or a group of lights, with their color, as an alternative to direct control by DEMII. + +Both scenes and individual devices (from the device list above) can be used simultaneously. -Version 2.4 also adds the ability to limit the number of targets (devices or scenes) that DEMII can have "on" simultaneously. If this limit is 0, there is no limit enforced. +##### Maximum "On" Targets ##### -Finally, 2.4 adds the ability for a "final scene" to run when DEMII is disabled or turns off the last light after the "lights out" time. This could be used for any purpose. I personally use it to make sure a whole-house off is run, but you could use it to ensure your alarm system is armed, or your garage door is closed, etc. +Version 2.4 also adds the ability to limit the number of targets (devices or scenes) that DEMII can have "on" simultaneously. +If this limit is 0, there is no limit enforced. If you have DEMII control a large number of devices, it's probably not a bad idea to +set this value to some reasonable limit. -#### Control by Scene #### +##### Final Scene ##### -As of version 2.0 and on UI7, DeusExMachina can be enabled or disabled like a light switch in scenes, through the regular graphical interface (no Lua required). +DEMII allows a "final scene" to run when DEMII is disabled or turns off the last light after the "lights out" time. This could be used for any purpose. I personally use it to make sure a whole-house off is run, but you could use it to ensure your alarm system is armed, or your garage door is closed, etc. -A Lua interface is also supported since version 1.1 for both UI5 and UI7. All versions of support the SetEnabled action, although for DEMII versions 2.4 and higher, the use of SetTarget in the standard SwitchPower1 service is preferred. Examples (the "0|1" means use either 0 or 1 to disable or enable, respectively): +#### Control of DEMII by Scenes #### + +DeusExMachina can be enabled or disabled like a light switch in scenes, through the regular graphical interface (no Lua required). +A Lua interface is also supported. All versions of support the SetEnabled action, although for DEMII versions 2.4 and higher, the use of SetTarget in the standard SwitchPower1 service is preferred. Examples (the "0|1" means use either 0 or 1 to disable or enable, respectively): ``` -- Preferred for DEMII versions 2.4 and higher: @@ -93,18 +105,9 @@ luup.call_action("urn:toggledbits-com:serviceId:DeusExMachinaII1", "SetEnabled", luup.call_action("urn:futzle-com:serviceId:DeusExMachina1", "SetEnabled", { NewEnabledValue = "0|1" }, deviceID) ``` -Note that when disabling Deus Ex Machina from a scene or the user interface, versions 1.1 and 2.x (DEMII) operate differently. Version 1.1 will simply stop cycling lights, leaving on any controlled lights it may have turned on. -DEMII, however, will turn off all controlled lights _if it was in the cycling period (between sunset and lights out time) at the time it was disabled_. - -DEMII version 2.0 also added the ability for a change of DeusExMachina's operating state to be used as trigger in scenes and other places where events can be watched (e.g. Program Logic plugins, etc.). This also works on UI7 only. - #### Triggers #### -Version 2.0 on UI7 supports events (e.g. to trigger a scene or use with Program Logic Event Generator) for its state changes. - -If the "device is enabled or disabled" event is chosen, the trigger will fire when DEMII's state is changed to enabled or disabled (you will be given the option to choose which). - -For the "operating mode changes" event, the trigger fires when DEMII's operating mode changes. DEMII's operating modes are: +DEMII signals changes to its enabled/disabled state, and changes to its internal operating mode. These can be used as triggers for scenes or notifications. DEMII's operating modes are: * Standby - DEMII is disabled (this is equivalent to the "device is disabled" state event); @@ -123,7 +126,50 @@ respectively. DEMII will also transition into or out of Standby mode immediately #### Cycle Timing #### -Version 2.0 has added device state variables to alter the default cycle timing. They can be changed by accessing them through the "Advanced" tab in the device user interface. -The random delay between turning lights on or off is between 5 and 30 minutes by default. By setting `MinCycleTime` and `MaxCycleTime` (integer number of seconds, -default 300 and 1800, respectively), the user can modify the default settings for on/off cycling. Similarly the `MinOffTime` and `MaxOffTime` variables (default 60 and 300 seconds, -respectively) change the rate of the "lights out" mode (i.e. the transition to all lights out at the user-configured time). +DEMII's cycle timing is controlled by a set of state variables. By default, DEMII's random cycling of lights occurs at randomly selected intervals between 300 seconds (5 minutes) and 1800 seconds (30 minutes), as determined by the `MinCycleDelay` and `MaxCycleDelay` variables. You may change these values to customize the cycling time for your application. + +When DEMII is in its "lights out" (shut-off) mode, it uses a different set of shorter (by default) cycle times, to more closely imitate actual human behavior. The random interval for lights-out is between 60 seconds and 300 seconds (5 minutes), as determined by `MinOffDelay` and `MaxOffDelay`. These intervals could be kept short, particularly if DEMII is controlling a large number of lights. + +#### Troubleshooting #### + +If DEMII isn't behaving as expected, post a message in the MCV forums +[in this thread](http://forum.micasaverde.com/index.php/topic,11333.0.html) +or open up an issue in the +[GitHub repository](https://github.com/toggledbits/DeusExMachina/issues). + +Please don't just say "DEMII isn't working for me." I can't tell you how long a piece of string is without seeing the piece of string. Give me details of what you are doing, how you are configured, and what behavior you observe. + +##### Test Mode and Log Output ##### + +If I'm troubleshooting a problem with you, I may ask you to enable test mode, run DEMII a bit, and send me the log output. Here's how you do that: + +1. Go into the setting for the DEMII device, and click the "Advanced" tab. +1. Click on the Variables tab. +1. Set the TestMode variable to 1 (just change the field and hit the TAB key) +1. If requested, set the TestSunset value to whatever I ask you (this allows the sunset time to be overriden so we don't have to wait for real sunset to see what DEMII is doing). + +### FAQ ### + +
    +
    What happens if DEMII is enabled afer sunset? Does it wait until the next day to start running?
    +
    No. If DEMII is enabled during its active period (between sunset and the configured "lights out" time, + it will begin cycling the configured devices and scenes.
    + +
    What's the difference between House Mode and Enabled/Disabled? Can I just use House Mode to enable and disable DEMII?
    +
    The enabled/disabled state of DEMII is the "big red button" for its operation. If you configure DEMII to only run in certain + house modes, then you can theoretically leave DEMII enabled all the time, as it will operate (cycle lights) only when a + selected house mode is active. But, some people don't use the House Modes for various reasons, so having a master switch + for DEMII is necessary.
    + +
    I have a feature request. Will you implement it?
    +
    Absolutely definitely maybe. I'm willing to listen to what you want to do. But, keep in mind, nobody's getting rich writing Vera + plugins, and I do other things that put food on my table. And, what seems like a good idea to you may be just that: a good idea for + the way _you_ want to use it. The more generally applicable your request is, the higher the likelihood that I'll entertain it. What + I don't want to do is over-complicate this plug-in so it begins to rival PLEG for size and weight (no disrespect intended there at + all--I'm a huge PLEG fan and use it extensively, but, dang). DEMII really has a simple job: make lights go on and off to cast a serious + shadow of doubt in the mind of some knucklehead who might be thinking your house is empty and ripe for his picking. In any case, + the best way to give me feature requests is to open up an issue (if you have a list, one issue per feature, please) in the + [GitHub repository](https://github.com/toggledbits/DeusExMachina/issues). Second best is sending me a message via the MCV forums + (I'm user `rigpapa`). +
    +
    From 0500b3d57b7b4ebc27782b90e374c32e03d668f1 Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Fri, 23 Dec 2016 19:14:16 -0500 Subject: [PATCH 25/49] Message field to say what we are doing --- L_DeusExMachinaII1.lua | 65 ++++++++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 19 deletions(-) diff --git a/L_DeusExMachinaII1.lua b/L_DeusExMachinaII1.lua index 9048e64..3e54f97 100644 --- a/L_DeusExMachinaII1.lua +++ b/L_DeusExMachinaII1.lua @@ -15,7 +15,7 @@ local STATE_SHUTDOWN = 3 local runStamp = 0 -local debugMode = true +local debugMode = false local function debug(...) if debugMode then @@ -68,9 +68,23 @@ local function deleteVar( name, devid ) -- could have been used to delete variables, since a later get would yield nil anyway. But it turns out -- that using the variableget Luup request with no value WILL delete the variable. local req = "http://127.0.0.1:3480/data_request?id=variableset&DeviceNum=" .. tostring(devid) .. "&serviceId=" .. SID .. "&Variable=" .. name .. "&Value=" - -- debug("DeusExMachinaII::deleteVar(" .. name .. "): req=" .. tostring(req)) + debug("deleteVar(%1,%2) wget %3", name, devid, req) local status, result = luup.inet.wget(req) - -- debug("DeusExMachinaII::deleteVar(" .. name .. "): status=" .. tostring(status) .. ", result=" .. tostring(result)) + debug("deleteVar(%1,%2) status=%3, result=%4", name, devid, status, result) +end + +local function pad(n) + if (n < 10) then return "0" .. n end + return n; +end + +local function timeToString(t) + if t == nil then t = os.time() end + return pad(t['hour']) .. ':' .. pad(t['min']) .. ':' .. pad(t['sec']) +end + +local function setMessage(s) + luup.variable_set(SID, "Message", s or "", lul_device) end -- Shortcut function to return state of SwitchPower1 Status variable @@ -94,10 +108,10 @@ local function isActiveHouseMode() -- Get the current house mode (1=Home,2=Away,3=Night,4=Vacation) currentMode = math.pow(2, tonumber(currentMode,10)) if (bit.band(modebits, currentMode) == 0) then - debug('DeusExMachinaII::isActiveHouseMode(): Current mode bit %1 not set in %2', string.format("0x%x", currentMode), string.format("0x%x", modebits)) + debug('DeusExMachinaII:isActiveHouseMode(): Current mode bit %1 not set in %2', string.format("0x%x", currentMode), string.format("0x%x", modebits)) return false -- not active in this mode else - debug('DeusExMachinaII::isActiveHouseMode(): Current mode bit %1 SET in %2', string.format("0x%x", currentMode), string.format("0x%x", modebits)) + debug('DeusExMachinaII:isActiveHouseMode(): Current mode bit %1 SET in %2', string.format("0x%x", currentMode), string.format("0x%x", modebits)) end end return true -- default is we're active in the current house mode @@ -111,7 +125,7 @@ local function getRandomDelay(minStateName,maxStateName,defMin,defMax) if mind < 1 then mind = 1 elseif mind > 7200 then mind = 7200 end local maxd = getVarNumeric(maxStateName, defMax) if maxd < 1 then maxd = 1 elseif maxd > 7200 then maxd = 7200 end - if maxd < mind then maxd = mind end + if maxd <= mind then return mind end return math.random( mind, maxd ) end @@ -131,8 +145,8 @@ local function getSunset() t['min'] = math.floor(m % 60) t['sec'] = 0 sunset = os.time(t) + debug('getSunset(): testing mode sunset override %1, as timeval is %2', m, sunset) end - debug('getSunset(): testing mode sunset override %1, as timeval is %2', m, sunset) end if (sunset <= os.time()) then sunset = sunset + 86400 end return sunset @@ -148,7 +162,10 @@ end -- to minutes-since-midnight units. local function isBedtime() local testing = getVarNumeric("TestMode", 0) - if (testing ~= 0) then luup.log('DeusExMachinaII::isBedtime(): TestMode is on') end + if (testing ~= 0) then + luup.log('DeusExMachinaII:isBedtime(): TestMode is on') + debugMode = true + end -- Establish the lights-out time local bedtime = 1439 -- that's 23:59 in minutes since midnight (default) @@ -179,7 +196,7 @@ local function isBedtime() ret = 0 end end - debug('isBedtime(): returning %1", ret) + debug("isBedtime(): returning %1", ret) return ret end @@ -379,7 +396,7 @@ local function turnOffLight(on) targetControl(target, false) table.remove(on, i) n = n - 1 - debug(":turnOffLight(): turned %1 OFF, still %2 targets on", target, n) + debug("turnOffLight(): turned %1 OFF, still %2 targets on", target, n) end return (n > 0), on, n end @@ -488,6 +505,7 @@ end -- Enable DEM by setting a new cycle stamp and scheduling our first cycle step. function deusEnable() + setMessage("Enabling...") luup.log("DeusExMachinaII:deusEnable(): enabling...") luup.variable_set(SID, "ScenesRunning", "", lul_device) -- start with a clean slate runStamp = os.time() @@ -499,6 +517,7 @@ end -- Disable DEM and go to standby state. If we are currently cycling (as opposed to idle/waiting for sunset), -- turn off any controlled lights that are on. function deusDisable() + setMessage("Disabling...") local s = getVarNumeric("State", STATE_STANDBY) luup.log("DeusExMachinaII:deusDisable(): disabling...") if ( s == STATE_CYCLE or s == STATE_SHUTDOWN ) then @@ -507,11 +526,13 @@ function deusDisable() luup.variable_set(SID, "State", STATE_STANDBY, lul_device) luup.variable_set(SID, "Enabled", "0", lul_device) luup.variable_set(SWITCH_SID, "Status", "0", lul_device) + setMessage("") end -- Initialize. function deusInit(deusDevice) - luup.log("DeusExMachinaII::deusInit(): Version 2.4RC3 (2016-12-21), initializing...") + setMessage("Initializing...") + luup.log("DeusExMachinaII:deusInit(): Version 2.4RC4 (2016-12-23), initializing...") -- One-time stuff runOnce() @@ -549,7 +570,7 @@ function deusStep(stepStampCheck) local currentState = getVarNumeric("State", 0) if (currentState == STATE_STANDBY or currentState == STATE_IDLE) then - debug("deusStep(): run in state %1, lightsout=%2, sunset=%3, os.time=%4", currentState, + debug("deusStep(): step in state %1, lightsout=%2, sunset=%3, os.time=%4", currentState, luup.variable_get(SID, "LightsOut", lul_device), sunset, os.time()) debug("deusStep(): luup variables longitude=%1, latitude=%2, timezone=%3, city=%4, sunset=%5, version=%6", luup.longitude, luup.latitude, luup.timezone, luup.city, luup.sunset(), luup.version) @@ -557,7 +578,7 @@ function deusStep(stepStampCheck) local inActiveTimePeriod = true if (isBedtime() ~= 0) then - luup.log("DeusExMachinaII:deusStep(): in lights out time") + debug("deusStep(): in lights out time") inActiveTimePeriod = false end @@ -566,17 +587,18 @@ function deusStep(stepStampCheck) if (currentState == STATE_STANDBY and not inActiveTimePeriod) then -- Transition from STATE_STANDBY (i.e. we're enabling) in the inactive period. -- Go to IDLE and delay for next sunset. - luup.log("DeusExMachinaII::deusStep(): transitioning to IDLE from STANDBY, waiting for next sunset...") + luup.log("DeusExMachinaII:deusStep(): transitioning to IDLE from STANDBY, waiting for next sunset...") nextCycleDelay = sunset - os.time() + getRandomDelay("MinCycleDelay", "MaxCycleDelay") luup.variable_set(SID, "State", STATE_IDLE, lul_device) + setMessage("Waiting for sunset " .. timeToString(os.date("*t", os.time() + nextCycleDelay))) elseif (not isActiveHouseMode()) then -- Not in an active house mode. If we're not STANDBY or IDLE, turn everything back off and go to IDLE. if (currentState ~= STATE_IDLE) then - luup.log("DeusExMachinaII::deusStep(): transitioning to IDLE, not in an active house mode.") + luup.log("DeusExMachinaII:deusStep(): transitioning to IDLE, not in an active house mode.") if (currentState ~= STATE_STANDBY) then clearLights() end -- turn off lights quickly unless transitioning from STANDBY luup.variable_set(SID, "State", STATE_IDLE, lul_device) else - luup.log("DeusExMachinaII::deusStep(): IDLE in an inactive house mode; waiting for mode change.") + luup.log("DeusExMachinaII:deusStep(): IDLE in an inactive house mode; waiting for mode change.") end -- Figure out how long to delay. If we're lights-out, delay to next sunset. Otherwise, short delay @@ -586,21 +608,24 @@ function deusStep(stepStampCheck) else nextCycleDelay = sunset - os.time() + getRandomDelay("MinCycleDelay", "MaxCycleDelay") end + setMessage("Waiting for active house mode") elseif (not inActiveTimePeriod) then - luup.log("DeusExMachinaII::deusStep(): running off cycle") + luup.log("DeusExMachinaII:deusStep(): running off cycle") luup.variable_set(SID, "State", STATE_SHUTDOWN, lul_device) if (not turnOffLight()) then -- No more lights to turn off runFinalScene() luup.variable_set(SID, "State", STATE_IDLE, lul_device) nextCycleDelay = sunset - os.time() + getRandomDelay("MinCycleDelay", "MaxCycleDelay") - luup.log("DeusExMachina::deusStep(): all lights out; now IDLE, setting delay to restart cycling at next sunset") + luup.log("DeusExMachina:deusStep(): all lights out; now IDLE, setting delay to restart cycling at next sunset") + setMessage("Waiting for sunset " .. timeToString(os.date("*t", os.time() + nextCycleDelay))) else nextCycleDelay = getRandomDelay("MinOffDelay", "MaxOffDelay", 60, 300) + setMessage("Shut-off cycle, next " .. timeToString(os.date("*t", os.time() + nextCycleDelay))) end else -- Fully active. Find a random target to control and control it. - luup.log("DeusExMachinaII::deusStep(): running toggle cycle") + luup.log("DeusExMachinaII:deusStep(): running toggle cycle") luup.variable_set(SID, "State", STATE_CYCLE, lul_device) nextCycleDelay = getRandomDelay("MinCycleDelay", "MaxCycleDelay") local devs, max @@ -633,7 +658,9 @@ function deusStep(stepStampCheck) end end end + setMessage("Cycling; next " .. timeToString(os.date("*t", os.time() + nextCycleDelay))) else + setMessage("Nothing to do") luup.log("DeusExMachinaII:deusStep(): no targets to control") end end From 2a304b113f22a4c064b54298a7a733506923c84f Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Tue, 27 Dec 2016 09:40:00 -0500 Subject: [PATCH 26/49] More FAQ and troubleshooting commentary --- README.md | 99 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 65 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 263acc6..e81ce18 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -DeusExMachina: The Vacation Plugin +DeusExMachinaII: The Vacation Plugin ============= -### Introduction ### +## Introduction ## DeusExMachina is a plugin for the MiCasaVerde Vera home automation system. It takes over your house while you're away on vacation by creating a ghost that moves from room to room, turning on and off lights. Simply specify the lights you want to have controlled by the plugin, specify a "Lights Out" time when lights will begin to turn off, and come sundown DeusExMachina will take over. @@ -11,27 +11,27 @@ There are currently two versions of Deus Ex Machina available: * Deus Ex Machina II -- for UI7 (only). This version was developed and tested on firmware version 1.7.855, but should work for any full release of UI7 provided by MiCasaVerde. The current release is available via both GitHub and the MiCasaVerde plugin library. Advanced builds are also available in the GitHub repository. -### History ### +## History ## DeusExMachina was originally written and published in 2012 by Andy Lintner (beowulfe), and maintained by Andy through the 1.x versions. In May 2016, Andy turned the project over to Patrick Rigney (toggledbits here on Github, rigpapa in the MCV/MiOS world) for ongoing support (version 2.0 onward). At this point, the plugin became known as Deus Ex Machina II, or just DEMII. -### How It Works ### +## How It Works ## When Deus Ex Machina II (DEMII) is enabled, it first waits until two conditions are satisfied: the current time is at or after sunset, and the "house mode" is one of the selected modes in which DEMII is configured by the user to be active. If both conditions are met, DEMII enters a cycle of turning a set of user-selected lights on and off at random intervals. This continues until the user-configured "lights out" time, at which point DEMII begins its shutdown cycle, in which any of the user-selected lights that are on are turned off at random intervals until all lights are off. DEMII then waits until the next day's sunset. For more information, see Additional Documentation below. -### Reporting Bugs/Enhancement Requests ### +## Reporting Bugs/Enhancement Requests ## Bug reports and enhancement requests are welcome! Please use the "Issues" link for the repository to open a new bug report or make an enhancement request. -### License ### +## License ## DeusExMachina is offered under GPL (the GNU Public License). -### Additional Documentation ### +## Additional Documentation ## -#### Installation #### +### Installation ### The plugin is installed in the usual way: go to Apps in the left navigation, and click on "Install Apps". Search for "Deus Ex Machina II" (make sure you include the "II" at the end to get the UI7-compatible version), @@ -40,23 +40,23 @@ and wait for the install to complete. A full refresh of the UI is necessary (e.g Once you have installed the plugin and refreshed the browser, you can proceed to device configuation. -#### Simple Configuration #### +### Simple Configuration ### Deus Ex Machina's "Configure" tab gives you a set of simple controls to control the behavior of your vacation haunt. -##### Lights-Out Time ##### +#### Lights-Out Time #### The "Lights Out" time is a time, expressed in 24-hour HH:MM format, that is the time at which lights should begin shutting off. This time should be after sunset. Keep in mind that sunset is a moving target, and at certain times of year in some places can be quite late, so a Lights Out time of 20:15, for example, may not be a good choice for the longest days of summer. The lights out time can be a time after midnight. -##### House Modes ##### +#### House Modes #### The next group of controls is the House Modes in which DEMII should be active when enabled. If no house mode is selected, DEMII will operate in _any_ house mode. -##### Controlled Devices ##### +#### Controlled Devices #### Next is a set of checkboxes for each of the devices you'd like DEMII to control. Selecting the devices to be controlled is a simple matter of clicking the check boxes. Because the operating cycle of @@ -68,7 +68,7 @@ Non-dimming devices are simply turned on and off (no dimmer slider is shown for > like some motion sensors and thermostats being listed. It may not be entirely obvious (or standard) what a thermostat, for example, > might do when you try to turn it off and on like a light, so be careful selecting these devices. -##### Scene Control ##### +#### Scene Control #### The next group of settings allows you to use scenes with DEMII. Scenes must be specified in pairs, with @@ -79,35 +79,32 @@ DEMII. But a scene could be used to control that light or a group of lights, wit Both scenes and individual devices (from the device list above) can be used simultaneously. -##### Maximum "On" Targets ##### +#### Maximum "On" Targets #### Version 2.4 also adds the ability to limit the number of targets (devices or scenes) that DEMII can have "on" simultaneously. If this limit is 0, there is no limit enforced. If you have DEMII control a large number of devices, it's probably not a bad idea to set this value to some reasonable limit. -##### Final Scene ##### +#### Final Scene #### DEMII allows a "final scene" to run when DEMII is disabled or turns off the last light after the "lights out" time. This could be used for any purpose. I personally use it to make sure a whole-house off is run, but you could use it to ensure your alarm system is armed, or your garage door is closed, etc. -#### Control of DEMII by Scenes #### +### Control of DEMII by Scenes and Lua ### -DeusExMachina can be enabled or disabled like a light switch in scenes, through the regular graphical interface (no Lua required). -A Lua interface is also supported. All versions of support the SetEnabled action, although for DEMII versions 2.4 and higher, the use of SetTarget in the standard SwitchPower1 service is preferred. Examples (the "0|1" means use either 0 or 1 to disable or enable, respectively): +DeusExMachina can be enabled or disabled like a light switch in scenes or through the regular graphical interface (no Lua required), +or by scripting in Lua. +DEMII implements the SwitchPower1 service, so enabling and disabling is the same as turning a light switch on and off: +you simply use the SetTarget action to enable (newTargetValue=1) or disable (newTargetValue=0) DEMII. +The MiOS GUI for devices and scenes takes care of this for you in its code; if scripting in Lua, you simply do this: ``` --- Preferred for DEMII versions 2.4 and higher: -luup.call_action("urn:upnp-org-serviceId:SwitchPower1", "SetTarget", { newTargetValue = "0|1" }, deviceID) - --- Also works for all versions of DEMII: -luup.call_action("urn:toggledbits-com:serviceId:DeusExMachinaII1", "SetEnabled", { NewEnabledValue = "0|1" }, deviceID) - --- For the old Deus Ex Machina plugin (v1.1 and earlier) running on UI5 or UI7, do this: -luup.call_action("urn:futzle-com:serviceId:DeusExMachina1", "SetEnabled", { NewEnabledValue = "0|1" }, deviceID) +luup.call_action("urn:upnp-org-serviceId:SwitchPower1", "SetTarget", { newTargetValue = "0|1" }, pluginDeviceId) ``` -#### Triggers #### +### Triggers ### -DEMII signals changes to its enabled/disabled state, and changes to its internal operating mode. These can be used as triggers for scenes or notifications. DEMII's operating modes are: +DEMII signals changes to its enabled/disabled state and changes to its internal operating mode. +These can be used as triggers for scenes or notifications. DEMII's operating modes are: * Standby - DEMII is disabled (this is equivalent to the "device is disabled" state event); @@ -124,22 +121,28 @@ is disabled (at which point it goes to Standby). It should be noted that DEMII can enter Cycling or Shut-off mode immediately, without passing through Ready, if it is enabled after sunset or after the "lights out" time, respectively. DEMII will also transition into or out of Standby mode immediately and from any other mode when disabled or enabled, respectively. -#### Cycle Timing #### +### Cycle Timing ### DEMII's cycle timing is controlled by a set of state variables. By default, DEMII's random cycling of lights occurs at randomly selected intervals between 300 seconds (5 minutes) and 1800 seconds (30 minutes), as determined by the `MinCycleDelay` and `MaxCycleDelay` variables. You may change these values to customize the cycling time for your application. When DEMII is in its "lights out" (shut-off) mode, it uses a different set of shorter (by default) cycle times, to more closely imitate actual human behavior. The random interval for lights-out is between 60 seconds and 300 seconds (5 minutes), as determined by `MinOffDelay` and `MaxOffDelay`. These intervals could be kept short, particularly if DEMII is controlling a large number of lights. -#### Troubleshooting #### +### Troubleshooting ### + +If you're not sure what DEMII is going, the easiest way to see is to go into the Settings interface for the plugin. +There is a text field to the right of the on/off switch in that interface that will tell you what DEMII is currently +doing (when enabled; it's blank when DEMII is disabled). If DEMII isn't behaving as expected, post a message in the MCV forums [in this thread](http://forum.micasaverde.com/index.php/topic,11333.0.html) or open up an issue in the [GitHub repository](https://github.com/toggledbits/DeusExMachina/issues). -Please don't just say "DEMII isn't working for me." I can't tell you how long a piece of string is without seeing the piece of string. Give me details of what you are doing, how you are configured, and what behavior you observe. +Please don't just say "DEMII isn't working for me." I can't tell you how long your piece of string is without seeing +your piece of string. Give me details of what you are doing, how you are configured, and what behavior you observe. +Screen shots help. In many cases, log output may be needed. -##### Test Mode and Log Output ##### +#### Test Mode and Log Output #### If I'm troubleshooting a problem with you, I may ask you to enable test mode, run DEMII a bit, and send me the log output. Here's how you do that: @@ -147,13 +150,41 @@ If I'm troubleshooting a problem with you, I may ask you to enable test mode, ru 1. Click on the Variables tab. 1. Set the TestMode variable to 1 (just change the field and hit the TAB key) 1. If requested, set the TestSunset value to whatever I ask you (this allows the sunset time to be overriden so we don't have to wait for real sunset to see what DEMII is doing). +1. After operating for a while, I'll ask you to email me the log file (`/etc/cmh/LuaPNP.log` on your Vera). This will require you +to log in to your Vera directly with ssh, or use the Vera's native "write log to USB drive" function, or use one of the many +log capture scripts that's available. -### FAQ ### +Above all, I ask that you please be patient. You probably already know that it an be challenging at times to figure out +what's going on in your Vera's head. It's no different for developers. + +## FAQ ##
    +
    My lights aren't cycling at sunset. Why?
    +
    The most common reasons that lights don't start cycling at midnight are:
      +
    1. The time and location on your Vera are not set correctly. Go into Settings > Location on your + Vera and make sure everything is correct for the Vera's physical location. Remember that in + the western hemisphere (North, Central & South America, principally) your longitude will + be a negative number. If you are below the equator, latitude will be negative. If you're not + sure what your latitude/longitude are, use a site like [MyGeoPosition.com](http://mygeoposition.com). + If you make any changes to your time or location configuration, restart your Vera.
    2. +
    3. You're not waiting long enough. DEMII doesn't instantly jump into action at sunset, it employs its + configured cycle delays as well, so cycling will usually begin sometime after sunset, up to the + configured maximum cycle delay (30 minutes by default).
    4. +
    5. Your house mode isn't "active." If you've configured DEMII to operate only in certain house modes, + make sure you're in one of those modes, otherwise DEMII will sit, even though it's enabled.
    6. +
    +
    + +
    I made configuration changes, but when I go back into configuration, they seem to be back to the old + settings.
    +
    Refresh your browser or flush your browser cache. On most browsers, you do this by using the F5 key, or + Ctrl-F5, or Command + R or Option + R on Macs.
    +
    What happens if DEMII is enabled afer sunset? Does it wait until the next day to start running?
    No. If DEMII is enabled during its active period (between sunset and the configured "lights out" time, - it will begin cycling the configured devices and scenes.
    + it will begin cycling the configured devices and scenes. If you enable DEMII after "lights-out," it will + wait until the next sunset.
    What's the difference between House Mode and Enabled/Disabled? Can I just use House Mode to enable and disable DEMII?
    The enabled/disabled state of DEMII is the "big red button" for its operation. If you configure DEMII to only run in certain From 0e5589de765b5f1cf58dc2a4d8dc7c78a1b38ba6 Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Tue, 27 Dec 2016 09:48:35 -0500 Subject: [PATCH 27/49] Smooth out language --- README.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e81ce18..ffc6e77 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ When DEMII is in its "lights out" (shut-off) mode, it uses a different set of sh If you're not sure what DEMII is going, the easiest way to see is to go into the Settings interface for the plugin. There is a text field to the right of the on/off switch in that interface that will tell you what DEMII is currently -doing (when enabled; it's blank when DEMII is disabled). +doing when enabled (it's blank when DEMII is disabled). If DEMII isn't behaving as expected, post a message in the MCV forums [in this thread](http://forum.micasaverde.com/index.php/topic,11333.0.html) @@ -139,23 +139,25 @@ or open up an issue in the [GitHub repository](https://github.com/toggledbits/DeusExMachina/issues). Please don't just say "DEMII isn't working for me." I can't tell you how long your piece of string is without seeing -your piece of string. Give me details of what you are doing, how you are configured, and what behavior you observe. +_your_ piece of string. Give me details of what you are doing, how you are configured, and what behavior you observe. Screen shots help. In many cases, log output may be needed. #### Test Mode and Log Output #### If I'm troubleshooting a problem with you, I may ask you to enable test mode, run DEMII a bit, and send me the log output. Here's how you do that: -1. Go into the setting for the DEMII device, and click the "Advanced" tab. -1. Click on the Variables tab. -1. Set the TestMode variable to 1 (just change the field and hit the TAB key) +1. Go into the settings for the DEMII device, and click the "Advanced" tab. +1. Click on the "Variables" tab. +1. Set the "TestMode" variable to 1 (just change the field and hit the TAB key). If the variable doesn't exist, you'll need to create it using the "New Service" tab, which requires you to enter the service ID _exactly_ as shown here (use copy/paste if possible): `urn:toggledbits-com:serviceId:DeusExMachinaII1` 1. If requested, set the TestSunset value to whatever I ask you (this allows the sunset time to be overriden so we don't have to wait for real sunset to see what DEMII is doing). -1. After operating for a while, I'll ask you to email me the log file (`/etc/cmh/LuaPNP.log` on your Vera). This will require you +1. After operating for a while, I'll ask you to email me your log file (`/etc/cmh/LuaUPnP.log` on your Vera). This will require you to log in to your Vera directly with ssh, or use the Vera's native "write log to USB drive" function, or use one of the many log capture scripts that's available. +1. Don't forget to turn TestMode off (0) when finished. -Above all, I ask that you please be patient. You probably already know that it an be challenging at times to figure out -what's going on in your Vera's head. It's no different for developers. +Above all, I ask that you please be patient. You probably already know that it can be frustrating at times to figure out +what's going on in your Vera's head. It's no different for developers--it can be a challenging development environment +when the Vera is sitting in front of you, and moreso when dealing with someone else's Vera at a distance. ## FAQ ## From 57adb4c3c90db62aca15f89b39d5f3e1291bb6f3 Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Tue, 27 Dec 2016 09:57:00 -0500 Subject: [PATCH 28/49] Smooth out language --- README.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index ffc6e77..449059f 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,10 @@ DeusExMachinaII: The Vacation Plugin ## Introduction ## -DeusExMachina is a plugin for the MiCasaVerde Vera home automation system. It takes over your house while you're away on vacation by creating a ghost that moves from room to room, turning on and off lights. Simply specify the lights you want to have controlled by the plugin, specify a "Lights Out" time when lights will begin to turn off, and come sundown DeusExMachina will take over. +DeusExMachina is a plugin for the MiOS home automation operating system used on MiCasaVerde Vera gateway/controllers. +It takes over your house while you're away on vacation by creating a ghost that moves from room to room, turning on and off lights. +Simply specify the lights you want to have controlled by the plugin, specify a "Lights Out" time when lights will begin to +turn off, and come sundown DeusExMachina will take over. There are currently two versions of Deus Ex Machina available: @@ -168,13 +171,13 @@ when the Vera is sitting in front of you, and moreso when dealing with someone e Vera and make sure everything is correct for the Vera's physical location. Remember that in the western hemisphere (North, Central & South America, principally) your longitude will be a negative number. If you are below the equator, latitude will be negative. If you're not - sure what your latitude/longitude are, use a site like [MyGeoPosition.com](http://mygeoposition.com). + sure what your latitude/longitude are, use a site like MyGeoPosition.com. If you make any changes to your time or location configuration, restart your Vera.
  • You're not waiting long enough. DEMII doesn't instantly jump into action at sunset, it employs its configured cycle delays as well, so cycling will usually begin sometime after sunset, up to the configured maximum cycle delay (30 minutes by default).
  • Your house mode isn't "active." If you've configured DEMII to operate only in certain house modes, - make sure you're in one of those modes, otherwise DEMII will sit, even though it's enabled.
  • + make sure you're in one of those modes, otherwise DEMII will just sit, even though it's enabled.
    @@ -190,19 +193,20 @@ when the Vera is sitting in front of you, and moreso when dealing with someone e
    What's the difference between House Mode and Enabled/Disabled? Can I just use House Mode to enable and disable DEMII?
    The enabled/disabled state of DEMII is the "big red button" for its operation. If you configure DEMII to only run in certain - house modes, then you can theoretically leave DEMII enabled all the time, as it will operate (cycle lights) only when a - selected house mode is active. But, some people don't use the House Modes for various reasons, so having a master switch + house modes, then you can theoretically leave DEMII enabled all the time, as it will only operate (cycle lights) when a + selected house mode is active. But, some people don't use House Modes for various reasons, so having a master switch for DEMII is necessary.
    I have a feature request. Will you implement it?
    Absolutely definitely maybe. I'm willing to listen to what you want to do. But, keep in mind, nobody's getting rich writing Vera plugins, and I do other things that put food on my table. And, what seems like a good idea to you may be just that: a good idea for - the way _you_ want to use it. The more generally applicable your request is, the higher the likelihood that I'll entertain it. What + the way you want to use it. The more generally applicable your request is, the higher the likelihood that I'll entertain it. What I don't want to do is over-complicate this plug-in so it begins to rival PLEG for size and weight (no disrespect intended there at all--I'm a huge PLEG fan and use it extensively, but, dang). DEMII really has a simple job: make lights go on and off to cast a serious shadow of doubt in the mind of some knucklehead who might be thinking your house is empty and ripe for his picking. In any case, the best way to give me feature requests is to open up an issue (if you have a list, one issue per feature, please) in the - [GitHub repository](https://github.com/toggledbits/DeusExMachina/issues). Second best is sending me a message via the MCV forums - (I'm user `rigpapa`). + GitHub repository. + Second best is sending me a message via the MCV forums (I'm user `rigpapa`).
    + From 2b9bf1497e52d1e0ec7afd70cb965abaf0da142b Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Tue, 27 Dec 2016 09:58:59 -0500 Subject: [PATCH 29/49] Fix URN --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 449059f..6f2fc19 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ you simply use the SetTarget action to enable (newTargetValue=1) or disable (new The MiOS GUI for devices and scenes takes care of this for you in its code; if scripting in Lua, you simply do this: ``` -luup.call_action("urn:upnp-org-serviceId:SwitchPower1", "SetTarget", { newTargetValue = "0|1" }, pluginDeviceId) +luup.call_action("urn:upnp-org:serviceId:SwitchPower1", "SetTarget", { newTargetValue = "0|1" }, pluginDeviceId) ``` ### Triggers ### From 33cd37c0d173ba870113f64ef386325c9acdd50f Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Fri, 6 Jan 2017 19:15:40 -0500 Subject: [PATCH 30/49] Remove temp debug function --- J_DeusExMachinaII1_UI7.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/J_DeusExMachinaII1_UI7.js b/J_DeusExMachinaII1_UI7.js index a80f5c8..a7c8dd5 100644 --- a/J_DeusExMachinaII1_UI7.js +++ b/J_DeusExMachinaII1_UI7.js @@ -19,12 +19,6 @@ var DeusExMachinaII = (function(api) { api.registerEventHandler('on_ui_cpanel_before_close', myModule, 'onBeforeCpanelClose'); } - function safe(obj) { - if (obj === undefined) return "undefined" - else if (obj instanceof jQuery || obj.constructor.prototype.jquery) return "jQuery[" + obj.length + "]"; - else return '(' + typeof(obj) + ')' + obj.toString(); - } - function isDimmer(devid) { var v = api.getDeviceState( devid, "urn:upnp-org:serviceId:Dimming1", "LoadLevelStatus" ); if (v === undefined || v === false) return false; From 9e06074561aa2b14504996ca4af6e44894e53350 Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Sat, 7 Jan 2017 10:43:58 -0500 Subject: [PATCH 31/49] Fix argument list --- S_DeusExMachinaII1.xml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/S_DeusExMachinaII1.xml b/S_DeusExMachinaII1.xml index 9a8b1de..a59ae9c 100644 --- a/S_DeusExMachinaII1.xml +++ b/S_DeusExMachinaII1.xml @@ -40,9 +40,11 @@ SetEnabled - NewEnabledValue - Enabled - in + + NewEnabledValue + Enabled + in +
    From 65abf3b50d09296efe4189c943d6ca378bb6062d Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Sat, 7 Jan 2017 12:59:23 -0500 Subject: [PATCH 32/49] Add action to get plugin version string --- S_DeusExMachinaII1.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/S_DeusExMachinaII1.xml b/S_DeusExMachinaII1.xml index a59ae9c..59b8d20 100644 --- a/S_DeusExMachinaII1.xml +++ b/S_DeusExMachinaII1.xml @@ -47,5 +47,15 @@ + + GetPluginVersion + + + ResultVersion + out + TempStorage + + + \ No newline at end of file From 5a17b1f781a91b873a3094b51a0dfd3688310e77 Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Sat, 7 Jan 2017 12:59:49 -0500 Subject: [PATCH 33/49] Add action to get plugin version string --- I_DeusExMachinaII1.xml | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/I_DeusExMachinaII1.xml b/I_DeusExMachinaII1.xml index d647e1d..29450df 100644 --- a/I_DeusExMachinaII1.xml +++ b/I_DeusExMachinaII1.xml @@ -19,7 +19,7 @@ urn:upnp-org:serviceId:SwitchPower1 SetTarget - + local newTargetValue = lul_settings.newTargetValue or "0" luup.variable_set("urn:upnp-org:serviceId:SwitchPower1", "Target", newTargetValue, lul_device) if (newTargetValue == "1") then @@ -27,12 +27,26 @@ else demII.deusDisable() end - + + + + urn:upnp-org:serviceId:SwitchPower1 + GetTarget + + luup.variable_get("urn:upnp-org:serviceId:SwitchPower1", "Target", lul_device) + + + + urn:upnp-org:serviceId:SwitchPower1 + GetStatus + + luup.variable_get("urn:upnp-org:serviceId:SwitchPower1", "Status", lul_device) + urn:toggledbits-com:serviceId:DeusExMachinaII1 SetEnabled - + local newEnabledValue = lul_settings.NewEnabledValue or "0" luup.variable_set("urn:upnp-org:serviceId:SwitchPower1", "Target", newEnabledValue, lul_device) if (newEnabledValue == "1") then @@ -40,7 +54,18 @@ else demII.deusDisable() end - + + + + urn:toggledbits-com:serviceId:DeusExMachinaII1 + GetPluginVersion + + -- Ugly hack. Luup seems to only be able to return values from related state variables (see S_), so use a temp + -- one to store the result we want to pass back. Blech. C'mon guys. Amateur hour. Add an extra return argument + -- for a table of return values or something, please? + luup.variable_set("urn:toggledbits-com:serviceId:DeusExMachinaII1", "TempStorage", demII.getVersion(), lul_device) + return true + From f329697bbe8e043b3babdd8feba7ea8ba6b29d8e Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Sat, 7 Jan 2017 13:00:12 -0500 Subject: [PATCH 34/49] Comment out debug in prep for release --- J_DeusExMachinaII1_UI7.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/J_DeusExMachinaII1_UI7.js b/J_DeusExMachinaII1_UI7.js index a7c8dd5..0961daf 100644 --- a/J_DeusExMachinaII1_UI7.js +++ b/J_DeusExMachinaII1_UI7.js @@ -12,7 +12,7 @@ var DeusExMachinaII = (function(api) { var sceneNamesById = []; function onBeforeCpanelClose(args) { - console.log('handler for before cpanel close'); + // console.log('handler for before cpanel close'); } function init() { @@ -54,12 +54,12 @@ var DeusExMachinaII = (function(api) { }); jQuery('.controlled-scenes').each( function( ix, obj ) { var devid = jQuery(obj).attr('id'); - console.log('updateControlledList: handling scene pair ' + devid); + // console.log('updateControlledList: handling scene pair ' + devid); controlled.push(devid); }); var s = controlled.join(','); - console.log('Updating controlled list to ' + s); + // console.log('Updating controlled list to ' + s); api.setDeviceStatePersistent(deusDevice, serviceId, "Devices", s, 0); } @@ -347,7 +347,7 @@ var DeusExMachinaII = (function(api) { // Push generated HTML to page api.setCpanelContent(html); - + // Restore time field var time = "23:59"; var timeMins = parseInt(api.getDeviceState(deusDevice, serviceId, "LightsOut")); @@ -381,13 +381,11 @@ var DeusExMachinaII = (function(api) { var id = jQuery(obj).attr('id'); id = id.substr(6); jQuery('input#device'+id+':checked').each( function() { -console.log('input#device'+id+' is checked, enabling slider...'); // Corresponding checked checkbox, enable slider. jQuery(obj).slider("option", "disabled", false); var ix = DeusExMachinaII.findControlledDevice(id); if (ix >= 0) { var info = DeusExMachinaII.getControlled(ix); -console.log('found for ' + info.type + ' ' + info.device + ' with value=' + info.value + ', raw=' + info.raw + ", restoring slider value..."); jQuery(obj).slider("option", "value", info.type == "device" ? info.value : 100); } }); From ed641ffa2c56079dd95ac79638e5b5a3cc6addbb Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Sat, 7 Jan 2017 13:01:13 -0500 Subject: [PATCH 35/49] Support for action to get plugin version string --- L_DeusExMachinaII1.lua | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/L_DeusExMachinaII1.lua b/L_DeusExMachinaII1.lua index 3e54f97..81b8a5c 100644 --- a/L_DeusExMachinaII1.lua +++ b/L_DeusExMachinaII1.lua @@ -1,8 +1,10 @@ module("L_DeusExMachinaII1", package.seeall) -local SID = "urn:toggledbits-com:serviceId:DeusExMachinaII1" +local _VERSION = "2.4RC4" local DEMVERSION = 20400 +local SID = "urn:toggledbits-com:serviceId:DeusExMachinaII1" + local SWITCH_TYPE = "urn:schemas-upnp-org:device:BinaryLight:1" local SWITCH_SID = "urn:upnp-org:serviceId:SwitchPower1" local DIMMER_TYPE = "urn:schemas-upnp-org:device:DimmableLight:1" @@ -503,6 +505,11 @@ local function runOnce() luup.variable_set(SID, "Version", DEMVERSION, lul_device) end +-- Return the plugin version string +function getVersion() + return _VERSION +end + -- Enable DEM by setting a new cycle stamp and scheduling our first cycle step. function deusEnable() setMessage("Enabling...") @@ -532,7 +539,7 @@ end -- Initialize. function deusInit(deusDevice) setMessage("Initializing...") - luup.log("DeusExMachinaII:deusInit(): Version 2.4RC4 (2016-12-23), initializing...") + luup.log("DeusExMachinaII:deusInit(): Version " .. _VERSION .. ", initializing...") -- One-time stuff runOnce() From 6668d415dc0a2c595f9034ae8732414ff1e3ba3c Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Sat, 7 Jan 2017 13:26:24 -0500 Subject: [PATCH 36/49] Fix version for release --- D_DeusExMachinaII1_UI7.json | 2 +- L_DeusExMachinaII1.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/D_DeusExMachinaII1_UI7.json b/D_DeusExMachinaII1_UI7.json index a1081c6..7a0f00e 100644 --- a/D_DeusExMachinaII1_UI7.json +++ b/D_DeusExMachinaII1_UI7.json @@ -103,7 +103,7 @@ "left": "0", "Label": { "lang_tag": "dem_about", - "text": "DeusExMachina II ver 2.4RC4 2016-12-23
    + "text": "DeusExMachina II ver 2.4 2017-01-07
    For documentation or to report bugs, please go to the DeusExMachina Github repository.
    This plugin is offered for use as-is and without warranties of any kind. By using this plugin,
    you agree to assume all risks in connection with its use without limitation." diff --git a/L_DeusExMachinaII1.lua b/L_DeusExMachinaII1.lua index 81b8a5c..2e81c5b 100644 --- a/L_DeusExMachinaII1.lua +++ b/L_DeusExMachinaII1.lua @@ -1,6 +1,6 @@ module("L_DeusExMachinaII1", package.seeall) -local _VERSION = "2.4RC4" +local _VERSION = "2.4" local DEMVERSION = 20400 local SID = "urn:toggledbits-com:serviceId:DeusExMachinaII1" From a1c73476856536a152fd8ad8f163b6cded7ab67d Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Sun, 8 Jan 2017 22:30:39 -0500 Subject: [PATCH 37/49] Include name of device in debug output (helps with identification) --- L_DeusExMachinaII1.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/L_DeusExMachinaII1.lua b/L_DeusExMachinaII1.lua index 2e81c5b..b7ad1dd 100644 --- a/L_DeusExMachinaII1.lua +++ b/L_DeusExMachinaII1.lua @@ -345,19 +345,19 @@ local function targetControl(targetid, turnOn) -- Level for all types is 0 if turning device off if not turnOn then lvl = 0 end if luup.devices[targetid] == nil then - -- Device doesn't exist (user deleted, etc.). PHR??? Should remove from Devices state variable. + -- Device doesn't exist (user deleted, etc.). Remove from Devices state variable. luup.log("DeusExMachinaII:targetControl(): device " .. tostring(targetid) .. " not found in luup.devices") removeTarget(targetid) return end if luup.device_supports_service(DIMMER_SID, targetid) then -- Handle as Dimming1 - debug("targetControl(): handling %1 as dimmmer, set load level to %2", targetid, lvl) + debug("targetControl(): handling %1 (%3) as dimmmer, set load level to %2", targetid, lvl, luup.devices[targetid].description) luup.call_action(DIMMER_SID, "SetLoadLevelTarget", {newLoadlevelTarget=lvl}, targetid) -- note odd case inconsistency elseif luup.device_supports_service(SWITCH_SID, targetid) then -- Handle as SwitchPower1 if turnOn then lvl = 1 end - debug("targetControl(): handling %1 as switch, setting target to %2", targetid, lvl) + debug("targetControl(): handling %1 (%3) as switch, set target to %2", targetid, lvl, luup.devices[targetid].description) luup.call_action(SWITCH_SID, "SetTarget", {newTargetValue=lvl}, targetid) else luup.log("DeusExMachinaII:targetControl(): don't know how to control target " .. tostring(targetid)) From 94f09929c54f3384a27abf723a6c3240854d5d67 Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Sun, 8 Jan 2017 22:31:18 -0500 Subject: [PATCH 38/49] Reformat --- D_DeusExMachinaII1_UI7.json | 537 +++++++++++++++++++----------------- 1 file changed, 285 insertions(+), 252 deletions(-) diff --git a/D_DeusExMachinaII1_UI7.json b/D_DeusExMachinaII1_UI7.json index 7a0f00e..ce95038 100644 --- a/D_DeusExMachinaII1_UI7.json +++ b/D_DeusExMachinaII1_UI7.json @@ -1,258 +1,291 @@ -{ - "default_icon": "https://dtabq7xg0g1t1.cloudfront.net/deusII.png", - "doc_url": { - "doc_language": 1, - "doc_manual": 1, - "doc_version": 1, - "doc_platform": 0, - "doc_page": "devices" - }, - "state_icons": [{ - "img": "https://dtabq7xg0g1t1.cloudfront.net/deus-red.png", - "conditions": [{ - "service": "urn:upnp-org:serviceId:SwitchPower1", - "variable": "Status", - "operator": "==", - "value": 0, - "subcategory_num": 0 - }] - }, { - "img": "https://dtabq7xg0g1t1.cloudfront.net/deus-green.png", - "conditions": [{ - "service": "urn:upnp-org:serviceId:SwitchPower1", - "variable": "Status", - "operator": "==", - "value": 1, - "subcategory_num": 0 - }] - }], - "x": "2", - "y": "4", - "inScene": "1", - "ToggleButton": "1", - "Tabs": [{ - "Label": { - "lang_tag": "ui7_tabname_control", - "text": "Control" - }, - "Position": "0", - "TabType": "flash", - "top_navigation_tab": 1, - "ControlGroup": [{ - "id": "1", - "isSingle": "1", - "scenegroup": "1" - }], - "SceneGroup": [{ - "id": "1", - "top": "2", - "left": "0", - "x": "2", - "y": "1" - }], - "Control": [{ - "ControlGroup": "1", - "ControlType": "multi_state_button", - "top": "0", - "left": "1", - "states": [{ - "Label": { - "lang_tag": "ui7_cmd_on", - "text": "On" - }, - "ControlGroup": 1, - "Display": { - "Service": "urn:upnp-org:serviceId:SwitchPower1", - "Variable": "Status", - "Value": "1" - }, - "Command": { - "Service": "urn:upnp-org:serviceId:SwitchPower1", - "Action": "SetTarget", - "Parameters": [{ - "Name": "newTargetValue", - "Value": "1" - }] - }, - "ControlCode": "power_on" - }, { - "Label": { - "lang_tag": "ui7_cmd_off", - "text": "Off" - }, - "ControlGroup": 1, - "Display": { - "Service": "urn:upnp-org:serviceId:SwitchPower1", - "Variable": "Status", - "Value": "0" - }, - "Command": { - "Service": "urn:upnp-org:serviceId:SwitchPower1", - "Action": "SetTarget", - "Parameters": [{ - "Name": "newTargetValue", - "Value": "0" - }] - }, - "ControlCode": "power_off" - }], - "ControlCode": "dem_statecontrol" - }, { - "ControlType": "label", - "top": "0", - "left": "0", - "Label": { - "lang_tag": "dem_about", - "text": "DeusExMachina II ver 2.4 2017-01-07
    - For documentation or to report bugs, please go to the DeusExMachina Github repository.
    - This plugin is offered for use as-is and without warranties of any kind. By using this plugin,
    - you agree to assume all risks in connection with its use without limitation." - }, - "Display": { - "Top": "80", - "Left": "0", - "Width": "200", - "Height": "24" +{ + "default_icon":"https://dtabq7xg0g1t1.cloudfront.net/deusII.png", + "state_icons":[ + { + "img":"https://dtabq7xg0g1t1.cloudfront.net/deus-red.png", + "conditions":[ + { + "service":"urn:upnp-org:serviceId:SwitchPower1", + "variable":"Status", + "operator":"==", + "value":0, + "subcategory_num":0 + } + ] + }, + { + "img":"https://dtabq7xg0g1t1.cloudfront.net/deus-green.png", + "conditions":[ + { + "service":"urn:upnp-org:serviceId:SwitchPower1", + "variable":"Status", + "operator":"==", + "value":1, + "subcategory_num":0 } - }, { - "ControlType": "variable", - "top": "0", - "left": "0", - "Display": { - "Service": "urn:toggledbits-com:serviceId:DeusExMachinaII1", - "Variable": "Message", - "Top": "16", - "Left": "120", - "Width": "200", - "Height": "20" + ] + } + ], + "x":"2", + "y":"4", + "inScene":"1", + "ToggleButton":"1", + "Tabs":[ + { + "Label":{ + "lang_tag":"ui7_tabname_control", + "text":"Control" + }, + "Position":"0", + "TabType":"flash", + "top_navigation_tab":1, + "ControlGroup":[ + { + "id":"1", + "isSingle":"1", + "scenegroup":"1" } - }] - }, { - "Label": { - "lang_tag": "configure", - "text": "Configure" - }, - "Position": "1", - "TabType": "javascript", - "ScriptName": "J_DeusExMachinaII1_UI7.js", - "Function": "DeusExMachinaII.configureDeus" - }, { - "Label": { - "lang_tag": "ui7_advanced", - "text": "Advanced" - }, - "Position": "2", - "TabType": "javascript", - "ScriptName": "shared.js", - "Function": "advanced_device" - }], - "sceneList": { - "group_1": { - "cmd_1": { - "label": "ON", - "serviceId": "urn:upnp-org:serviceId:SwitchPower1", - "action": "SetTarget", - "arguments": { - "newTargetValue": "1" - }, - "display": { - "service": "urn:upnp-org:serviceId:SwitchPower1", - "variable": "Status", - "value": "1" - } + ], + "SceneGroup":[ + { + "id":"1", + "top":"2", + "left":"0", + "x":"2", + "y":"1" + } + ], + "Control":[ + { + "ControlGroup":"1", + "ControlType":"multi_state_button", + "top":"0", + "left":"1", + "states":[ + { + "Label":{ + "lang_tag":"ui7_cmd_on", + "text":"On" + }, + "ControlGroup":1, + "Display":{ + "Service":"urn:upnp-org:serviceId:SwitchPower1", + "Variable":"Status", + "Value":"1" + }, + "Command":{ + "Service":"urn:upnp-org:serviceId:SwitchPower1", + "Action":"SetTarget", + "Parameters":[ + { + "Name":"newTargetValue", + "Value":"1" + } + ] + }, + "ControlCode":"power_on" + }, + { + "Label":{ + "lang_tag":"ui7_cmd_off", + "text":"Off" + }, + "ControlGroup":1, + "Display":{ + "Service":"urn:upnp-org:serviceId:SwitchPower1", + "Variable":"Status", + "Value":"0" + }, + "Command":{ + "Service":"urn:upnp-org:serviceId:SwitchPower1", + "Action":"SetTarget", + "Parameters":[ + { + "Name":"newTargetValue", + "Value":"0" + } + ] + }, + "ControlCode":"power_off" + } + ] + }, + { + "ControlType":"label", + "top":"0", + "left":"0", + "Label":{ + "lang_tag":"dem_about", + "text":"DeusExMachina II ver 2.4 2017-01-07
    For documentation or to report bugs, please go to the DeusExMachina Github repository.
    This plugin is offered for use as-is and without warranties of any kind. By using this plugin,
    you agree to assume all risks in connection with its use without limitation." + }, + "Display":{ + "Top":"80", + "Left":"0", + "Width":"200", + "Height":"24" + } }, - "cmd_2": { - "label": "OFF", - "serviceId": "urn:upnp-org:serviceId:SwitchPower1", - "action": "SetTarget", - "arguments": { - "newTargetValue": "0" - }, - "display": { - "service": "urn:upnp-org:serviceId:SwitchPower1", - "variable": "Status", - "value": "0" - } + { + "ControlType":"variable", + "top":"0", + "left":"0", + "Display":{ + "Service":"urn:toggledbits-com:serviceId:DeusExMachinaII1", + "Variable":"Message", + "Top":"16", + "Left":"120", + "Width":"200", + "Height":"20" + } } - } - }, - "eventList2": [{ - "id": 1, - "label": { - "lang_tag": "dem_enabledisable", - "text": "Is enabled or disabled" - }, - "serviceId": "urn:upnp-org:serviceId:SwitchPower1", - "argumentList": [{ - "id": 1, - "dataType": "boolean", - "defaultValue": "0", - "allowedValueList": [{ - "Disabled": "0", - "HumanFriendlyText": { - "lang_tag": "dem_disabled", - "text": "_DEVICE_NAME_ is disabled" - } - }, { - "Enabled": "1", - "HumanFriendlyText": { - "lang_tag": "dem_enabled", - "text": "_DEVICE_NAME_ is enabled" - } - }], - "name": "Status", - "comparisson": "=", - "prefix": { - "lang_tag": "dem_when", - "text": "When" + ] + }, + { + "Label":{ + "lang_tag":"configure", + "text":"Configure" + }, + "Position":"1", + "TabType":"javascript", + "ScriptName":"J_DeusExMachinaII1_UI7.js", + "Function":"DeusExMachinaII.configureDeus" + }, + { + "Label":{ + "lang_tag":"ui7_advanced", + "text":"Advanced" + }, + "Position":"2", + "TabType":"javascript", + "ScriptName":"shared.js", + "Function":"advanced_device" + } + ], + "sceneList":{ + "group_1":{ + "cmd_1":{ + "label":"ON", + "serviceId":"urn:upnp-org:serviceId:SwitchPower1", + "action":"SetTarget", + "arguments":{ + "newTargetValue":"1" }, - "suffix": {} - }] - }, { - "id": 2, - "label": { - "lang_tag": "dem_opstate", - "text": "The operating mode changes" - }, - "serviceId": "urn:toggledbits-com:serviceId:DeusExMachinaII1", - "argumentList": [{ - "id": 1, - "dataType": "ui1", - "defaultValue": "0", - "allowedValueList": [{ - "value": "0", - "HumanFriendlyText": { - "lang_tag": "dem_standby", - "text": "Standby" - } - }, { - "value": "1", - "HumanFriendlyText": { - "lang_tag": "dem_ready", - "text": "Ready" - } - }, { - "value": "2", - "HumanFriendlyText": { - "lang_tag": "dem_cycle", - "text": "Cycling" - } - }, { - "value": "3", - "HumanFriendlyText": { - "lang_tag": "dem_shutoff", - "text": "Shut-off" - } - }], - "name": "State", - "comparisson": "=", - "prefix": { - "lang_tag": "dem_when", - "text": "to" + "display":{ + "service":"urn:upnp-org:serviceId:SwitchPower1", + "variable":"Status", + "value":"1" + } + }, + "cmd_2":{ + "label":"OFF", + "serviceId":"urn:upnp-org:serviceId:SwitchPower1", + "action":"SetTarget", + "arguments":{ + "newTargetValue":"0" }, - "suffix": {} - }] - }], - "DeviceType": "urn:schemas-toggledbits-com:device:DeusExMachinaII:1", - "device_type": "urn:schemas-toggledbits-com:device:DeusExMachinaII:1" -} + "display":{ + "service":"urn:upnp-org:serviceId:SwitchPower1", + "variable":"Status", + "value":"0" + } + } + } + }, + "eventList2":[ + { + "id":1, + "label":{ + "lang_tag":"dem_enabledisable", + "text":"Is enabled or disabled" + }, + "serviceId":"urn:upnp-org:serviceId:SwitchPower1", + "argumentList":[ + { + "id":1, + "dataType":"boolean", + "defaultValue":"0", + "allowedValueList":[ + { + "Disabled":"0", + "HumanFriendlyText":{ + "lang_tag":"dem_disabled", + "text":"_DEVICE_NAME_ is disabled" + } + }, + { + "Enabled":"1", + "HumanFriendlyText":{ + "lang_tag":"dem_enabled", + "text":"_DEVICE_NAME_ is enabled" + } + } + ], + "name":"Status", + "comparisson":"=", + "prefix":{ + "lang_tag":"dem_when", + "text":"When" + }, + "suffix":{ + + } + } + ] + }, + { + "id":2, + "label":{ + "lang_tag":"dem_opstate", + "text":"The operating mode changes" + }, + "serviceId":"urn:toggledbits-com:serviceId:DeusExMachinaII1", + "argumentList":[ + { + "id":1, + "dataType":"ui1", + "defaultValue":"0", + "allowedValueList":[ + { + "value":"0", + "HumanFriendlyText":{ + "lang_tag":"dem_standby", + "text":"Standby" + } + }, + { + "value":"1", + "HumanFriendlyText":{ + "lang_tag":"dem_ready", + "text":"Ready" + } + }, + { + "value":"2", + "HumanFriendlyText":{ + "lang_tag":"dem_cycle", + "text":"Cycling" + } + }, + { + "value":"3", + "HumanFriendlyText":{ + "lang_tag":"dem_shutoff", + "text":"Shut-off" + } + } + ], + "name":"State", + "comparisson":"=", + "prefix":{ + "lang_tag":"dem_when", + "text":"to" + }, + "suffix":{ + + } + } + ] + } + ], + "device_type":"urn:schemas-toggledbits-com:device:DeusExMachinaII:1" +} \ No newline at end of file From d766a94c9b9b4a3495ff0fea48982f49178dcf19 Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Mon, 9 Jan 2017 18:14:42 -0500 Subject: [PATCH 39/49] 1) Fix call_delay use so that delayed execution occurs async and the current thread is released. Change order of initialization around those calls to make sure all state is initialized even if the call doesn't return (redundant fix for the same issue: some state variables were being set after call_delay, but the old usage may not have allowed the call to return before a Luup crash, so the inits would never get done). 2) Fix device number handling. The 'lul_device' pseudo-global is only available in a limited context, and most of L_ isn't it. While Luup behaves well when passed nil device numbers to many calls, it's a bug and would be sloppy coding IMHO to leave it wrong even though it still works right (and not just sloppy, but misleading to anyone else reading the code). --- I_DeusExMachinaII1.xml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/I_DeusExMachinaII1.xml b/I_DeusExMachinaII1.xml index 29450df..235d189 100644 --- a/I_DeusExMachinaII1.xml +++ b/I_DeusExMachinaII1.xml @@ -7,11 +7,11 @@ -- Original code and releases 1.x by Andy Lintner (beowulfe) Version 2.0 and beyond by Patrick Rigney (rigpapa/toggledbits). -- A big thanks to Andy for passing the torch so that this great plug-in can live on. -- ------------------------------------------------------------------------------------------------------------------------- - function startupDeusExMachinaII1() + function startupDeusExMachinaII1(deusDevice) luup.log("DeusExMachinaII STARTUP!") demII = require("L_DeusExMachinaII1") deusStep = demII.deusStep - demII.deusInit() + demII.deusInit(deusDevice) end startupDeusExMachinaII1 @@ -21,11 +21,10 @@ SetTarget local newTargetValue = lul_settings.newTargetValue or "0" - luup.variable_set("urn:upnp-org:serviceId:SwitchPower1", "Target", newTargetValue, lul_device) if (newTargetValue == "1") then - demII.deusEnable() + demII.deusEnable(lul_device) else - demII.deusDisable() + demII.deusDisable(lul_device) end @@ -50,9 +49,9 @@ local newEnabledValue = lul_settings.NewEnabledValue or "0" luup.variable_set("urn:upnp-org:serviceId:SwitchPower1", "Target", newEnabledValue, lul_device) if (newEnabledValue == "1") then - demII.deusEnable() + demII.deusEnable(lul_device) else - demII.deusDisable() + demII.deusDisable(lul_device) end From 5d8f3d1ecfb9780044647398a8337b7ff7dd2e08 Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Mon, 9 Jan 2017 18:19:29 -0500 Subject: [PATCH 40/49] 1) Fix call_delay use so that delayed execution occurs async and the current thread is released. Change order of initialization around those calls to make sure all state is initialized even if the call doesn't return (redundant fix for the same issue: some state variables were being set after call_delay, but the old usage may not have allowed the call to return before a Luup crash, so the inits would never get done). 2) Fix device number handling. The 'lul_device' pseudo-global is only available in a limited context, and most of L_ isn't it. While Luup behaves well when passed nil device numbers to many calls, it's a bug and would be sloppy coding IMHO to leave it wrong even though it still works right (and not just sloppy, but misleading to anyone else reading the code). --- L_DeusExMachinaII1.lua | 155 ++++++++++++++++++++++++----------------- 1 file changed, 90 insertions(+), 65 deletions(-) diff --git a/L_DeusExMachinaII1.lua b/L_DeusExMachinaII1.lua index b7ad1dd..bce2a8b 100644 --- a/L_DeusExMachinaII1.lua +++ b/L_DeusExMachinaII1.lua @@ -1,6 +1,6 @@ module("L_DeusExMachinaII1", package.seeall) -local _VERSION = "2.4" +local _VERSION = "2.4RC5" local DEMVERSION = 20400 local SID = "urn:toggledbits-com:serviceId:DeusExMachinaII1" @@ -16,12 +16,11 @@ local STATE_CYCLE = 2 local STATE_SHUTDOWN = 3 local runStamp = 0 - -local debugMode = false +local debugMode = true local function debug(...) if debugMode then - local str = "DeusExMachinaII1:" .. arg[1] + local str = "DeusExMachinaII1(dbg):" .. arg[1] local ipos = 1 while true do local i, j, n @@ -42,21 +41,22 @@ local function debug(...) end local function checkVersion() - local ui7Check = luup.variable_get(SID, "UI7Check", lul_device) or "" + local ui7Check = luup.variable_get(SID, "UI7Check", luup.device) or "" if ui7Check == "" then - luup.variable_set(SID, "UI7Check", "false", lul_device) + luup.variable_set(SID, "UI7Check", "false", luup.device) ui7Check = "false" end if ( luup.version_branch == 1 and luup.version_major == 7 and ui7Check == "false" ) then - luup.variable_set(SID, "UI7Check", "true", lul_device) - luup.attr_set("device_json", "D_DeusExMachinaII1_UI7.json", lul_device) + luup.variable_set(SID, "UI7Check", "true", luup.device) + luup.attr_set("device_json", "D_DeusExMachinaII1_UI7.json", luup.device) luup.reload() end end -- Get numeric variable, or return default value if not set or blank -local function getVarNumeric( name, dflt ) - local s = luup.variable_get(SID, name, lul_device) +local function getVarNumeric( name, dflt, dev ) + if dev == nil then dev = luup.device end + local s = luup.variable_get(SID, name, dev) if (s == nil or s == "") then return dflt end s = tonumber(s, 10) if (s == nil) then return dflt end @@ -86,12 +86,12 @@ local function timeToString(t) end local function setMessage(s) - luup.variable_set(SID, "Message", s or "", lul_device) + luup.variable_set(SID, "Message", s or "", luup.device) end -- Shortcut function to return state of SwitchPower1 Status variable local function isEnabled() - local s = luup.variable_get(SWITCH_SID, "Status", lul_device) + local s = luup.variable_get(SWITCH_SID, "Status", luup.device) if (s == nil or s == "") then return false end return (s ~= "0") end @@ -163,7 +163,7 @@ end -- if the current time is between sunset and off; otherwise 1. Note that all times are reduced -- to minutes-since-midnight units. local function isBedtime() - local testing = getVarNumeric("TestMode", 0) + local testing = getVarNumeric("TestMode", 0, luup.device) if (testing ~= 0) then luup.log('DeusExMachinaII:isBedtime(): TestMode is on') debugMode = true @@ -171,7 +171,7 @@ local function isBedtime() -- Establish the lights-out time local bedtime = 1439 -- that's 23:59 in minutes since midnight (default) - local bedtime_tmp = luup.variable_get(SID, "LightsOut", lul_device) + local bedtime_tmp = luup.variable_get(SID, "LightsOut", luup.device) if (bedtime_tmp ~= nil) then bedtime_tmp = tonumber(bedtime_tmp,10) if (bedtime_tmp >= 0 and bedtime_tmp < 1440) then bedtime = bedtime_tmp end @@ -228,7 +228,7 @@ end -- Return true if a specified scene has been run (i.e. on the list) local function isSceneOn(spec) - local stateList = luup.variable_get(SID, "ScenesRunning", lul_device) or "" + local stateList = luup.variable_get(SID, "ScenesRunning", luup.device) or "" for i in string.gfind(stateList, "[^,]+") do if (i == spec) then return true end end @@ -237,7 +237,7 @@ end -- Mark or unmark a scene as having been run local function updateSceneState(spec, isOn) - local stateList = luup.variable_get(SID, "ScenesRunning", lul_device) or "" + local stateList = luup.variable_get(SID, "ScenesRunning", luup.device) or "" local i local t = {} for i in string.gfind(stateList, "[^,]+") do @@ -250,7 +250,7 @@ local function updateSceneState(spec, isOn) end stateList = "" for i in pairs(t) do stateList = stateList .. "," .. tostring(i) end - luup.variable_set(SID, "ScenesRunning", string.sub(stateList, 2, -1), lul_device) + luup.variable_set(SID, "ScenesRunning", string.sub(stateList, 2, -1), luup.device) end -- Run "final" scene, if defined. This scene is run after all other targets have been @@ -265,7 +265,7 @@ end -- Get the list of targets from our device state, parse to table of targets. local function getTargetList() - local s = luup.variable_get(SID, "Devices", lul_device) or "" + local s = luup.variable_get(SID, "Devices", luup.device) or "" return split(s) end @@ -276,7 +276,7 @@ local function removeTarget(target, tlist) for i = 1,table.getn(tlist) do if tostring(target) == tlist[i] then table.remove(tlist, i) - luup.variable_set(SID, "Devices", table.concat(tlist, ","), lul_device) + luup.variable_set(SID, "Devices", table.concat(tlist, ","), deusDevuce) return true end end @@ -409,7 +409,7 @@ end local function clearLights() local devs, count devs, count = getTargetList() - luup.variable_set(SID, "State", STATE_SHUTDOWN, lul_device) + luup.variable_set(SID, "State", STATE_SHUTDOWN, luup.device) while count > 0 do targetControl(devs[count], false) count = count - 1 @@ -421,7 +421,7 @@ end -- takes place. For us, that means looking to see if an older version of Deus is still -- installed, and copying its config into our new config. Then disable the old Deus. local function runOnce() - local s = luup.variable_get(SID, "Devices", lul_device) + local s = luup.variable_get(SID, "Devices", luup.device) if (s == nil) then luup.log("DeusExMachinaII:runOnce(): Devices variable not found, setting up new instance...") -- See if there are variables from older version of DEM @@ -442,8 +442,8 @@ local function runOnce() s = luup.variable_get(oldsid, "LightsOutTime", olddev) if (s ~= nil) then local n = tonumber(s,10) / 60000 - luup.variable_set(SID, "LightsOut", n, lul_device) - deleteVar("LightsOutTime", lul_device) + luup.variable_set(SID, "LightsOut", n, luup.device) + deleteVar("LightsOutTime", luup.device) end s = luup.variable_get(oldsid, "controlCount", olddev) if (s ~= nil) then @@ -457,25 +457,25 @@ local function runOnce() end end devList = table.concat(t, ",") - deleteVar("controlCount", lul_device) + deleteVar("controlCount", luup.device) end -- Finally, turn off old Deus luup.call_action(oldsid, "SetEnabled", { NewEnabledValue = "0" }, olddev) end - luup.variable_set(SID, "Devices", devList, lul_device) + luup.variable_set(SID, "Devices", devList, luup.device) -- Set up some other default config - luup.variable_set(SID, "MinCycleDelay", "300", lul_device) - luup.variable_set(SID, "MaxCycleDelay", "1800", lul_device) - luup.variable_set(SID, "MinOffDelay", "60", lul_device) - luup.variable_set(SID, "MaxOffDelay", "300", lul_device) - luup.variable_set(SID, "LightsOut", 1439, lul_device) - luup.variable_set(SID, "MaxTargetsOn", 0, lul_device) - luup.variable_set(SID, "Enabled", "0", lul_device, true) - luup.variable_set(SID, "Version", DEMVERSION, lul_device) - luup.variable_set(SWITCH_SID, "Status", "0", lul_device, true) - luup.variable_set(SWITCH_SID, "Target", "0", lul_device, true) + luup.variable_set(SID, "MinCycleDelay", "300", luup.device) + luup.variable_set(SID, "MaxCycleDelay", "1800", luup.device) + luup.variable_set(SID, "MinOffDelay", "60", luup.device) + luup.variable_set(SID, "MaxOffDelay", "300", luup.device) + luup.variable_set(SID, "LightsOut", 1439, luup.device) + luup.variable_set(SID, "MaxTargetsOn", 0, luup.device) + luup.variable_set(SID, "Enabled", "0", luup.device) + luup.variable_set(SID, "Version", DEMVERSION, luup.device) + luup.variable_set(SWITCH_SID, "Status", "0", luup.device) + luup.variable_set(SWITCH_SID, "Target", "0", luup.device) end -- Consider per-version changes. @@ -483,63 +483,88 @@ local function runOnce() if (s < 20300) then -- v2.3: LightsOutTime (in milliseconds) deprecated, now using LightsOut (in minutes since midnight) luup.log("DeusExMachinaII:runOnce(): updating config, version " .. tostring(s) .. " < 20300") - s = luup.variable_get(SID, "LightsOut", lul_device) + s = luup.variable_get(SID, "LightsOut", luup.device) if (s == nil) then s = getVarNumeric("LightsOutTime") -- get pre-2.3 variable if (s == nil) then - luup.variable_set(SID, "LightsOut", 1439, lul_device) -- default 23:59 + luup.variable_set(SID, "LightsOut", 1439, luup.device) -- default 23:59 else - luup.variable_set(SID, "LightsOut", tonumber(s,10) / 60000, lul_device) -- conv ms to minutes + luup.variable_set(SID, "LightsOut", tonumber(s,10) / 60000, luup.device) -- conv ms to minutes end end - deleteVar("LightsOutTime", lul_device) + deleteVar("LightsOutTime", luup.device) end if (s < 20400) then - luup.variable_set(SID, "MaxTargetsOn", 0, lul_device) + -- v2.4: SwitchPower1 variables added. Follow previous plugin state in case of auto-update. + luup.log("DeusExMachinaII:runOnce(): updating config, version " .. tostring(s) .. " < 20400") + luup.variable_set(SID, "MaxTargetsOn", 0, luup.device) local e = getVarNumeric("Enabled", 0) - luup.variable_set(SWITCH_SID, "Status", e, lul_device, true) - luup.variable_set(SWITCH_SID, "Target", e, lul_device, true) + luup.variable_set(SWITCH_SID, "Status", e, luup.device) + luup.variable_set(SWITCH_SID, "Target", e, luup.device) end - -- Update version last. - luup.variable_set(SID, "Version", DEMVERSION, lul_device) + -- Update version state last. + if (s ~= DEMVERSION) then + luup.variable_set(SID, "Version", DEMVERSION, luup.device) + end end -- Return the plugin version string function getVersion() - return _VERSION + return _VERSION, DEMVERSION end -- Enable DEM by setting a new cycle stamp and scheduling our first cycle step. function deusEnable() + luup.log("DeusExMachinaII:deusEnable(): enabling, luup.device=" .. tostring(luup.device)) + luup.variable_set(SWITCH_SID, "Target", "1", luup.device) + setMessage("Enabling...") - luup.log("DeusExMachinaII:deusEnable(): enabling...") - luup.variable_set(SID, "ScenesRunning", "", lul_device) -- start with a clean slate + + luup.variable_set(SWITCH_SID, "Status", "1", luup.device) + luup.variable_set(SID, "Enabled", "1", luup.device) + runStamp = os.time() - luup.call_delay("deusStep", 1, runStamp, 1) - luup.variable_set(SID, "Enabled", "1", lul_device) - luup.variable_set(SWITCH_SID, "Status", "1", lul_device) + + luup.call_delay("deusStep", 1, runStamp) + debug("deusEnable(): scheduled first step, done") end -- Disable DEM and go to standby state. If we are currently cycling (as opposed to idle/waiting for sunset), -- turn off any controlled lights that are on. function deusDisable() + luup.log("DeusExMachinaII:deusDisable(): disabling, luup.device=" .. tostring(luup.device)) + luup.variable_set(SWITCH_SID, "Target", "0", luup.device) + setMessage("Disabling...") - local s = getVarNumeric("State", STATE_STANDBY) - luup.log("DeusExMachinaII:deusDisable(): disabling...") + + local s = getVarNumeric("State", STATE_STANDBY, luup.device) if ( s == STATE_CYCLE or s == STATE_SHUTDOWN ) then clearLights() end - luup.variable_set(SID, "State", STATE_STANDBY, lul_device) - luup.variable_set(SID, "Enabled", "0", lul_device) - luup.variable_set(SWITCH_SID, "Status", "0", lul_device) + runStamp = 0 + + luup.variable_set(SID, "ScenesRunning", "", luup.device) -- start with a clean slate next time + luup.variable_set(SID, "State", STATE_STANDBY, luup.device) + luup.variable_set(SID, "Enabled", "0", luup.device) + luup.variable_set(SWITCH_SID, "Status", "0", luup.device) + setMessage("") end -- Initialize. -function deusInit(deusDevice) +function deusInit(pdev) + luup.log("DeusExMachinaII:deusInit(" .. tostring(pdev) .. "): Version " .. _VERSION .. ", initializing, luup.device=" .. tostring(luup.device)) + + runStamp = 0 + setMessage("Initializing...") - luup.log("DeusExMachinaII:deusInit(): Version " .. _VERSION .. ", initializing...") + + if debugMode or true then + local status, body, httpStatus + status, body, httpStatus = luup.inet.wget("http://127.0.0.1:3480/data_request?id=status&DeviceNum=" .. tostring(luup.device) .. "&output_format=json") + debug("deusInit(): status %2, startup state is %1", body, status) + end -- One-time stuff runOnce() @@ -562,7 +587,7 @@ end -- delays, so the lights going off looks more "natural" (i.e. not all at once just slamming off). function deusStep(stepStampCheck) local stepStamp = tonumber(stepStampCheck) - luup.log("DeusExMachinaII:deusStep(): wakeup, stamp " .. stepStampCheck) + luup.log("DeusExMachinaII:deusStep(): wakeup, stamp " .. tostring(stepStampCheck) .. ", luup.device=" .. tostring(luup.device)) if (stepStamp ~= runStamp) then luup.log("DeusExMachinaII:deusStep(): stamp mismatch, another thread running. Bye!") return @@ -578,7 +603,7 @@ function deusStep(stepStampCheck) local currentState = getVarNumeric("State", 0) if (currentState == STATE_STANDBY or currentState == STATE_IDLE) then debug("deusStep(): step in state %1, lightsout=%2, sunset=%3, os.time=%4", currentState, - luup.variable_get(SID, "LightsOut", lul_device), sunset, os.time()) + luup.variable_get(SID, "LightsOut", luup.device), sunset, os.time()) debug("deusStep(): luup variables longitude=%1, latitude=%2, timezone=%3, city=%4, sunset=%5, version=%6", luup.longitude, luup.latitude, luup.timezone, luup.city, luup.sunset(), luup.version) end @@ -596,14 +621,14 @@ function deusStep(stepStampCheck) -- Go to IDLE and delay for next sunset. luup.log("DeusExMachinaII:deusStep(): transitioning to IDLE from STANDBY, waiting for next sunset...") nextCycleDelay = sunset - os.time() + getRandomDelay("MinCycleDelay", "MaxCycleDelay") - luup.variable_set(SID, "State", STATE_IDLE, lul_device) + luup.variable_set(SID, "State", STATE_IDLE, luup.device) setMessage("Waiting for sunset " .. timeToString(os.date("*t", os.time() + nextCycleDelay))) elseif (not isActiveHouseMode()) then -- Not in an active house mode. If we're not STANDBY or IDLE, turn everything back off and go to IDLE. if (currentState ~= STATE_IDLE) then luup.log("DeusExMachinaII:deusStep(): transitioning to IDLE, not in an active house mode.") if (currentState ~= STATE_STANDBY) then clearLights() end -- turn off lights quickly unless transitioning from STANDBY - luup.variable_set(SID, "State", STATE_IDLE, lul_device) + luup.variable_set(SID, "State", STATE_IDLE, luup.device) else luup.log("DeusExMachinaII:deusStep(): IDLE in an inactive house mode; waiting for mode change.") end @@ -618,11 +643,11 @@ function deusStep(stepStampCheck) setMessage("Waiting for active house mode") elseif (not inActiveTimePeriod) then luup.log("DeusExMachinaII:deusStep(): running off cycle") - luup.variable_set(SID, "State", STATE_SHUTDOWN, lul_device) + luup.variable_set(SID, "State", STATE_SHUTDOWN, luup.device) if (not turnOffLight()) then -- No more lights to turn off runFinalScene() - luup.variable_set(SID, "State", STATE_IDLE, lul_device) + luup.variable_set(SID, "State", STATE_IDLE, luup.device) nextCycleDelay = sunset - os.time() + getRandomDelay("MinCycleDelay", "MaxCycleDelay") luup.log("DeusExMachina:deusStep(): all lights out; now IDLE, setting delay to restart cycling at next sunset") setMessage("Waiting for sunset " .. timeToString(os.date("*t", os.time() + nextCycleDelay))) @@ -633,7 +658,7 @@ function deusStep(stepStampCheck) else -- Fully active. Find a random target to control and control it. luup.log("DeusExMachinaII:deusStep(): running toggle cycle") - luup.variable_set(SID, "State", STATE_CYCLE, lul_device) + luup.variable_set(SID, "State", STATE_CYCLE, luup.device) nextCycleDelay = getRandomDelay("MinCycleDelay", "MaxCycleDelay") local devs, max devs, max = getTargetList() @@ -676,7 +701,7 @@ function deusStep(stepStampCheck) if nextCycleDelay ~= nil then luup.log("DeusExMachinaII:deusStep(): cycle finished, next in " .. nextCycleDelay .. " seconds") if nextCycleDelay < 1 then nextCycleDelay = 60 end - luup.call_delay("deusStep", nextCycleDelay, stepStamp, 1) + luup.call_delay("deusStep", nextCycleDelay, stepStamp) else luup.log("DeusExMachinaII:deusStep(): nil nextCycleDelay, next cycle not scheduled!") end From 017a51ebfaa9b3f1bbc7e4801fd53316853145c5 Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Mon, 9 Jan 2017 19:00:24 -0500 Subject: [PATCH 41/49] Remove scene target from device list if on or off scene no longer exists; little code cleanup/remove redundant parameters from function calls --- L_DeusExMachinaII1.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/L_DeusExMachinaII1.lua b/L_DeusExMachinaII1.lua index bce2a8b..99d87ef 100644 --- a/L_DeusExMachinaII1.lua +++ b/L_DeusExMachinaII1.lua @@ -163,7 +163,7 @@ end -- if the current time is between sunset and off; otherwise 1. Note that all times are reduced -- to minutes-since-midnight units. local function isBedtime() - local testing = getVarNumeric("TestMode", 0, luup.device) + local testing = getVarNumeric("TestMode", 0) if (testing ~= 0) then luup.log('DeusExMachinaII:isBedtime(): TestMode is on') debugMode = true @@ -326,6 +326,7 @@ local function targetControl(targetid, turnOn) if luup.scenes[onScene] == nil or luup.scenes[offScene] == nil then -- Both on scene and off scene must exist (defensive). luup.log("DeusExMachinaII:targetControl(): one or both of the scenes in " .. tostring(targetid) .. " not found in luup.scenes!") + removeTarget(targetid) return end debug("targetControl(): on scene is %1, off scene is %2", onScene, offScene) @@ -538,7 +539,7 @@ function deusDisable() setMessage("Disabling...") - local s = getVarNumeric("State", STATE_STANDBY, luup.device) + local s = getVarNumeric("State", STATE_STANDBY) if ( s == STATE_CYCLE or s == STATE_SHUTDOWN ) then clearLights() end From 50d3a3eb968d2966e27d39c0d1800652bc7cb4ac Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Mon, 9 Jan 2017 19:17:14 -0500 Subject: [PATCH 42/49] Don't allow DEMII to operate on itself --- J_DeusExMachinaII1_UI7.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/J_DeusExMachinaII1_UI7.js b/J_DeusExMachinaII1_UI7.js index 0961daf..ae5af2c 100644 --- a/J_DeusExMachinaII1_UI7.js +++ b/J_DeusExMachinaII1_UI7.js @@ -26,8 +26,10 @@ var DeusExMachinaII = (function(api) { } function isControllable(devid) { + var v = api.getDeviceState( devid, "urn:toggledbits-com:serviceId:DeusExMachinaII1", "LightsOut" ); + if (!(v === undefined || v === false)) return false; if (isDimmer(devid)) return true; /* a dimmer is a light */ - var v = api.getDeviceState( devid, "urn:upnp-org:serviceId:SwitchPower1", "Status" ); + v = api.getDeviceState( devid, "urn:upnp-org:serviceId:SwitchPower1", "Status" ); if (v === undefined || v === false) return false; return true; } From ddf5dc9d155821f3dc992c6dfafd7c64f55d456b Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Tue, 10 Jan 2017 20:10:36 -0500 Subject: [PATCH 43/49] Add example Lua for final scene to detect difference between run when disabling vs run when going to sleep/idle --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 6f2fc19..394525e 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,19 @@ set this value to some reasonable limit. DEMII allows a "final scene" to run when DEMII is disabled or turns off the last light after the "lights out" time. This could be used for any purpose. I personally use it to make sure a whole-house off is run, but you could use it to ensure your alarm system is armed, or your garage door is closed, etc. +The scene can differentiate between DEMII being disabled and DEMII just going to sleep by checking the `Target` variable in service `urn:upnp-org:serviceId:SwitchPower1`. If the value is "0", then DEMII is being disabled. Otherwise, DEMII is going to sleep. The following code snippet, added as scene Lua, will allow the scene to only run when DEMII is being disabled: + +``` +local val = luup.variable_get("urn:upnp-org:serviceId:SwitchPower1", "Target", pluginDeviceId) +if val == "0" then + -- Disabling, so return true (scene execution continues). + return true +else + -- Not disabling, just going to sleep. Returning false stops scene execution. + return false +end +``` + ### Control of DEMII by Scenes and Lua ### DeusExMachina can be enabled or disabled like a light switch in scenes or through the regular graphical interface (no Lua required), From c48ddec0e06fe2402967021e826c76d8e0cd9229 Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Tue, 10 Jan 2017 20:12:00 -0500 Subject: [PATCH 44/49] Move destruction of runStamp so delay thread won't execute while we're trying to shut down --- L_DeusExMachinaII1.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/L_DeusExMachinaII1.lua b/L_DeusExMachinaII1.lua index 99d87ef..ad3e8f6 100644 --- a/L_DeusExMachinaII1.lua +++ b/L_DeusExMachinaII1.lua @@ -538,12 +538,14 @@ function deusDisable() luup.variable_set(SWITCH_SID, "Target", "0", luup.device) setMessage("Disabling...") + + -- Destroy runStamp so any thread running while we shut down just exits. + runStamp = 0 local s = getVarNumeric("State", STATE_STANDBY) if ( s == STATE_CYCLE or s == STATE_SHUTDOWN ) then clearLights() end - runStamp = 0 luup.variable_set(SID, "ScenesRunning", "", luup.device) -- start with a clean slate next time luup.variable_set(SID, "State", STATE_STANDBY, luup.device) From db7880181d90d1fd3e58502253485ab72f79b441 Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Fri, 13 Jan 2017 13:32:49 -0500 Subject: [PATCH 45/49] Trying to avoid referncing version in feature descriptions --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 394525e..8cf3495 100644 --- a/README.md +++ b/README.md @@ -84,8 +84,8 @@ Both scenes and individual devices (from the device list above) can be used simu #### Maximum "On" Targets #### -Version 2.4 also adds the ability to limit the number of targets (devices or scenes) that DEMII can have "on" simultaneously. -If this limit is 0, there is no limit enforced. If you have DEMII control a large number of devices, it's probably not a bad idea to +This value sets the limit on the number of targets (devices or scenes) that DEMII can have "on" simultaneously. +If 0, there is no limit. If you have DEMII controlling a large number of devices, it's probably not a bad idea to set this value to some reasonable limit. #### Final Scene #### From fda9c5c3a798c0559ebaeecceb6094b4761c3eeb Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Sun, 15 Jan 2017 07:12:38 -0500 Subject: [PATCH 46/49] Prep for release --- D_DeusExMachinaII1_UI7.json | 2 +- L_DeusExMachinaII1.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/D_DeusExMachinaII1_UI7.json b/D_DeusExMachinaII1_UI7.json index ce95038..3792828 100644 --- a/D_DeusExMachinaII1_UI7.json +++ b/D_DeusExMachinaII1_UI7.json @@ -116,7 +116,7 @@ "left":"0", "Label":{ "lang_tag":"dem_about", - "text":"DeusExMachina II ver 2.4 2017-01-07
    For documentation or to report bugs, please go to the DeusExMachina Github repository.
    This plugin is offered for use as-is and without warranties of any kind. By using this plugin,
    you agree to assume all risks in connection with its use without limitation." + "text":"DeusExMachina II ver 2.4 2017-01-15
    For documentation or to report bugs, please go to the DeusExMachina Github repository.
    This plugin is offered for use as-is and without warranties of any kind. By using this plugin,
    you agree to assume all risks in connection with its use without limitation." }, "Display":{ "Top":"80", diff --git a/L_DeusExMachinaII1.lua b/L_DeusExMachinaII1.lua index ad3e8f6..bf888d4 100644 --- a/L_DeusExMachinaII1.lua +++ b/L_DeusExMachinaII1.lua @@ -1,6 +1,6 @@ module("L_DeusExMachinaII1", package.seeall) -local _VERSION = "2.4RC5" +local _VERSION = "2.4" local DEMVERSION = 20400 local SID = "urn:toggledbits-com:serviceId:DeusExMachinaII1" From e6f6393103c9cf798217df7695ec527bf72ba433 Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Sun, 15 Jan 2017 07:17:20 -0500 Subject: [PATCH 47/49] Prep for release --- L_DeusExMachinaII1.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/L_DeusExMachinaII1.lua b/L_DeusExMachinaII1.lua index bf888d4..f21b8f0 100644 --- a/L_DeusExMachinaII1.lua +++ b/L_DeusExMachinaII1.lua @@ -16,7 +16,7 @@ local STATE_CYCLE = 2 local STATE_SHUTDOWN = 3 local runStamp = 0 -local debugMode = true +local debugMode = false local function debug(...) if debugMode then From 03359ae371cca389f462f3f12f0be4e4863f40b5 Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Sun, 15 Jan 2017 09:08:16 -0500 Subject: [PATCH 48/49] One more tweak to make sure Luup restarts don't bug us. --- L_DeusExMachinaII1.lua | 78 +++++++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/L_DeusExMachinaII1.lua b/L_DeusExMachinaII1.lua index f21b8f0..66c7200 100644 --- a/L_DeusExMachinaII1.lua +++ b/L_DeusExMachinaII1.lua @@ -91,7 +91,7 @@ end -- Shortcut function to return state of SwitchPower1 Status variable local function isEnabled() - local s = luup.variable_get(SWITCH_SID, "Status", luup.device) + local s = luup.variable_get(SWITCH_SID, "Target", luup.device) if (s == nil or s == "") then return false end return (s ~= "0") end @@ -500,8 +500,8 @@ local function runOnce() luup.log("DeusExMachinaII:runOnce(): updating config, version " .. tostring(s) .. " < 20400") luup.variable_set(SID, "MaxTargetsOn", 0, luup.device) local e = getVarNumeric("Enabled", 0) - luup.variable_set(SWITCH_SID, "Status", e, luup.device) luup.variable_set(SWITCH_SID, "Target", e, luup.device) + luup.variable_set(SWITCH_SID, "Status", 0, luup.device) end -- Update version state last. @@ -515,6 +515,16 @@ function getVersion() return _VERSION, DEMVERSION end +-- Start stepper running. Note that we don't change state here. The intention is that Deus continues +-- in its saved operating state. +local function deusStart() + -- Immediately set a new timestamp + runStamp = os.time() + + luup.call_delay("deusStep", 1, runStamp) + debug("deusStart(): scheduled first step, done") +end + -- Enable DEM by setting a new cycle stamp and scheduling our first cycle step. function deusEnable() luup.log("DeusExMachinaII:deusEnable(): enabling, luup.device=" .. tostring(luup.device)) @@ -522,13 +532,15 @@ function deusEnable() setMessage("Enabling...") - luup.variable_set(SWITCH_SID, "Status", "1", luup.device) + -- Old Enabled variable follows SwitchPower1's Target luup.variable_set(SID, "Enabled", "1", luup.device) + luup.variable_set(SID, "State", STATE_IDLE, luup.device) - runStamp = os.time() + -- Launch the stepper. It will figure everything else out. + deusStart() - luup.call_delay("deusStep", 1, runStamp) - debug("deusEnable(): scheduled first step, done") + -- SwitchPower1 status now on, we are running. + luup.variable_set(SWITCH_SID, "Status", "1", luup.device) end -- Disable DEM and go to standby state. If we are currently cycling (as opposed to idle/waiting for sunset), @@ -539,9 +551,10 @@ function deusDisable() setMessage("Disabling...") - -- Destroy runStamp so any thread running while we shut down just exits. + -- Destroy runStamp so any running stepper exits when triggered (delay expires). runStamp = 0 + -- If current state is cycling or shutting down, rush that function (turn everything off) local s = getVarNumeric("State", STATE_STANDBY) if ( s == STATE_CYCLE or s == STATE_SHUTDOWN ) then clearLights() @@ -576,11 +589,7 @@ function deusInit(pdev) checkVersion() -- Start up if we're enabled - if (isEnabled()) then - deusEnable() - else - deusDisable() - end + deusStart() end -- Run a cycle. If we're in "bedtime" (i.e. not between our cycle period between sunset and stop), @@ -595,15 +604,17 @@ function deusStep(stepStampCheck) luup.log("DeusExMachinaII:deusStep(): stamp mismatch, another thread running. Bye!") return end - if (not isEnabled()) then + if not isEnabled() then + -- Not enabled, so force standby and stop what we're doing. luup.log("DeusExMachinaII:deusStep(): not enabled, no more work for this thread...") + luup.variable_set(SID, "State", STATE_STANDBY, luup.device) return end -- Get next sunset as seconds since midnight (approx) local sunset = getSunset() - local currentState = getVarNumeric("State", 0) + local currentState = getVarNumeric("State", STATE_STANDBY) if (currentState == STATE_STANDBY or currentState == STATE_IDLE) then debug("deusStep(): step in state %1, lightsout=%2, sunset=%3, os.time=%4", currentState, luup.variable_get(SID, "LightsOut", luup.device), sunset, os.time()) @@ -619,33 +630,36 @@ function deusStep(stepStampCheck) -- Get going... local nextCycleDelay = 300 -- a default value to keep us out of hot water - if (currentState == STATE_STANDBY and not inActiveTimePeriod) then - -- Transition from STATE_STANDBY (i.e. we're enabling) in the inactive period. - -- Go to IDLE and delay for next sunset. - luup.log("DeusExMachinaII:deusStep(): transitioning to IDLE from STANDBY, waiting for next sunset...") - nextCycleDelay = sunset - os.time() + getRandomDelay("MinCycleDelay", "MaxCycleDelay") - luup.variable_set(SID, "State", STATE_IDLE, luup.device) - setMessage("Waiting for sunset " .. timeToString(os.date("*t", os.time() + nextCycleDelay))) - elseif (not isActiveHouseMode()) then + if currentState == STATE_IDLE and not inActiveTimePeriod then + -- IDLE and not in active time period, probably a Luup restart. Wait for active period. + luup.log("DeusExMachinaII:deusStep(): IDLE and waiting for next sunset...") + nextCycleDelay = sunset - os.time() + getRandomDelay("MinCycleDelay", "MaxCycleDelay") + luup.variable_set(SID, "State", STATE_IDLE, luup.device) + setMessage("Waiting for sunset " .. timeToString(os.date("*t", os.time() + nextCycleDelay))) + elseif not isActiveHouseMode() then -- Not in an active house mode. If we're not STANDBY or IDLE, turn everything back off and go to IDLE. - if (currentState ~= STATE_IDLE) then - luup.log("DeusExMachinaII:deusStep(): transitioning to IDLE, not in an active house mode.") - if (currentState ~= STATE_STANDBY) then clearLights() end -- turn off lights quickly unless transitioning from STANDBY - luup.variable_set(SID, "State", STATE_IDLE, luup.device) + if currentState == STATE_IDLE then + luup.log("DeusExMachinaII:deusStep(): IDLE in active period but inactive house mode; waiting for mode change.") else - luup.log("DeusExMachinaII:deusStep(): IDLE in an inactive house mode; waiting for mode change.") + luup.log("DeusExMachinaII:deusStep(): transitioning to IDLE, not in an active house mode.") + if currentState ~= STATE_STANDBY then + clearLights() + luup.variable_set(SID, "State", STATE_IDLE, luup.device) + end end -- Figure out how long to delay. If we're lights-out, delay to next sunset. Otherwise, short delay -- to re-check house mode, which could change at any time, so we must deal with it. if (inActiveTimePeriod) then nextCycleDelay = getRandomDelay("MinCycleDelay", "MaxCycleDelay") + setMessage("Waiting for active house mode") else nextCycleDelay = sunset - os.time() + getRandomDelay("MinCycleDelay", "MaxCycleDelay") + setMessage("Idle until " .. timeToString(os.date("*t", os.time() + nextCycleDelay))) end - setMessage("Waiting for active house mode") - elseif (not inActiveTimePeriod) then - luup.log("DeusExMachinaII:deusStep(): running off cycle") + elseif not inActiveTimePeriod then + -- Not in active period, but in active house mode and running state; shut things off... + luup.log("DeusExMachinaII:deusStep(): running off cycle, state=" .. tostring(currentState)) luup.variable_set(SID, "State", STATE_SHUTDOWN, luup.device) if (not turnOffLight()) then -- No more lights to turn off @@ -653,14 +667,14 @@ function deusStep(stepStampCheck) luup.variable_set(SID, "State", STATE_IDLE, luup.device) nextCycleDelay = sunset - os.time() + getRandomDelay("MinCycleDelay", "MaxCycleDelay") luup.log("DeusExMachina:deusStep(): all lights out; now IDLE, setting delay to restart cycling at next sunset") - setMessage("Waiting for sunset " .. timeToString(os.date("*t", os.time() + nextCycleDelay))) + setMessage("Lights-out complete; waiting for sunset " .. timeToString(os.date("*t", os.time() + nextCycleDelay))) else nextCycleDelay = getRandomDelay("MinOffDelay", "MaxOffDelay", 60, 300) setMessage("Shut-off cycle, next " .. timeToString(os.date("*t", os.time() + nextCycleDelay))) end else -- Fully active. Find a random target to control and control it. - luup.log("DeusExMachinaII:deusStep(): running toggle cycle") + luup.log("DeusExMachinaII:deusStep(): running toggle cycle, state=" .. tostring(currentState)) luup.variable_set(SID, "State", STATE_CYCLE, luup.device) nextCycleDelay = getRandomDelay("MinCycleDelay", "MaxCycleDelay") local devs, max From 46d198f34ec7403a003f0bc590154a24bb97962a Mon Sep 17 00:00:00 2001 From: Patrick Rigney Date: Sun, 15 Jan 2017 09:08:50 -0500 Subject: [PATCH 49/49] getVersion() now returns two values, make sure we only pass the one we care about --- I_DeusExMachinaII1.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/I_DeusExMachinaII1.xml b/I_DeusExMachinaII1.xml index 235d189..4f4eda4 100644 --- a/I_DeusExMachinaII1.xml +++ b/I_DeusExMachinaII1.xml @@ -62,7 +62,8 @@ -- Ugly hack. Luup seems to only be able to return values from related state variables (see S_), so use a temp -- one to store the result we want to pass back. Blech. C'mon guys. Amateur hour. Add an extra return argument -- for a table of return values or something, please? - luup.variable_set("urn:toggledbits-com:serviceId:DeusExMachinaII1", "TempStorage", demII.getVersion(), lul_device) + local vs, vn = demII.getVersion() + luup.variable_set("urn:toggledbits-com:serviceId:DeusExMachinaII1", "TempStorage", vs, lul_device) return true