Re: Generic auto-off
Posted: Monday 27 January 2020 10:36
Unfortunately I need the script also for turning lights off, is it possible to have a command like "auto_level_minutes" next to the command "auto_off_minutes"?
Below code is not working yet. I get an error that I can't fix yet but I'm running out of time for the moment. Maybe @waaren can have a look at what I'm doing wrong? I've not even activated the new features yet; still using the original json values in the descriptions of my devices.
Code: Select all
2020-02-01 21:52:00.945 Error: dzVents: Error: (2.5.0) An error occurred when calling event handler Generic Auto Off
2020-02-01 21:52:00.945 Error: dzVents: Error: (2.5.0) ...z/scripts/dzVents/generated_scripts/Generic Auto Off.lua:105: attempt to compare number with table
Code: Select all
elseif motion_device.lastUpdate.compare(lastUpdate) > 0 then
Code: Select all
-- This script will run every minute and can automatically send an 'Off' command to turn off any device after
-- it has been on for some specified time. Each device can be individually configured by putting json coded
-- settings into the device's description field. The settings currently supported are:
-- - "auto_off_minutes" : <time in minutes>
-- - "auto_off_motion_device" : "<name of a motion detection device>"
-- If "auto_off_minutes" is not set, the device will never be turned off by this script. If
-- "auto_off_minutes" is set and <time in minutes> is a valid number, the device will be turned off when it
-- is found to be on plus the device's lastUpdate is at least <time in minutes> minutes old. This behavior
-- can be further modified by specifying a valid device name after "auto_off_motion_device". When a motion
-- device is specified and the device's lastUpdate is at least <time in minutes> old, the device will not
-- be turned off until the motion device is off and it's lastUpdate is also <time in minutes> old.
-- Specifying "auto_off_motion_device" without specifying "auto_off_minutes" does nothing.
--
-- Example 1: turn off the device after 2 minutes:
-- {
-- "auto_off_minutes": 2
-- }
--
-- Example 2: turn off the device when it has been on for 5 minutes and no motion has been detected for
-- at least 5 minutes:
-- {
-- "auto_off_minutes": 5,
-- "auto_off_motion_device": "Overloop: Motion"
-- }
--
-- Example 3: turn off the device when it has been on for 1 minute and not motion was detected for at least 1
-- minute on either one of a set of motion sensors.
--{
--"auto_off_minutes": 1,
--"auto_off_motion_device": ["Overloop 1: Motion 1", "Overloop 1: Motion 2"]
--}
return {
on = {
-- timer triggers
timer = {
'every minute'
}
},
execute = function(domoticz, triggeredItem, info)
local cnt = 0
domoticz.devices().forEach(
function(device)
cnt = cnt + 1
local description = device.description
if description ~= nil and description ~= '' then
local ok, settings = pcall( domoticz.utils.fromJSON, description)
if ok and settings ~= nil then
-- Determine highest level available in the settings that is
-- lower than the current level where 'Off' equals level 0.
local dimlevel = nil;
-- Lowest dim level is 'Off', this I will represent as level == 0.
-- If auto_off_minutes was specified, I will use this to
-- initialise the dimlevel variable with a level of 0.
if settings.auto_off_minutes ~= nil then
dimlevel = { level = 0, minutes = settings.auto_off_minutes }
end
-- If one or more dimlevels were specified, the user must think our
-- device is a dimmer switch, so it should be safe to reference
-- its level attribute. We search for the dimlevel that has the
-- highest level below that of our device's level.
if settings.auto_off_dimlevel ~= nil then
dimlevel = settings.auto_off_dimlevel.reduce(
function(acc, dl)
if dl.level < device.level then
if acc == nil or dl.level > acc.level then
acc = dl
end
end
return acc -- always return the accumulator
end,
dimlevel ) -- Initial value for the accumulator is either nil
-- if no auto_off_minutes was specified or it is
-- {level = 0, minutes = <auto_off_minutes>}.
end
-- If we have found a new dim level then see if it is time yet to
-- set this new level.
if dimlevel ~= nil then
local minutes = dimlevel.minutes
-- Find the latest last modified date from our device plus
-- any motion devices that may have been specified. Initially
-- assume our device has the latest last modified date.
local lastUpdate = device.lastUpdate
-- We will skip setting a new level if either of the following is true:
-- one or more of the motion devices has state 'On' or the lastUpdate
-- on our device or any of the specified motion devices is less than
-- <minutes> ago. To accomplish this, lets find out if any of the
-- motion devices has a more recent lastUpdate than that of our device.
-- If a motion device has state 'On', we will set lastUpdate to nil,
-- indicating we can skip the check for lastUpdate completely.
if type(settings.auto_off_motion_device) == "string" then
-- a single motion device was specified
local motion_device = domoticz.devices(settings.auto_off_motion_device)
if motion_device.state ~= 'Off' then
lastUpdate = nil
elseif motion_device.lastUpdate.compare(lastUpdate) > 0 then
lastUpdate = motion_device.lastUpdate
end
elseif type(settings.auto_off_motion_device) == "table" then
local motion_devices = domoticz.devices().filter(settings.auto_off_motion_device)
lastUpdate = motion_devices.reduce(
function(acc, md)
if md.timedOut ~= true then -- Ignore motion devices that have timed out, to
-- avoid leaving the light on because a sensor that
-- has an empty battery isn't updated to 'Off'.
if acc ~= nil then -- If a previous sensor was 'On', we will have set
-- lastUpdate to nil and we don't want to overwrite
-- this.
if md.state ~= 'Off' then
acc = nil -- Set lastUpdate to nil to indicate at least
-- one sensor is 'On'.
elseif md.lastUpdate.compare(acc) > 0 then
acc = md.lastUpdate -- We've found a more recent lastUpdate.
end
end
end
return acc -- Always return the accumulator.
end, lastUpdate)
end
if lastUpdate ~= nil and lastUpdate.minutesAgo > tonumber(minutes) then
if dimlevel.level > 0 then
domoticz.log(device.name .. ' is dimmed to level ' .. dimlevel.level .. ' after ' .. settings.auto_off_minutes .. ' minutes.', domoticz.LOG_INFO)
device.setLevel(tonumber(dimlevel.level))
else
domoticz.log(device.name .. ' is switched off after ' .. settings.auto_off_minutes .. ' minutes.', domoticz.LOG_INFO)
device.switchOff()
end
end
end
else
domoticz.log( 'Device description for '.. device.name ..' is not in json format. Ignoring this device.', domoticz.LOG_WARNING)
end
end
-- end
end
)
domoticz.log('Scanned ' .. tostring(cnt) .. ' devices.', domoticz.LOG_INFO)
-- -- Set ventilation back to automatic after 2 hours of manual control.
-- local hs = domoticz.devices('Humidity SetPoint')
-- if hs.state ~= 'On' and hs.lastUpdate.hoursAgo >= 2 then
-- hs.switchOn()
-- end
end
}
lastUpdate and motion_device.lastUpdate are both dzVents time objects... But 0 is a number and the result of the compare function is a time object.rrozema wrote: ↑Saturday 01 February 2020 22:01 Below code is not working yet. I get an error that I can't fix yet but I'm running out of time for the moment. Maybe @waaren can have a look at what I'm doing wrong? I've not even activated the new features yet; still using the original json values in the descriptions of my devices.
Line 105 is the lineCode: Select all
2020-02-01 21:52:00.945 Error: dzVents: Error: (2.5.0) An error occurred when calling event handler Generic Auto Off 2020-02-01 21:52:00.945 Error: dzVents: Error: (2.5.0) ...z/scripts/dzVents/generated_scripts/Generic Auto Off.lua:105: attempt to compare number with table
but I don't see how lastUpdate could suddenly be a number?Code: Select all
elseif motion_device.lastUpdate.compare(lastUpdate) > 0 then
Code: Select all
dz.utils._.print(motion_device.lastUpdate.compare(lastUpdate))
{["mins"]=87, ["seconds"]=5228, ["secs"]=5228, ["minutes"]=87, ["compare"]=-1, ["milliseconds"]=5228811, ["ms"]=5228811, ["days"]=0, ["hours"]=1}
dz.utils.dumpTable(motion_device.lastUpdate.compare(lastUpdate))
> days: 0
> mins: 92
> hours: 1
> minutes: 92
> compare: -1
> seconds: 5557
> milliseconds: 5557487
> ms: 5557487
> secs: 5557
Code: Select all
elseif motion_device.lastUpdate.compare(lastUpdate).minutes > 0 then
Code: Select all
elseif motion_device.lastUpdate.compare(lastUpdate).seconds > 0 then
Thanks, that was helpful. I keep forgetting that compare() returns that structure. Not there yet, but this helped me further. What I needed was:waaren wrote: ↑Sunday 02 February 2020 1:30 ...
so what you probably need is something likeorCode: Select all
elseif motion_device.lastUpdate.compare(lastUpdate).minutes > 0 then
Code: Select all
elseif motion_device.lastUpdate.compare(lastUpdate).seconds > 0 then
Code: Select all
elseif motion_device.lastUpdate.compare(lastUpdate).compare > 0 then
I am not completely satisfied yet, but here's a 1st version that does mostly what you asked for. I'm not putting the code in the opening post yet, but you can give it a try, see how you like it.
Code: Select all
{
"auto_off_dimlevel": [{ "level": 50, "minutes": 2 }, { "level": 33, "minutes": 2 }],
"auto_off_minutes": 10
}
Code: Select all
{
"auto_off_dimlevel": { "level": 33, "minutes": 2 },
"auto_off_minutes": 10
}
Code: Select all
{
"auto_off_dimlevel": { "level": 33, "minutes": 2 }
}
Code: Select all
-- This script will run every minute and can automatically send an 'Off' command to turn off any device after
-- it has been on for some specified time. Each device can be individually configured by putting json coded
-- settings into the device's description field. The settings currently supported are:
-- - "auto_off_minutes" : <time in minutes>
-- - "auto_off_motion_device" : "<name of a motion detection device>"
-- If "auto_off_minutes" is not set, the device will never be turned off by this script. If
-- "auto_off_minutes" is set and <time in minutes> is a valid number, the device will be turned off when it
-- is found to be on plus the device's lastUpdate is at least <time in minutes> minutes old. This behavior
-- can be further modified by specifying a valid device name after "auto_off_motion_device". When a motion
-- device is specified and the device's lastUpdate is at least <time in minutes> old, the device will not
-- be turned off until the motion device is off and it's lastUpdate is also <time in minutes> old.
-- Specifying "auto_off_motion_device" without specifying "auto_off_minutes" does nothing.
--
-- Example 1: turn off the device after 2 minutes:
-- {
-- "auto_off_minutes": 2
-- }
--
-- Example 2: turn off the device when it has been on for 5 minutes and no motion has been detected for
-- at least 5 minutes:
-- {
-- "auto_off_minutes": 5,
-- "auto_off_motion_device": "Overloop: Motion"
-- }
--
-- Example 3: turn off the device when it has been on for 1 minute and not motion was detected for at least 1
-- minute on either one of a set of motion sensors.
--{
--"auto_off_minutes": 1,
--"auto_off_motion_device": ["Overloop 1: Motion 1", "Overloop 1: Motion 2"]
--}
return {
on = {
-- timer triggers
timer = {
'every minute'
}
},
data = {
triggers = {initial = {}}, -- Holds a list of (unique) motion devices and a list of devices it triggers.
triggered_by = {initial = {}} -- Holds a list of (unique) triggered devices and a list of motion devices that trigger it.
},
logging = {
level = domoticz.LOG_INFO,
marker = "Generic Auto Off v2"
},
execute = function(domoticz, triggeredItem, info)
local cnt = 0
--domoticz.dump(domoticz.utils)
--domoticz.utils.dumpTable(domoticz)
--domoticz.utils.dumpTable(domoticz.utils)
--local Time = require('Time')
local now = domoticz.time
-- if domoticz.data.triggers == nil then
domoticz.data.triggers = {}
-- end
-- if domoticz.data.triggered_by == nil then
domoticz.data.triggered_by = {}
-- end
domoticz.devices().forEach(
function(device)
cnt = cnt + 1
local description
local motion_device_names -- If no settings are found we may still have to
-- remove some device's motion devices from the
-- trigger lists. So we start with an empty list.
motion_device_names = {}
description = device.description
if description ~= nil and description ~= '' then
local ok, settings = pcall( domoticz.utils.fromJSON, description)
if ok and settings ~= nil then
-- Determine highest level available in the settings that is
-- lower than the current level where 'Off' equals level 0.
local dimlevel = nil;
-- Determine the list of motion devices configured for this
-- device in the settings.
if type(settings.auto_off_motion_device) == "string" then
table.insert( motion_device_names, settings.auto_off_motion_device)
elseif type(settings.auto_off_motion_device) == "table" then
for i,v in ipairs(settings.auto_off_motion_device) do
table.insert( motion_device_names, v)
end
end
-- Lowest dim level is 'Off', this I will represent as level == 0.
-- If auto_off_minutes was specified and the device is not off, I
-- will use this to initialise the dimlevel variable with a level
-- of 0. If the device is off already, we don't need to change it.
if settings.auto_off_minutes ~= nil and device.state ~= 'Off' then
dimlevel = { level = 0, minutes = settings.auto_off_minutes}
end
-- If one or more dimlevels were specified, the user must think our
-- device is a dimmer switch, so it should be safe to reference
-- its level attribute. We search for the dimlevel that has the
-- highest level below that of our device's level.
if settings.auto_off_dimlevel ~= nil and type(settings.auto_off_dimlevel) == "table" then
-- if a single dimlevel was specified...
if settings.auto_off_dimlevel.level ~= nil then
if settings.auto_off_dimlevel.level < device.level then
if dimlevel == nil or settings.auto_off_dimlevel.level > dimlevel.level then
dimlevel = settings.auto_off_dimlevel
end
end
else
-- or when multiple dimlevels were specified as a table of tables.
for i,v in ipairs(settings.auto_off_dimlevel) do
if v.level < device.level then
if dimlevel == nil or v.level > dimlevel.level then
dimlevel = v
end
end
end
end
end
-- If we have found a new dim level then see if it is time yet to
-- set this new level.
if dimlevel ~= nil then
local minutes = dimlevel.minutes
-- Find the latest last modified date from our device plus
-- any motion devices that may have been specified. Initially
-- assume our device has the latest last modified date.
local lastUpdate = device.lastUpdate
--domoticz.utils.dumpTable(lastUpdate)
-- We will skip setting a new level if either of the following is true:
-- at least one of the motion devices has state 'On' or the lastUpdate
-- on our device or any of the specified motion devices is less than
-- <minutes> ago. To accomplish this, lets find out if any of the
-- motion devices has a more recent lastUpdate than that of our device.
-- If a motion device has state 'On', we will set lastUpdate to nil,
-- indicating we can skip the check for lastUpdate completely.
local motion_devices = domoticz.devices().filter(motion_device_names)
lastUpdate = motion_devices.reduce(
function(acc, md)
if md.timedOut ~= true then -- Ignore motion devices that have timed out, to
-- avoid leaving the light on because a sensor that
-- has an empty battery isn't updated to 'Off'.
if acc ~= nil then -- If a previous sensor was 'On', we will have set
-- lastUpdate to nil and we don't want to overwrite
-- this.
if md.state ~= 'Off' then
--domoticz.utils._.print( 'Sensor ' .. md.name .. ' is on')
acc = nil -- Set lastUpdate to nil to indicate at least
-- one sensor is 'On'.
elseif md.lastUpdate.compare(acc).compare < 0 then -- If acc < lastUpdate
domoticz.utils._.print( 'Found lastUpdate ' .. md.lastUpdate.raw .. ' on ' .. md.name .. ' to be more recent than ' .. acc.raw .. '.')
acc = md.lastUpdate -- We've found a more recent lastUpdate.
end
end
else
domoticz.log( 'Ignoring motion device ' .. md.name .. ' because it timed out. Do you need to replace it\'s battery?', domoticz.LOG_WARNING)
end
return acc -- Always return the accumulator.
end, lastUpdate)
if lastUpdate ~= nil and lastUpdate.secondsAgo > tonumber(minutes * 60) then
if dimlevel.level > 0 then
domoticz.log(device.name .. ' is dimmed to level ' .. dimlevel.level .. ' after ' .. dimlevel.minutes .. ' minutes.', domoticz.LOG_INFO)
device.setLevel(tonumber(dimlevel.level))
else
domoticz.log(device.name .. ' is switched off after ' .. dimlevel.minutes .. ' minutes.', domoticz.LOG_INFO)
device.switchOff()
end
end
end
-- Now see if we need to update our triggers lists. I keep a list of trigger devices that I
-- use to see 1) which devices are our motion devices and 2) which devices need to be
-- switched on for each motion device. Plus I keep an additional list of triggered devices
-- so I can determine that a motion device was removed from the settings. Because in this
-- case I need to remove it from the list of trigger devices too.
--
-- Do we need to add entries to the triggers lists?
-- For each of the motion devices in settings
-- If not exists data.triggers[ <settings.motion device> ]
-- Then
-- add a new entry in data.triggers with our current device in the list of devices to trigger.
-- Else
-- If our current device is not in the list of devices to trigger
-- Then
-- Add our device to the list of devices to trigger.
--
-- If data.triggered_by does not have our device
-- Then
-- add a new entry in data.triggered_by for our current device with our motion device in the triggered_by list.
-- Else
-- If our motion device is not in the list of trigger devices
-- Then
-- add the motion device to the list of trigger devices
--
for i,v in ipairs(motion_device_names) do
if domoticz.data.triggers[v] == nil then
--domoticz.log('adding trigger ' .. v .. '.', domoticz.LOG_DEBUG)
domoticz.data.triggers[v] = {}
end
-- TODO: This code keeps adding entries, even though following if exists. Why?
if domoticz.data.triggers[v][device.name] == nil then
--domoticz.log('adding triggered device ' .. device.name .. '.', domoticz.LOG_DEBUG)
table.insert(domoticz.data.triggers[v], device.name)
end
if domoticz.data.triggered_by[device.name] == nil then
domoticz.data.triggered_by[device.name] = {}
end
if domoticz.data.triggered_by[device.name][v] == nil then
table.insert(domoticz.data.triggered_by[device.name], v)
end
end
else
domoticz.log( 'Device description for '.. device.name ..' is not in json format. Ignoring this device.', domoticz.LOG_WARNING)
end
-- Do we need to remove entries from the triggers lists?
-- For each of the entries in the list of trigger devices from data.triggered_by[ <current device> ]
-- If settings does not have a motion device[ <listed motion device> ]
-- Then
-- remove data.triggered_by[<current device>][ <listed motion device> ]
-- If data.triggered_by[<current device>] has no more entries
-- Then
-- remove data.triggered_by[<current device>]
-- If exists data.triggers[ <listed motion device> ][ <current device> ]
-- Then
-- remove data.triggers[ <listed motion device> ][ <current device> ]
-- If data.triggers[ <listed motion device> ] has no more entries
-- remove data.triggers[ <listed motion device> ]
if domoticz.data.triggered_by[device.name] ~= nil then
for i,v in ipairs(domoticz.data.triggered_by[device.name]) do
if motion_device_names[v] == nil then
domoticz.data.triggered_by[device.name][v] = nil
if #(domoticz.data.triggered_by[device.name]) <= 0 then
domoticz.data.triggered_by[device.name] = nil
end
if domoticz.data.triggers[v][device.name] ~= nil then
domoticz.data.triggers[v][device.name] = nil
end
if #(domoticz.data.triggers[v]) <= 0 then
domoticz.data.triggers[v] = nil
end
end
end
end
end
end
)
domoticz.log('Scanned ' .. tostring(cnt) .. ' devices.', domoticz.LOG_INFO)
--domoticz.utils.dumpTable(domoticz.data.triggers)
--domoticz.utils.dumpTable(domoticz.data.triggered_by)
-- -- Set ventilation back to automatic after 2 hours of manual control.
-- local hs = domoticz.devices('Humidity SetPoint')
-- if hs.state ~= 'On' and hs.lastUpdate.hoursAgo >= 2 then
-- hs.switchOn()
-- end
end
}
Thanks, you're right. I update the script in my previous post. I'm not done with it yet, so I'm not putting it in the opening post yet. As You may see from the code I'm trying to add auto_on functionality to it too: I would like to have the possibility to specify in the settings the name of the sensor(s) that should switch the device On too. This would reduce the number of scripts running in my system even more. Because dzvents doesn't have meta-data triggers I am building a list of trigger devices, which gets updated every minute. Using this list I can then have this script check for every device activation if a device needs to be switched "On" too. This would make the functionality of auto_dim more useful, as I've noticed that swithcing a dimmer "On", returns it to the last level set. So that will always be the lowest dim level specified for that device. I want auto_on to set it to a specified level so that the light for can example go on at 100%, dim to 20% after 5 minutes of no movement and only after another 10 minutes go Off. If however someone enters the room, the light should go back to 100% again, even if it hasn't gone Off yet. But I'm still trying to get this to work correctly and I don't have a lot a free time for it.Vondee wrote: ↑Saturday 15 February 2020 20:51 After the set time, Domoticz has an error:
2020-02-15 20:43:00.454 Error: dzVents: Error: (2.5.5) An error occurred when calling event handler Generec_Auto_Off
2020-02-15 20:43:00.454 Error: dzVents: Error: (2.5.5) ...z/scripts/dzVents/generated_scripts/Generec_Auto_Off.lua:143: attempt to concatenate a nil value (field 'auto_off_minutes')
ah, That's my mistake. These are the settings I initially described for when you have more than one (motion) sensor for a light. Waaren pointed out however, that when you put them like this, only one of the auto_motion_device entries is used. The correct way to list multiple motion devices is like this:
Code: Select all
{
"auto_off_minutes": 30,
"auto_off_motion_device": ["kamer sensor PIR", "Keuken Sensor PIR", "PIR Garage"]
}
I do not think that I would use this functionality, since time / lux etc. is also an input to determine that a light should go on.If however someone enters the room, the light should go back to 100% again, even if it hasn't gone Off yet. But I'm still trying to get this to work correctly and I don't have a lot a free time for it.
That should not be possible: at every run the script searches in the settings for a dim level that is lower than the light's current level and only activates that dim level. In other words: if the light is off, it should not find a new dim level to set, i.e. it makes no changes to the light's settings. Please post your settings for this light, maybe something is still wrong with that. Please also post the type of dimmer you have installed and the domoticz & dzvents version you are running on.Vondee wrote: ↑Thursday 20 February 2020 13:39 If I manually (or on time) turn the dimmed light off, then the script put it back at the level what was set by the '"auto_off_dimlevel".
The light was at the selected level for the "auto_off_dimlevel" when I turned off the light.
Or do I something wrong?