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:DeusExMachinaII1S_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 7bb8eb8..3792828 100644
--- a/D_DeusExMachinaII1_UI7.json
+++ b/D_DeusExMachinaII1_UI7.json
@@ -1,243 +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:toggledbits-com:serviceId:DeusExMachinaII1",
- "variable": "Enabled",
- "operator": "==",
- "value": 0,
- "subcategory_num": 0
- }]
- }, {
- "img": "https://dtabq7xg0g1t1.cloudfront.net/deus-green.png",
- "conditions": [{
- "service": "urn:toggledbits-com:serviceId:DeusExMachinaII1",
- "variable": "Enabled",
- "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": "dem_enable",
- "text": "Enabled"
- },
- "ControlGroup": 1,
- "Display": {
- "Service": "urn:toggledbits-com:serviceId:DeusExMachinaII1",
- "Variable": "Enabled",
- "Value": "1"
- },
- "Command": {
- "Service": "urn:toggledbits-com:serviceId:DeusExMachinaII1",
- "Action": "SetEnabled",
- "Parameters": [{
- "Name": "NewEnabledValue",
- "Value": "1"
- }]
- },
- "ControlCode": "dem_enable"
- }, {
- "Label": {
- "lang_tag": "dem_disable",
- "text": "Disabled"
- },
- "ControlGroup": 1,
- "Display": {
- "Service": "urn:toggledbits-com:serviceId:DeusExMachinaII1",
- "Variable": "Enabled",
- "Value": "0"
- },
- "Command": {
- "Service": "urn:toggledbits-com:serviceId:DeusExMachinaII1",
- "Action": "SetEnabled",
- "Parameters": [{
- "Name": "NewEnabledValue",
- "Value": "0"
- }]
- },
- "ControlCode": "dem_disable"
- }],
- "ControlCode": "dem_statecontrol"
- }, {
- "ControlType": "label",
- "top": "0",
- "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."
- },
- "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
+ }
+ ]
+ }
+ ],
+ "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": "2",
- "TabType": "javascript",
- "ScriptName": "J_DeusExMachinaII1_UI7.js",
- "Function": "DeusExMachinaII.configureDeus"
- }, {
- "Label": {
- "lang_tag": "advanced",
- "text": "Advanced"
- },
- "Position": "3",
- "TabType": "javascript",
- "ScriptName": "shared.js",
- "Function": "advanced_device"
- }],
- "sceneList": {
- "group_1": {
- "cmd_1": {
- "label": "ON",
- "serviceId": "urn:toggledbits-com:serviceId:DeusExMachinaII1",
- "action": "SetEnabled",
- "arguments": {
- "NewEnabledValue": "1"
- },
- "display": {
- "service": "urn:toggledbits-com:serviceId:DeusExMachinaII1",
- "variable": "Enabled",
- "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-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",
+ "Left":"0",
+ "Width":"200",
+ "Height":"24"
+ }
},
- "cmd_2": {
- "label": "OFF",
- "serviceId": "urn:toggledbits-com:serviceId:DeusExMachinaII1",
- "action": "SetEnabled",
- "arguments": {
- "NewEnabledValue": "0"
- },
- "display": {
- "service": "urn:toggledbits-com:serviceId:DeusExMachinaII1",
- "variable": "Enabled",
- "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": "The device is enabled or disabled"
- },
- "serviceId": "urn:toggledbits-com:serviceId:DeusExMachinaII1",
- "argumentList": [{
- "id": 1,
- "dataType": "boolean",
- "defaultValue": "0",
- "allowedValueList": [{
- "value": "0",
- "HumanFriendlyText": {
- "lang_tag": "dem_disabled",
- "text": "_DEVICE_NAME_ is disabled"
- }
- }, {
- "value": "1",
- "HumanFriendlyText": {
- "lang_tag": "dem_enabled",
- "text": "_DEVICE_NAME_ is enabled"
- }
- }],
- "name": "Enabled",
- "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": "_DEVICE_NAME_ mode is standby"
- }
- }, {
- "value": "1",
- "HumanFriendlyText": {
- "lang_tag": "dem_ready",
- "text": "_DEVICE_NAME_ mode is ready"
- }
- }, {
- "value": "2",
- "HumanFriendlyText": {
- "lang_tag": "dem_cycle",
- "text": "_DEVICE_NAME_ mode is cycling"
- }
- }, {
- "value": "3",
- "HumanFriendlyText": {
- "lang_tag": "dem_shutoff",
- "text": "_DEVICE_NAME_ mode is shut-off"
- }
- }],
- "name": "State",
- "comparisson": "=",
- "prefix": {
- "lang_tag": "dem_when",
- "text": "When"
+ "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
diff --git a/I_DeusExMachinaII1.xml b/I_DeusExMachinaII1.xml
index fc55c05..4f4eda4 100644
--- a/I_DeusExMachinaII1.xml
+++ b/I_DeusExMachinaII1.xml
@@ -7,440 +7,65 @@
-- 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 = 20300
-
- 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 Enabled variable
- local function isEnabled()
- return getVarNumeric("Enabled", 0)
- 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.
- 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
- 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
- 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
- date = os.date('*t')
- local time = date['hour'] * 60 + date['min']
- 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;
- 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
-
- -- 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 ""
- local t = {}
- local k = 1
- local n = 0
- repeat
- local i = string.find(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 = i + 1
- end
- until k > string.len(s)
- return t, n
- 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)
- else
- luup.log("DeusExMachinaII::isDeviceOn(): device " .. tostring(devid) .. " unknown device_type " .. tostring(t))
- end
- return val ~= "0"
- end
-
- -- Control device
- 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)
- 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()
- local devs, max
- devs, max = getDeviceList()
- if (max > 0) then
- 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)
- 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.")
- if (n > 1) then
- return 1
- end
- end
- end
- return 0
- 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, "Enabled", lul_device)
- if (s == nil) then
- luup.log("DeusExMachinaII::runOnce(): Enabled variable not found, running...")
- -- See if there are variables from older version of DEM
- -- Start by finding the old Deus device, if there is one...
- 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
- s = table.concat(t, ",")
- 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)
- 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, "Enabled", "0", lul_device)
- luup.variable_set(SID, "Version", DEMVERSION, lul_device)
- 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)
- s = luup.variable_get(SID, "LightsOut", lul_device)
- if (s == nil) then
- s = getVarNumeric("LightsOutTime") -- get pre-2.2 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)
-
- -- 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...")
- runStamp = os.time()
- deusStep(runStamp)
- 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 state = 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(tonumber(devs[count],10), false)
- count = count - 1
- end
- 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
- 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(): another thread running, skipping")
- return
- end
- if (isEnabled() ~= 1) then
- luup.log("DeusExMachinaII::deusStep(): not enabled, skipping")
- 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)
-
- local currentState = getVarNumeric("State", 0)
- if (currentState == 0 or currentState == 1) 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())
- .. ", os.time=" .. tostring(os.time())
- )
- luup.log("+++ longitude="
- .. tostring(luup.longitude)
- .. ", latitude=" .. tostring(luup.latitude)
- .. ", timezone=" .. tostring(luup.timezone)
- .. ", city=" .. tostring(luup.city)
- .. ", 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
- end
-
- -- See if we've crossed the lights-out time
- if (runCycle == 0) 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
- 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
- end
- else
- luup.log("DeusExMachinaII::deusStep(): running toggle cycle")
- luup.variable_set(SID, "State", STATE_CYCLE, lul_device)
- mindelay = getVarNumeric("MinCycleDelay", 300)
- maxdelay = getVarNumeric("MaxCycleDelay", 1800)
- local devs, max
- 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")
- else
- deviceControl(deviceId, true)
- luup.log("DeusExMachinaII::deusStep(): set " .. deviceId .. " to ON")
- 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")
+ function startupDeusExMachinaII1(deusDevice)
+ luup.log("DeusExMachinaII STARTUP!")
+ demII = require("L_DeusExMachinaII1")
+ deusStep = demII.deusStep
+ demII.deusInit(deusDevice)
end
- deusInit
+ startupDeusExMachinaII1
+
+ urn:upnp-org:serviceId:SwitchPower1
+ SetTarget
+
+ local newTargetValue = lul_settings.newTargetValue or "0"
+ if (newTargetValue == "1") then
+ demII.deusEnable(lul_device)
+ else
+ demII.deusDisable(lul_device)
+ 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:DeusExMachinaII1SetEnabled
-
- local newEnabledValue = lul_settings.NewEnabledValue
+
+ local newEnabledValue = lul_settings.NewEnabledValue or "0"
+ luup.variable_set("urn:upnp-org:serviceId:SwitchPower1", "Target", newEnabledValue, lul_device)
if (newEnabledValue == "1") then
- luup.variable_set(SID, "Enabled", 1, lul_device)
- deusEnable()
+ demII.deusEnable(lul_device)
else
- luup.variable_set(SID, "Enabled", 0, lul_device)
- deusDisable()
+ demII.deusDisable(lul_device)
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?
+ local vs, vn = demII.getVersion()
+ luup.variable_set("urn:toggledbits-com:serviceId:DeusExMachinaII1", "TempStorage", vs, lul_device)
+ return true
+
diff --git a/J_DeusExMachinaII1_UI7.js b/J_DeusExMachinaII1_UI7.js
index 487d57a..ae5af2c 100644
--- a/J_DeusExMachinaII1_UI7.js
+++ b/J_DeusExMachinaII1_UI7.js
@@ -8,36 +8,111 @@ 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');
+ // console.log('handler for before cpanel close');
}
function init() {
api.registerEventHandler('on_ui_cpanel_before_close', myModule, 'onBeforeCpanelClose');
}
- 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) {
+ 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 */
+ v = api.getDeviceState( devid, "urn:upnp-org:serviceId:SwitchPower1", "Status" );
+ if (v === undefined || v === false) return false;
+ return true;
}
- 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 +130,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 )
@@ -93,6 +233,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()
@@ -101,23 +252,39 @@ 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 = [];
var noroom = { "id": "0", "name": "No Room", "devices": [] };
rooms[noroom.id] = noroom;
for (i=0; i
";
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 (isDimmer(roomObj.devices[i].id)) html += '';
html += " \n";
}
}
- html += "
";
+ html += ""; // devs
+
+ // Handle scene pairs
+ html += '
Scene Control
';
+ 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 += '
';
- // Finish up
+ // Maximum number of targets allowed to be "on" simultaneously
+ html += "
Maximum \"On\" Targets
";
+ html += " (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.';
+
+ // 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 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"));
for (var k=1; k<=4; ++k) {
- if (houseModes & (1<= 0) {
+ var info = DeusExMachinaII.getControlled(ix);
+ 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= 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", luup.device) or ""
+ if ui7Check == "" then
+ 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", 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, 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
+ 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("deleteVar(%1,%2) wget %3", name, devid, req)
+ local status, result = luup.inet.wget(req)
+ 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 "", luup.device)
+end
+
+-- Shortcut function to return state of SwitchPower1 Status variable
+local function isEnabled()
+ local s = luup.variable_get(SWITCH_SID, "Target", luup.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 return 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)
+ debug('getSunset(): testing mode sunset override %1, as timeval is %2', m, sunset)
+ end
+ 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')
+ debugMode = true
+ 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", 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
+ 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", luup.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", luup.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), luup.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", luup.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, ","), deusDevuce)
+ 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!")
+ removeTarget(targetid)
+ 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.). 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 (%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 (%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))
+ 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, luup.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", 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
+ -- 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, luup.device)
+ deleteVar("LightsOutTime", luup.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", luup.device)
+ end
+
+ -- Finally, turn off old Deus
+ luup.call_action(oldsid, "SetEnabled", { NewEnabledValue = "0" }, olddev)
+ end
+ luup.variable_set(SID, "Devices", devList, luup.device)
+
+ -- Set up some other default config
+ 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.
+ 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", luup.device)
+ if (s == nil) then
+ s = getVarNumeric("LightsOutTime") -- get pre-2.3 variable
+ if (s == nil) then
+ luup.variable_set(SID, "LightsOut", 1439, luup.device) -- default 23:59
+ else
+ luup.variable_set(SID, "LightsOut", tonumber(s,10) / 60000, luup.device) -- conv ms to minutes
+ end
+ end
+ deleteVar("LightsOutTime", luup.device)
+ end
+ if (s < 20400) then
+ -- 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, "Target", e, luup.device)
+ luup.variable_set(SWITCH_SID, "Status", 0, luup.device)
+ end
+
+ -- 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, 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))
+ luup.variable_set(SWITCH_SID, "Target", "1", luup.device)
+
+ setMessage("Enabling...")
+
+ -- Old Enabled variable follows SwitchPower1's Target
+ luup.variable_set(SID, "Enabled", "1", luup.device)
+ luup.variable_set(SID, "State", STATE_IDLE, luup.device)
+
+ -- Launch the stepper. It will figure everything else out.
+ deusStart()
+
+ -- 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),
+-- 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...")
+
+ -- 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()
+ end
+
+ 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(pdev)
+ luup.log("DeusExMachinaII:deusInit(" .. tostring(pdev) .. "): Version " .. _VERSION .. ", initializing, luup.device=" .. tostring(luup.device))
+
+ runStamp = 0
+
+ setMessage("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()
+
+ -- Check UI version
+ checkVersion()
+
+ -- Start up if we're enabled
+ deusStart()
+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 " .. tostring(stepStampCheck) .. ", luup.device=" .. tostring(luup.device))
+ if (stepStamp ~= runStamp) then
+ luup.log("DeusExMachinaII:deusStep(): stamp mismatch, another thread running. Bye!")
+ return
+ end
+ 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", 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())
+ 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
+ debug("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_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(): IDLE in active period but inactive house mode; waiting for mode change.")
+ else
+ 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
+ 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
+ runFinalScene()
+ 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("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, state=" .. tostring(currentState))
+ luup.variable_set(SID, "State", STATE_CYCLE, luup.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
+ 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
+
+ -- 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)
+ else
+ luup.log("DeusExMachinaII:deusStep(): nil nextCycleDelay, next cycle not scheduled!")
+ end
+end
diff --git a/README.md b/README.md
index 50e4dac..8cf3495 100644
--- a/README.md
+++ b/README.md
@@ -1,113 +1,225 @@
-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.
+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:
-* 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.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 -- 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).
+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.
-For information about modifications, fixes and enhancements, please see the Changelog.
+## How It Works ##
-### 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.
-### 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:
+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.
-* 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.
+Once you have installed the plugin and refreshed the browser, you can proceed to device configuation.
-* 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.
+### Simple Configuration ###
-Once you have installed the plugin and refreshed the browser, you can proceed to device configuation.
+Deus Ex Machina's "Configure" tab gives you a set of simple controls to control the behavior of your vacation haunt.
-#### Simple Configuration ####
+#### Lights-Out Time ####
-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.
+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.
-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.
+#### House Modes ####
-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).
+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.
-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).
+#### Controlled Devices ####
-#### Control by Scene ####
+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).
+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.0 and on UI7, DeusExMachina can be enabled or disabled like a light switch in scenes, through the regular graphical interface (no Lua required).
+> 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.
-A Lua interface is also supported since version 1.1 for both UI5 and UI7, via a luup.call_action() call:
+#### Scene Control ####
-```
--- For the new Deus Ex Machina II plugin (v2.0 and higher), do this:
-luup.call_action("urn:toggledbits-com:serviceId:DeusExMachinaII1", "SetEnabled", { NewEnabledValue = "0|1" }, deviceID)
+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
+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, as an alternative to direct control by DEMII.
--- 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)
-```
+Both scenes and individual devices (from the device list above) can be used simultaneously.
+
+#### Maximum "On" Targets ####
+
+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.
-Of course, only one of either "0" or "1" should be specified.
+#### Final Scene ####
-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_.
+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.
-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.
+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:
-#### Triggers ####
+```
+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),
+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:
-Version 2.0 on UI7 supports events (e.g. to trigger a scene or use with Program Logic Event Generator) for its state changes.
+```
+luup.call_action("urn:upnp-org:serviceId:SwitchPower1", "SetTarget", { newTargetValue = "0|1" }, pluginDeviceId)
+```
-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).
+### Triggers ###
-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);
-* 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).
-
-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 ####
+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.
+
+### 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 ###
+
+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 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 ####
+
+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 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 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 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 ##
+
+
+
My lights aren't cycling at sunset. Why?
+
The most common reasons that lights don't start cycling at midnight are:
+
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.
+ 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 just sit, even though it's enabled.
+
+
+
+
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. 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
+ 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
+ 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.
+ Second best is sending me a message via the MCV forums (I'm user `rigpapa`).
+
+
-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).
diff --git a/S_DeusExMachinaII1.xml b/S_DeusExMachinaII1.xml
index 2125a2d..59b8d20 100644
--- a/S_DeusExMachinaII1.xml
+++ b/S_DeusExMachinaII1.xml
@@ -40,10 +40,22 @@
SetEnabled
- NewEnabledValue
- Enabled
- in
+
+ NewEnabledValue
+ Enabled
+ in
+
+
+
+
+ GetPluginVersion
+
+
+ ResultVersion
+ out
+ TempStorage
+
-
+
\ No newline at end of file