Vacation ghosts

Hi,

Here is an alternative to the vacation ghosts plugin, implemented within a LUUP scene.

Prerequisite : create a Virtual Switch (plugin) to enable/disable ghosts.

How it works: when the Virtual Switch is ON, the scene will generate and schedule ghosts according to specified rules.
Ghosts will turn lights ON/OFF according to the specifications given in the scene: for each lights group (e.g., room), start & end times are given (actually, for each time two are given : a min & a max, with a random time being chosen in between); and if you want multiple ON/OFF cycles, specify ON & OFF cycles duration (also random between min & max values).

Other points :

  • If a ghost wants to turn a light ON but if it’s already ON, it won’t touch it (neither will it turn it OFF later)
  • If the Virtual Switch is turned OFF while running, non started ghosts will be stopped
  • If the Virtual Switch is turned ON again, all current ghosts will be stopped. New ones will be regenerated & rescheduled.
  • I’m notified on my phone when ghosts are scheduled + get the full detail by email

Example of generated ghosts :

[tt]__living_room #1 17:09:30 => 22:40:35 (05:31:05)
ceiling_room1 #1 17:22:14 => 18:10:12 (00:47:58)
bedside_room2 #1 17:32:59 => 22:23:00 (04:50:01)
bedside_room1 #1 17:43:56 => 18:59:48 (01:15:52)
ceiling_room1 #2 18:28:22 => 19:27:02 (00:58:40)
bedside_room1 #2 19:21:22 => 20:42:21 (01:20:59)
[/tt]

Create the following scene and specify:

  • Virtual Switch device ID
  • set of rules that gives (per lights group, ie, per room):
    . switch/dimmer IDs (and dimming level for dimmers)
    . timing. I specify start times around calculated sunset & end times absolutely

Start the scene at Vera startup + 1h before sunset + when the Virtual Switch is turned ON.

[tt]local ghost_virtual_switch_device = 42
local dry_run = false

local sunset_time = os.date(“*t”, luup.sunset())
local sunset = sunset_time.hour * 60 + sunset_time.min
luup.log(“Sunset is at " … string.format(”%02i:%02i:%02i", sunset_time.hour, sunset_time.min, sunset_time.sec))

local ghost_rules = {
living_room = {
switch_devices = { 10, 11 },
dimmer_devices = { 12 },
dimmer_level = 33, – 33%
start_min = sunset - 10, start_max = sunset + 15, – start between t-10 and t+15 minutes
end_min = 2260, end_max = 2360 – end between 10pm & 11pm
},
bedside_room1 = {
switch_devices = { 20 },
start_min = sunset, start_max = sunset + 30, – start between t and t+30 minutes
end_min = 20.560, end_max = 2160, – end between 8:30pm and 9:00pm
cycles = true,
cycles_on_min = 30, cycles_on_max = 3 * 60, – each cycle should leave light ON for 30 to 3 hours
cycles_off_min = 5, cycles_off_max = 30 – each cycle should leave light OFF for 5 to 30 minutes
},
ceiling_room1 = {
switch_devices = { 21 },
start_min = sunset, start_max = sunset + 30, – start between t and t+30 minutes
end_min = 20.560, end_max = 21.560, – end between 8:30pm and 9:30pm
cycles = true,
cycles_on_min = 30, cycles_on_max = 60, – each cycle should leave light ON for 30 to 1 hour
cycles_off_min = 15, cycles_off_max = 60 – each cycle should leave light OFF for 15 to 60 minutes
},
bedside_room2 = {
switch_devices = { 28 },
start_min = sunset, start_max = sunset + 30, – start between t and t+30 minutes
end_min = 2160, end_max = 2360, – end between 9:00pm and 11:00pm
}
}

function ghostDeviceIdToName(device_id)
return luup.devices[device_id].description … " " … luup.rooms[luup.devices[device_id].room_num] … " (#" … device_id … “)”
end

function generateGhosts()
– Reinitialize ghost list
if ghosts then
terminateAllGhosts() – terminate all current ghosts if any
end
ghosts = {}
sorted_ghosts = {}

-- Calc ghosts start/end
luup.log("Generating ghosts...")
math.randomseed(os.time())
for rule_name, rule in pairs(ghost_rules) do
    local starts = math.random(rule['start_min'] * 60, rule['start_max'] * 60)
    local ends = math.random(rule['end_min'] * 60, rule['end_max'] * 60)

    local ghost_i = 1
    local ghost_starts = starts
    local ghost_ends = ends
    while ghost_starts < ends do
        local ghost_name = rule_name .. " #" .. ghost_i
        if rule['cycles'] then
            ghost_ends = math.min(ghost_starts + math.random(rule['cycles_on_min'] * 60, rule['cycles_on_max'] * 60), ends)
        end
        ghosts["" .. ghost_starts .. "/" .. ghost_name] = { name = ghost_name, rule_name = rule_name, starts = ghost_starts, duration = ghost_ends - ghost_starts }
        if rule['cycles'] then
            ghost_starts = ghost_ends + math.random(rule['cycles_off_min'] * 60, rule['cycles_off_max'] * 60)
        else
            ghost_starts = ghost_ends
        end
        ghost_i = ghost_i + 1
    end
end

-- Sort ghosts by start time
for ghost_id in pairs(ghosts) do
    table.insert(sorted_ghosts, ghost_id)
end
table.sort(sorted_ghosts)

end

function wakeGhost(ghost_id)
if (luup.variable_get(“urn:upnp-org:serviceId:VSwitch1”, “Status”, ghost_virtual_switch_device) == “0”) then
luup.log("Not waking up ghost " … ghost_id … “, virtual switch has been turned OFF”)
else
local ghost = ghosts[ghost_id]
if ghost then
luup.log(“Waking up ghost " … ghost_id)
rule = ghost_rules[ghost[‘rule_name’]]
if rule[‘switch_devices’] then
for i, device_id in ipairs(rule[‘switch_devices’]) do
if luup.variable_get(“urn:upnp-org:serviceId:SwitchPower1”, “Status”, device_id) == “1” then
luup.log(“Ghost " … ghost_id … " is NOT switching " … ghostDeviceIdToName(device_id) … " ON, it’s already ON!”)
else
luup.log(“Ghost " … ghost_id … " is switching " … ghostDeviceIdToName(device_id) … " ON”)
if not ghost[‘switch_devices_to_turn_off’] then
ghost[‘switch_devices_to_turn_off’] = {}
end
table.insert(ghost[‘switch_devices_to_turn_off’], device_id)
if not dry_run then
luup.call_action(“urn:upnp-org:serviceId:SwitchPower1”, “SetTarget”, { newTargetValue = “1” }, device_id)
end
end
end
end
if rule[‘dimmer_devices’] then
for i, device_id in ipairs(rule[‘dimmer_devices’]) do
if luup.variable_get(“urn:upnp-org:serviceId:SwitchPower1”, “Status”, device_id) == “1” then
luup.log(“Ghost " … ghost_id … " is NOT switching " … ghostDeviceIdToName(device_id) … " ON, it’s already ON!”)
else
local dimmer_level = rule[‘dimmer_level’] or 25
luup.log(“Ghost " … ghost_id … " is switching " … ghostDeviceIdToName(device_id) … " ON to " … dimmer_level …”%”)
if not ghost[‘dimmer_devices_to_turn_off’] then
ghost[‘dimmer_devices_to_turn_off’] = {}
end
table.insert(ghost[‘dimmer_devices_to_turn_off’], device_id)
if not dry_run then
luup.call_action(“urn:upnp-org:serviceId:Dimming1”, “SetLoadLevelTarget”, { newLoadlevelTarget = “” … dimmer_level }, device_id)
end
end
end
end
luup.log(“Schedule ghost termination in " … ghost[‘duration’] … " seconds”)
luup.call_delay(“terminateGhost”, ghost[‘duration’], ghost_id)
else
luup.log(“Ghost " … ghost_id … " already killed, do nothing”)
end
end
end

function terminateAllGhosts()
for i, ghost_id in ipairs(sorted_ghosts) do
ghost = ghosts[ghost_id]
terminateGhost(ghost_id)
end
end

function terminateGhost(ghost_id)
local ghost = ghosts[ghost_id]
if ghost then
luup.log("Terminating ghost " … ghost_id)
if ghost[‘switch_devices_to_turn_off’] then
for i, device_id in ipairs(ghost[‘switch_devices_to_turn_off’]) do
luup.log(“Ghost " … ghost_id … " is switching " … ghostDeviceIdToName(device_id) … " OFF”)
if not dry_run then
luup.call_action(“urn:upnp-org:serviceId:SwitchPower1”, “SetTarget”, { newTargetValue = “0” }, device_id)
end
end
end
if ghost[‘dimmer_devices_to_turn_off’] then
for i, device_id in ipairs(ghost[‘dimmer_devices_to_turn_off’]) do
luup.log(“Ghost " … ghost_id … " is switching " … ghostDeviceIdToName(device_id) … " OFF”)
if not dry_run then
luup.call_action(“urn:upnp-org:serviceId:SwitchPower1”, “SetTarget”, { newTargetValue = “0” }, device_id)
end
end
end
else
luup.log(“Ghost " … ghost_id … " already terminated”)
end
end

function scheduleGhosts()
local current_time = os.date(“*t”, os.time())
local seconds_from_midnight = current_time.hour * 3600 + current_time.min * 60 + current_time.sec

luup.log("Scheduling ghosts...")
local report = ""
local ghosts_count = 0
local scheduled_ghosts_count = 0
for i, ghost_id in ipairs(sorted_ghosts) do
    ghost = ghosts[ghost_id]

    ghosts_count = ghosts_count + 1

    local starts_in = ghost['starts'] - seconds_from_midnight
    local ends_in = starts_in + ghost['duration']
    local status = ""
    if ends_in < 60 then
        status = "skipped, too late"
    elseif starts_in < 0 then
        status = "started " .. -starts_in .. " seconds ago, run now"
        scheduled_ghosts_count = scheduled_ghosts_count + 1
        wakeGhost(ghost_id)
    else
        status = "scheduled"
        scheduled_ghosts_count = scheduled_ghosts_count + 1
        luup.call_delay("wakeGhost", starts_in, ghost_id)
    end

    -- report
    local start_h = math.floor(ghost['starts'] / 3600)
    local start_m = math.floor((ghost['starts'] - (start_h * 3600)) / 60)
    local start_s = ghost['starts'] - (start_h * 3600) - (start_m * 60)
    local start_str = string.format("%02i:%02i:%02i", start_h, start_m, start_s)
    local duration_h = math.floor(ghost['duration'] / 3600)
    local duration_m = math.floor((ghost['duration'] - (duration_h * 3600)) / 60)
    local duration_s = ghost['duration'] - (duration_h * 3600) - (duration_m * 60)
    local duration_str = string.format("%02i:%02i:%02i", duration_h, duration_m, duration_s)
    local ends = ghost['starts'] + ghost['duration']
    local end_h = math.floor(ends / 3600)
    local end_m = math.floor((ends - (end_h * 3600)) / 60)
    local end_s = ends - (end_h * 3600) - (end_m * 60)
    local end_str = string.format("%02i:%02i:%02i", end_h, end_m, end_s)

    local report_line = string.format("%25s %s => %s (%s): %s", ghost['name'], start_str, end_str, duration_str, status)
    luup.log(report_line)
    report = report .. report_line .. "\n"
end
local alert_title = "" .. scheduled_ghosts_count .. "/" .. ghosts_count .. " scheduled ghosts"
-- <notification code goes here, can use alert_title & report variables> --

end

if (luup.variable_get(“urn:upnp-org:serviceId:VSwitch1”, “Status”, ghost_virtual_switch_device) == “1”) then
generateGhosts()
luup.call_delay(‘scheduleGhosts’, 10)
else
luup.log(“Not generating any ghost, virtual switch is OFF”)
end[/tt]

Apologies for bumping an old thread, but I am looking for something to flip the light switches when I am away from the house for extended periods of time. While I understand chixxi’s frustration, I wish I was able to get a copy of his app before he pulled it.

Anyway, my veralite is on UI5 and I am trying to modify cedricm’s code to work in my house. I created a virtual switch to signify when I want the code to run and the corresponding schedule and trigger but can’t get it to flip the actual light switch.

Here are the modifications I made:

-changed the id of the virtual switch device

-reduced the number of rooms and changed the labels to match my house in the “ghost_rules” section
-changed the switch_devices number in the {} to point to the device number listed on my vera in the “ghost_rules” section

[quote=“cedricm, post:1, topic:177691”]local ghost_rules = {
living_room = {
switch_devices = { 10, 11 },

dimmer_devices = { 12 },
dimmer_level = 33, – 33%
start_min = sunset - 10, start_max = sunset + 15, – start between t-10 and t+15 minutes
end_min = 2260, end_max = 2360 – end between 10pm & 11pm
},[/quote]

-changed the name of the virtual switch in the code that turns the “ghost” on in the “wakeGhost” function

[quote=“cedricm, post:1, topic:177691”]function wakeGhost(ghost_id)
if (luup.variable_get(“urn:upnp-org:serviceId:VSwitch1”, “Status”, ghost_virtual_switch_device) == “0”) then
luup.log("Not waking up ghost " … ghost_id … “, virtual switch has been turned OFF”)[/quote]

Is there anything else that I missed?

I can post logs later when I get home.

Thanks.

Hi,

First of all, let me share the latest version I’m using here.
IIRC, minor bugs where fixed.
I also use two different virtual switches. One to enable/disable the feature, and another to let the Vera know when somebody is home or not (I also use that one for other stuff). This means that the ghosts will be generated only when nobody is home AND if the feature is enabled. If you prefer to have only one virtual switch, it should be quite easy to adapt.
You also get a report of last generated/scheduled ghosts in /tmp/ghosts.txt.

-- This scene
-- . needs a virtual switch to know when somebody is home
-- . needs a virtual switch to enable/disable simulation when nobody is home
-- . is triggered 1 hour before sunset, at Vera startup and when virtual switch is turned ON

local at_home_virtual_switch_device = 122
local ghost_virtual_switch_device = 42
local dry_run = false

local sunset_time = os.date("*t", luup.sunset())
local sunset = sunset_time.hour * 60 + sunset_time.min
luup.log("Sunset is at " .. string.format("%02i:%02i:%02i", sunset_time.hour, sunset_time.min, sunset_time.sec))

local offset_times = 0

local ghost_rules = {
    living_room = {
        switch_devices = { 10, 11 },
        dimmer_devices = { 12 },
        dimmer_level = 33, -- 33%
        start_min = sunset - 10, start_max = sunset + 15, -- start between t-10 and t+15 minutes
        end_min = 22*60, end_max = 23*60 -- end between 10pm & 11pm
    },
    bedside_room1 = {
        switch_devices = { 20 },
        start_min = sunset, start_max = sunset + 30, -- start between t and t+30 minutes
        end_min = 20.5*60, end_max = 21*60, -- end between 8:30pm and 9:00pm
        cycles = true,
        cycles_on_min = 30, cycles_on_max = 3 * 60, -- each cycle should leave light ON for 30 to 3 hours
        cycles_off_min = 5, cycles_off_max = 30 -- each cycle should leave light OFF for 5 to 30 minutes
    },
    ceiling_room1 = {
        switch_devices = { 21 },
        start_min = sunset, start_max = sunset + 30, -- start between t and t+30 minutes
        end_min = 20.5*60, end_max = 21.5*60, -- end between 8:30pm and 9:30pm
        cycles = true,
        cycles_on_min = 30, cycles_on_max = 60, -- each cycle should leave light ON for 30 to 1 hour
        cycles_off_min = 15, cycles_off_max = 60 -- each cycle should leave light OFF for 15 to 60 minutes
    },
    bedside_room2 = {
        switch_devices = { 28 },
        start_min = sunset, start_max = sunset + 30, -- start between t and t+30 minutes
        end_min = 21*60, end_max = 23*60, -- end between 9:00pm and 11:00pm
    }
}

function ghostDeviceIdToName(device_id)
    return luup.devices[device_id].description .. " " .. luup.rooms[luup.devices[device_id].room_num] .. " (#" .. device_id .. ")"
end

function generateGhosts()
    -- Reinitialize ghost list
    if ghosts then
        terminateAllGhosts() -- terminate all current ghosts if any
    end
    ghosts = {}
    sorted_ghosts = {}

    -- Calc ghosts start/end
    luup.log("Generating ghosts...")
    math.randomseed(os.time())
    for rule_name, rule in pairs(ghost_rules) do
        local starts = math.random(rule['start_min'] * 60, rule['start_max'] * 60) + (offset_times * 60)
        local ends = math.random(rule['end_min'] * 60, rule['end_max'] * 60) + (offset_times * 60)

        local ghost_i = 1
        local ghost_starts = starts
        local ghost_ends = ends
        while ghost_starts < ends do
            local ghost_name = rule_name .. " #" .. ghost_i
            if rule['cycles'] then
                ghost_ends = math.min(ghost_starts + math.random(rule['cycles_on_min'] * 60, rule['cycles_on_max'] * 60), ends)
            end
            ghosts["" .. ghost_starts .. "/" .. ghost_name] = { name = ghost_name, rule_name = rule_name, starts = ghost_starts, duration = ghost_ends - ghost_starts }
            if rule['cycles'] then
                ghost_starts = ghost_ends + math.random(rule['cycles_off_min'] * 60, rule['cycles_off_max'] * 60)
            else
                ghost_starts = ghost_ends
            end
            ghost_i = ghost_i + 1
        end
    end

    -- Sort ghosts by start time
    for ghost_id in pairs(ghosts) do
        table.insert(sorted_ghosts, ghost_id)
    end
    table.sort(sorted_ghosts)
end

function wakeGhost(ghost_id)
    if (luup.variable_get("urn:upnp-org:serviceId:VSwitch1", "Status", at_home_virtual_switch_device) == "1") then
        luup.log("Not waking up ghost " .. ghost_id .. ", somebody is home")
    else
        if (luup.variable_get("urn:upnp-org:serviceId:VSwitch1", "Status", ghost_virtual_switch_device) == "0") then
            luup.log("Not waking up ghost " .. ghost_id .. ", virtual switch has been turned OFF")
        else
            local ghost = ghosts[ghost_id]
            if ghost then
                luup.log("Waking up ghost " .. ghost_id)
                rule = ghost_rules[ghost['rule_name']]
                if rule['switch_devices'] then
                    for i, device_id in ipairs(rule['switch_devices']) do
                        if luup.variable_get("urn:upnp-org:serviceId:SwitchPower1", "Status", device_id) == "1" then
                            luup.log("Ghost " .. ghost_id .. " is NOT switching " .. ghostDeviceIdToName(device_id) .. " ON, it's already ON!")
                        else
                            luup.log("Ghost " .. ghost_id .. " is switching " .. ghostDeviceIdToName(device_id) .. " ON")
                            if not ghost['switch_devices_to_turn_off'] then
                                ghost['switch_devices_to_turn_off'] = {}
                            end
                            table.insert(ghost['switch_devices_to_turn_off'], device_id)
                            if not dry_run then
                                luup.call_action("urn:upnp-org:serviceId:SwitchPower1", "SetTarget", { newTargetValue = "1" }, device_id)
                            end
                        end
                    end
                end
                if rule['dimmer_devices'] then
                    for i, device_id in ipairs(rule['dimmer_devices']) do
                        if luup.variable_get("urn:upnp-org:serviceId:SwitchPower1", "Status", device_id) == "1" then
                            luup.log("Ghost " .. ghost_id .. " is NOT switching " .. ghostDeviceIdToName(device_id) .. " ON, it's already ON!")
                        else
                            local dimmer_level = rule['dimmer_level'] or 25
                            luup.log("Ghost " .. ghost_id .. " is switching " .. ghostDeviceIdToName(device_id) .. " ON to " .. dimmer_level .."%")
                            if not ghost['dimmer_devices_to_turn_off'] then
                                ghost['dimmer_devices_to_turn_off'] = {}
                            end
                            table.insert(ghost['dimmer_devices_to_turn_off'], device_id)
                            if not dry_run then
                                luup.call_action("urn:upnp-org:serviceId:Dimming1", "SetLoadLevelTarget", { newLoadlevelTarget = "" .. dimmer_level }, device_id)
                            end
                        end
                    end
                end
                luup.log("Schedule ghost termination in " .. ghost['duration'] .. " seconds")
                luup.call_delay("terminateGhost", ghost['duration'], ghost_id)
            else
                luup.log("Ghost " .. ghost_id .. " already killed, do nothing")
            end
        end
    end
end

function terminateAllGhosts()
    for i, ghost_id in ipairs(sorted_ghosts) do
        ghost = ghosts[ghost_id]
        terminateGhost(ghost_id)
    end
end

function terminateGhost(ghost_id)
    if (luup.variable_get("urn:upnp-org:serviceId:VSwitch1", "Status", at_home_virtual_switch_device) == "1") then
        luup.log("Not terminating ghost " .. ghost_id .. ", somebody is home")
    else
        local ghost = ghosts[ghost_id]
        if ghost then
            luup.log("Terminating ghost " .. ghost_id)
            if ghost['switch_devices_to_turn_off'] then
                for i, device_id in ipairs(ghost['switch_devices_to_turn_off']) do
                    luup.log("Ghost " .. ghost_id .. " is switching " .. ghostDeviceIdToName(device_id) .. " OFF")
                    if not dry_run then
                        luup.call_action("urn:upnp-org:serviceId:SwitchPower1", "SetTarget", { newTargetValue = "0" }, device_id)
                    end
                end
            end
            if ghost['dimmer_devices_to_turn_off'] then
                for i, device_id in ipairs(ghost['dimmer_devices_to_turn_off']) do
                    luup.log("Ghost " .. ghost_id .. " is switching " .. ghostDeviceIdToName(device_id) .. " OFF")
                    if not dry_run then
                        luup.call_action("urn:upnp-org:serviceId:SwitchPower1", "SetTarget", { newTargetValue = "0" }, device_id)
                    end
                end
            end
        else
            luup.log("Ghost " .. ghost_id .. " already terminated")
        end
    end
end

function scheduleGhosts()
    local current_time = os.date("*t", os.time())
    local seconds_from_midnight = current_time.hour * 3600 + current_time.min * 60 + current_time.sec

    luup.log("Scheduling ghosts...")
    local report = ""
    local ghosts_count = 0
    local scheduled_ghosts_count = 0
    for i, ghost_id in ipairs(sorted_ghosts) do
        ghost = ghosts[ghost_id]

        ghosts_count = ghosts_count + 1

        local starts_in = ghost['starts'] - seconds_from_midnight
        local ends_in = starts_in + ghost['duration']
        local status = ""
        if ends_in < 60 then
            status = "skipped, too late"
        elseif starts_in < 0 then
            status = "started " .. -starts_in .. " seconds ago, run now"
            ghost['duration'] = ghost['duration'] + starts_in
            scheduled_ghosts_count = scheduled_ghosts_count + 1
            wakeGhost(ghost_id)
        else
            status = "scheduled"
            scheduled_ghosts_count = scheduled_ghosts_count + 1
            luup.call_delay("wakeGhost", starts_in, ghost_id)
        end

        -- report
        local start_h = math.floor(ghost['starts'] / 3600)
        local start_m = math.floor((ghost['starts'] - (start_h * 3600)) / 60)
        local start_s = ghost['starts'] - (start_h * 3600) - (start_m * 60)
        local start_str = string.format("%02i:%02i:%02i", start_h, start_m, start_s)
        local duration_h = math.floor(ghost['duration'] / 3600)
        local duration_m = math.floor((ghost['duration'] - (duration_h * 3600)) / 60)
        local duration_s = ghost['duration'] - (duration_h * 3600) - (duration_m * 60)
        local duration_str = string.format("%02i:%02i:%02i", duration_h, duration_m, duration_s)
        local ends = ghost['starts'] + ghost['duration']
        local end_h = math.floor(ends / 3600)
        local end_m = math.floor((ends - (end_h * 3600)) / 60)
        local end_s = ends - (end_h * 3600) - (end_m * 60)
        local end_str = string.format("%02i:%02i:%02i", end_h, end_m, end_s)

        local report_line = string.format("%25s %s => %s (%s): %s", ghost['name'], start_str, end_str, duration_str, status)
        luup.log(report_line)
        report = report .. report_line .. "\n"
    end

    report_file = io.open('/tmp/ghosts.txt', 'w')
    report_file:write('Ghosts generated ' .. os.date("%x %X", os.time()) .. "\n\n")
    report_file:write(report)
end

if (luup.variable_get("urn:upnp-org:serviceId:VSwitch1", "Status", at_home_virtual_switch_device) == "0") then
    if (luup.variable_get("urn:upnp-org:serviceId:VSwitch1", "Status", ghost_virtual_switch_device) == "1") then
        generateGhosts()
        luup.call_delay('scheduleGhosts', 10)
    else
        luup.log("Not generating any ghost, virtual switch is OFF")
    end
else
    luup.log("Not generating any ghost, somebody is home")
end

Not sure where you are stuck. Are you sure that the scene is properly triggered when you turn your virtual switch on?

BTW, for testing purposes, you can set offset_times to a different value than 0. This will shift the actual time by this given amount of minutes, so that you can test the stuff at any time in the day without having to change your rules (eg, if it’s 2pm, setting offset_times to 6*60 will simulate what will happen at 8pm).

Feel free share some logs (including /tmp/ghosts.txt).

Thanks, cedricm. I was able to get it to work. The code enabled one ghost for me tonight. I must have fubar-ed the other code or something.

Is it possible, or did the code just not generate one this time, to have more than one cycle per room per day?


I went back to reviewed your earlier comments. I left the ‘cycles’ variable set to ‘true’ so I guess it could have created multiple cycles for the one room I tested with. In my test run, only one cycle was created. I will run it again for the next several days to see what gets created.

Hi,

Great, glad you got somewhere with this :slight_smile:

As you have probably figured out by now, you have two modes:

  1. without cycles, light goes ON randomly between start_min and start_max, and then OFF randomly between end_min and end_max. That’s the simple case that should be good enough most of the time. I’m using it on my living room (light goes ON around sunset, OFF around 11pm), as this is close to what we are doing when home.

  2. with cycles, light may go ON/OFF multiple times during that “start_min…start_max” to “end_min…end_max” period. The ON duration is random between cycles_on_min and cycles_on_max. Similar for the OFF duration. So, depending on the cycles_on/cycles_off lengths and complete start/end duration, you can end up with 0, 1 or many cycles. This was done to better simulate what happens in real life in my children bedrooms (light can goes ON/OFF multiple times during the evening, as children “randomly” enter or leave their rooms)

Best thing is indeed to write ghost rules while thinking of the usual behavior when home, and check that generated ghosts are indeed as you expected (check /tmp/ghosts.txt on my latest version).

Are there like instructions for newbies :slight_smile:

I am trying to create this but can’t figure out how to create the virtual switch in UI7. Also where do I place the code and how do I connect it to that virtual switch thereafter…

Hi RonLin,

Install the Virtual Switch plugin (can’t guide you through UI7 as I’m not using the Vera anymore), create two instances of it:

  • first one is a general simulation ON/OFF switch (ie, when OFF, ghosts will never run)
  • second one will be used as a “home/away” switch (ie, when home, ghosts won’t run either)

A single virtual switch instance should be enough if you don’t need the home/away switch for other things.

Then create a scene that will host the Lua/LUUP code. Copy/paste in the code in the scene (there’s a place for code in each scene).
Trigger the scene execution at Vera startup, when virtual switch(es) states changes, 1 hour before sunset.

Then adapt the code for your own use!

Or do this all in a separate PLEG …
PLEG has support for Random On/ Random Off timers.

Then just enable/disable the PLEG to turn it on when you are going into vacation mode, or if you are on UI7, let the Vacation House mode enable/disable the PLEG.

Hi Richard, I have been doing this with PLEG; How do you suggest to create the ‘cycles’ as mentioned in PLEG; e.g. bathroom should go on/off multiple times a evening/night. Do I need to create multiple schedules for each cycle or is there another way?

Thanks for the info!

So I’ve managed to create 2 virtual switches one for vacation_ghost and one for anyone_home. I’ve created a scene and added all info into it as per attachement. But I can’t get this to work…

My code:

[code]local at_home_virtual_switch_device = 1855
local ghost_virtual_switch_device = 1854
local dry_run = false

local sunset_time = os.date(“*t”, luup.sunset())
local sunset = sunset_time.hour * 60 + sunset_time.min
luup.log(“Sunset is at " … string.format(”%02i:%02i:%02i", sunset_time.hour, sunset_time.min, sunset_time.sec))

local offset_times = 0

local ghost_rules = {
bedroom_room = {
dimmer_devices = { 13 },
dimmer_level = 33, – 33%
start_min = sunset - 10, start_max = sunset + 15, – start between t-10 and t+15 minutes
end_min = 2160, end_max = 2360 – end between 10pm & 11pm
},
Movie_room = {
dimmer_devices = { 12 },
dimmer_level = 33, – 33%
start_min = sunset - 10, start_max = sunset + 15, – start between t-10 and t+15 minutes
end_min = 2160, end_max = 2360 – end between 10pm & 11pm
},
Kitchen_room = {
switch_devices = { 17, 18 },
start_min = sunset, start_max = sunset + 30, – start between t and t+30 minutes
end_min = 19.560, end_max = 2160, – end between 8:30pm and 9:00pm
cycles = true,
cycles_on_min = 30, cycles_on_max = 3 * 60, – each cycle should leave light ON for 30 to 3 hours
cycles_off_min = 5, cycles_off_max = 30 – each cycle should leave light OFF for 5 to 30 minutes
},
Office_room = {
switch_devices = { 6 },
start_min = sunset, start_max = sunset + 30, – start between t and t+30 minutes
end_min = 17.560, end_max = 2160, – end between 8:30pm and 9:30pm
cycles = true,
cycles_on_min = 30, cycles_on_max = 60, – each cycle should leave light ON for 30 to 1 hour
cycles_off_min = 15, cycles_off_max = 60 – each cycle should leave light OFF for 15 to 60 minutes
},
Corridor_Up = {
switch_devices = { 10 },
start_min = sunset, start_max = sunset + 30, – start between t and t+30 minutes
end_min = 19.560, end_max = 21.560, – end between 8:30pm and 9:30pm
cycles = true,
cycles_on_min = 30, cycles_on_max = 60, – each cycle should leave light ON for 30 to 1 hour
cycles_off_min = 15, cycles_off_max = 60 – each cycle should leave light OFF for 15 to 60 minutes
},
Corridor_Down = {
switch_devices = { 9 },
start_min = sunset, start_max = sunset + 30, – start between t and t+30 minutes
end_min = 19.560, end_max = 21.560, – end between 8:30pm and 9:30pm
cycles = true,
cycles_on_min = 30, cycles_on_max = 60, – each cycle should leave light ON for 30 to 1 hour
cycles_off_min = 15, cycles_off_max = 60 – each cycle should leave light OFF for 15 to 60 minutes
},
Living_room = {
switch_devices = { 7 },
start_min = sunset, start_max = sunset + 30, – start between t and t+30 minutes
end_min = 1960, end_max = 2360, – end between 9:00pm and 11:00pm
}

}

function ghostDeviceIdToName(device_id)
return luup.devices[device_id].description … " " … luup.rooms[luup.devices[device_id].room_num] … " (#" … device_id … “)”
end

function generateGhosts()
– Reinitialize ghost list
if ghosts then
terminateAllGhosts() – terminate all current ghosts if any
end
ghosts = {}
sorted_ghosts = {}

-- Calc ghosts start/end
luup.log("Generating ghosts...")
math.randomseed(os.time())
for rule_name, rule in pairs(ghost_rules) do
    local starts = math.random(rule['start_min'] * 60, rule['start_max'] * 60) + (offset_times * 60)
    local ends = math.random(rule['end_min'] * 60, rule['end_max'] * 60) + (offset_times * 60)

    local ghost_i = 1
    local ghost_starts = starts
    local ghost_ends = ends
    while ghost_starts < ends do
        local ghost_name = rule_name .. " #" .. ghost_i
        if rule['cycles'] then
            ghost_ends = math.min(ghost_starts + math.random(rule['cycles_on_min'] * 60, rule['cycles_on_max'] * 60), ends)
        end
        ghosts["" .. ghost_starts .. "/" .. ghost_name] = { name = ghost_name, rule_name = rule_name, starts = ghost_starts, duration = ghost_ends - ghost_starts }
        if rule['cycles'] then
            ghost_starts = ghost_ends + math.random(rule['cycles_off_min'] * 60, rule['cycles_off_max'] * 60)
        else
            ghost_starts = ghost_ends
        end
        ghost_i = ghost_i + 1
    end
end

-- Sort ghosts by start time
for ghost_id in pairs(ghosts) do
    table.insert(sorted_ghosts, ghost_id)
end
table.sort(sorted_ghosts)

end

function wakeGhost(ghost_id)
if (luup.variable_get(“urn:upnp-org:serviceId:VSwitch1”, “Status”, at_home_virtual_switch_device) == “1”) then
luup.log("Not waking up ghost " … ghost_id … “, somebody is home”)
else
if (luup.variable_get(“urn:upnp-org:serviceId:VSwitch1”, “Status”, ghost_virtual_switch_device) == “0”) then
luup.log("Not waking up ghost " … ghost_id … “, virtual switch has been turned OFF”)
else
local ghost = ghosts[ghost_id]
if ghost then
luup.log(“Waking up ghost " … ghost_id)
rule = ghost_rules[ghost[‘rule_name’]]
if rule[‘switch_devices’] then
for i, device_id in ipairs(rule[‘switch_devices’]) do
if luup.variable_get(“urn:upnp-org:serviceId:SwitchPower1”, “Status”, device_id) == “1” then
luup.log(“Ghost " … ghost_id … " is NOT switching " … ghostDeviceIdToName(device_id) … " ON, it’s already ON!”)
else
luup.log(“Ghost " … ghost_id … " is switching " … ghostDeviceIdToName(device_id) … " ON”)
if not ghost[‘switch_devices_to_turn_off’] then
ghost[‘switch_devices_to_turn_off’] = {}
end
table.insert(ghost[‘switch_devices_to_turn_off’], device_id)
if not dry_run then
luup.call_action(“urn:upnp-org:serviceId:SwitchPower1”, “SetTarget”, { newTargetValue = “1” }, device_id)
end
end
end
end
if rule[‘dimmer_devices’] then
for i, device_id in ipairs(rule[‘dimmer_devices’]) do
if luup.variable_get(“urn:upnp-org:serviceId:SwitchPower1”, “Status”, device_id) == “1” then
luup.log(“Ghost " … ghost_id … " is NOT switching " … ghostDeviceIdToName(device_id) … " ON, it’s already ON!”)
else
local dimmer_level = rule[‘dimmer_level’] or 25
luup.log(“Ghost " … ghost_id … " is switching " … ghostDeviceIdToName(device_id) … " ON to " … dimmer_level …”%”)
if not ghost[‘dimmer_devices_to_turn_off’] then
ghost[‘dimmer_devices_to_turn_off’] = {}
end
table.insert(ghost[‘dimmer_devices_to_turn_off’], device_id)
if not dry_run then
luup.call_action(“urn:upnp-org:serviceId:Dimming1”, “SetLoadLevelTarget”, { newLoadlevelTarget = “” … dimmer_level }, device_id)
end
end
end
end
luup.log(“Schedule ghost termination in " … ghost[‘duration’] … " seconds”)
luup.call_delay(“terminateGhost”, ghost[‘duration’], ghost_id)
else
luup.log(“Ghost " … ghost_id … " already killed, do nothing”)
end
end
end
end

function terminateAllGhosts()
for i, ghost_id in ipairs(sorted_ghosts) do
ghost = ghosts[ghost_id]
terminateGhost(ghost_id)
end
end

function terminateGhost(ghost_id)
if (luup.variable_get(“urn:upnp-org:serviceId:VSwitch1”, “Status”, at_home_virtual_switch_device) == “1”) then
luup.log("Not terminating ghost " … ghost_id … “, somebody is home”)
else
local ghost = ghosts[ghost_id]
if ghost then
luup.log("Terminating ghost " … ghost_id)
if ghost[‘switch_devices_to_turn_off’] then
for i, device_id in ipairs(ghost[‘switch_devices_to_turn_off’]) do
luup.log(“Ghost " … ghost_id … " is switching " … ghostDeviceIdToName(device_id) … " OFF”)
if not dry_run then
luup.call_action(“urn:upnp-org:serviceId:SwitchPower1”, “SetTarget”, { newTargetValue = “0” }, device_id)
end
end
end
if ghost[‘dimmer_devices_to_turn_off’] then
for i, device_id in ipairs(ghost[‘dimmer_devices_to_turn_off’]) do
luup.log(“Ghost " … ghost_id … " is switching " … ghostDeviceIdToName(device_id) … " OFF”)
if not dry_run then
luup.call_action(“urn:upnp-org:serviceId:SwitchPower1”, “SetTarget”, { newTargetValue = “0” }, device_id)
end
end
end
else
luup.log(“Ghost " … ghost_id … " already terminated”)
end
end
end

function scheduleGhosts()
local current_time = os.date(“*t”, os.time())
local seconds_from_midnight = current_time.hour * 3600 + current_time.min * 60 + current_time.sec

luup.log("Scheduling ghosts...")
local report = ""
local ghosts_count = 0
local scheduled_ghosts_count = 0
for i, ghost_id in ipairs(sorted_ghosts) do
    ghost = ghosts[ghost_id]

    ghosts_count = ghosts_count + 1

    local starts_in = ghost['starts'] - seconds_from_midnight
    local ends_in = starts_in + ghost['duration']
    local status = ""
    if ends_in < 60 then
        status = "skipped, too late"
    elseif starts_in < 0 then
        status = "started " .. -starts_in .. " seconds ago, run now"
        ghost['duration'] = ghost['duration'] + starts_in
        scheduled_ghosts_count = scheduled_ghosts_count + 1
        wakeGhost(ghost_id)
    else
        status = "scheduled"
        scheduled_ghosts_count = scheduled_ghosts_count + 1
        luup.call_delay("wakeGhost", starts_in, ghost_id)
    end

    -- report
    local start_h = math.floor(ghost['starts'] / 3600)
    local start_m = math.floor((ghost['starts'] - (start_h * 3600)) / 60)
    local start_s = ghost['starts'] - (start_h * 3600) - (start_m * 60)
    local start_str = string.format("%02i:%02i:%02i", start_h, start_m, start_s)
    local duration_h = math.floor(ghost['duration'] / 3600)
    local duration_m = math.floor((ghost['duration'] - (duration_h * 3600)) / 60)
    local duration_s = ghost['duration'] - (duration_h * 3600) - (duration_m * 60)
    local duration_str = string.format("%02i:%02i:%02i", duration_h, duration_m, duration_s)
    local ends = ghost['starts'] + ghost['duration']
    local end_h = math.floor(ends / 3600)
    local end_m = math.floor((ends - (end_h * 3600)) / 60)
    local end_s = ends - (end_h * 3600) - (end_m * 60)
    local end_str = string.format("%02i:%02i:%02i", end_h, end_m, end_s)

    local report_line = string.format("%25s %s => %s (%s): %s", ghost['name'], start_str, end_str, duration_str, status)
    luup.log(report_line)
    report = report .. report_line .. "\n"
end

report_file = io.open('/tmp/ghosts.txt', 'w')
report_file:write('Ghosts generated ' .. os.date("%x %X", os.time()) .. "\n\n")
report_file:write(report)

end

if (luup.variable_get(“urn:upnp-org:serviceId:VSwitch1”, “Status”, at_home_virtual_switch_device) == “0”) then
if (luup.variable_get(“urn:upnp-org:serviceId:VSwitch1”, “Status”, ghost_virtual_switch_device) == “1”) then
generateGhosts()
luup.call_delay(‘scheduleGhosts’, 10)
else
luup.log(“Not generating any ghost, virtual switch is OFF”)
end
else
luup.log(“Not generating any ghost, somebody is home”)
end[/code]

You can use PLEG counters.

On each cycle bump the counter … and only run while cnt < cntmax.
On sunset (or what ever you use to start each day) clear all of the counters.

@tyfoon per chance have you had any success with Richard’s suggestion(s) I’ve noticed that another thread has been opened relating to the same topic slightly different solution have you seen it? Mike

[url=http://forum.micasaverde.com/index.php/topic,37322.0.html]http://forum.micasaverde.com/index.php/topic,37322.0.html[/url]

@nmb nope, did not look into this yet. Not sure how to do this honestly.

Sent from my Nexus 6P using Tapatalk

[quote=“MNB, post:12, topic:177691”]@tyfoon per chance have you had any success with Richard’s suggestion(s) I’ve noticed that another thread has been opened relating to the same topic slightly different solution have you seen it? Mike
[url=http://forum.micasaverde.com/index.php/topic,37322.0.html]http://forum.micasaverde.com/index.php/topic,37322.0.html[/url][/quote]
Yeah, that’s mine. So far, the schedules are working as expected.

If I didn’t have PLEG, I would probably still be using the Vacation Ghost plugin. Since that’s no longer supported, I don’t know what I’d do otherwise. OP’s solution is a lotta Luup.

I’m doing this (emphasis mine).

@DeltaNu; did you also use Richards suggestion on the counters? I did a similar thing then you did but I would prefer some lights (for example toilet) to go on mutliple times per evening (relative random) but don’t want to create multiple schedules for all of these

I did not. I can (and I think I did) have lights go on multiple times just by using delays on the actions. My whole PLEG report is on that other post, so you can see what I did.

Keep this in mind… each schedule has an start time and (potentially) an end time. Each of those can satisfy a condition even if you only use one trigger per condition. So, two conditions per schedule means that at the start time, you can turn on some lights (with or without delays on the action) and then turn them off again (with or without delays). Then, at the end of the schedule, you can turn the same or other lights on, with or without delays.

What I’m trying to say is that you can accomplish a lot of light activity without a lot of schedules. My 6+1 is probably more than I need. It would be really easy to make the lights turn on & off so frequently that no one in their right mind would believe it could be the result of humans pressing on light switches. But, of course, the objective is just the opposite.

A completely different way to do this would be to have a condition for each light that turns off the light some number of minutes after it was turned on, and have schedules that each satisfy a condition that turns one or more lights on. I’m interested in the most efficient way… which I was hoping to get out of the thread I posted, but we’ll see what kind of feedback I get.