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]