Conditional timers and manual override
Posted: Saturday 03 June 2017 21:59
Hello,
Here is script used to make timers that can change dynamically and be overridden manually (until the next timer occurence.)
Take this snippet for instance :
It will turn the water heater on whenever there are guests (‘Invités’) home (1st line).
During a normal day, it will turn on the morning (2) and evening (4), plus an extra period the afternoon (3).
Otherwise, it should be off (5).
presence() is a function that takes an array of strings. It checks if any virtual device named “Presence <someone>” is activated (the prefix and suffix of the virtual switch is configurable manually.)
I think it is pretty much human readable, while still allowing some complex schedules.
I use this script with On/Off switches, dimmers (percentage) and thermostat setpoints (for these, please see my examples of how to set them up; the idx must be included.)
For thermostat, I have made a workaround for ugly graphs. It will set a value every 15 minutes, to ensure the graph line is horizontal. Activate it by changing the value of ‘thermostatGraphWorkaround’ to ‘true’.
I recommend you name the script ’Timers’ (otherwise, change the variable scriptName) and setting the script to run every minute (the ‘time’ option in Domoticz.)
Let me know what you think of it !
Here is script used to make timers that can change dynamically and be overridden manually (until the next timer occurence.)
Take this snippet for instance :
Code: Select all
{['device'] = 'Boiler Parents', ['schedule'] = {
{['value'] = 'On', ['condition'] = presence({'Invités'})},
{['value'] = 'On', ['timeInterval'] = {{ 4, 0},{ 7,15}}},
{['value'] = 'On', ['timeInterval'] = {{12, 0},{16, 0}}, ['wDays'] = {0,0,0,0,0,1,1}},
{['value'] = 'On', ['timeInterval'] = {{19, 0},{22, 0}}},
{['value'] = 'Off'}
}
}
During a normal day, it will turn on the morning (2) and evening (4), plus an extra period the afternoon (3).
Otherwise, it should be off (5).
presence() is a function that takes an array of strings. It checks if any virtual device named “Presence <someone>” is activated (the prefix and suffix of the virtual switch is configurable manually.)
I think it is pretty much human readable, while still allowing some complex schedules.

I use this script with On/Off switches, dimmers (percentage) and thermostat setpoints (for these, please see my examples of how to set them up; the idx must be included.)
For thermostat, I have made a workaround for ugly graphs. It will set a value every 15 minutes, to ensure the graph line is horizontal. Activate it by changing the value of ‘thermostatGraphWorkaround’ to ‘true’.
I recommend you name the script ’Timers’ (otherwise, change the variable scriptName) and setting the script to run every minute (the ‘time’ option in Domoticz.)
Code: Select all
commandArray = {}
--[[
This script is intended to be run every minute ('time' setting in Domoticz).
Device is the Domoticz name of the device you want to control. Ex: ['device'] = 'Relay 1'
Schedule contains a table with the schedule
First format must contain : value. May contain: time, timeInterval, wDays, condition (something not explicitely defined is considered to be always true)
A time. DO NOT ADD A LEADING 0. Ex : ['time'] = {23, 9} DON'T WRITE : {23,09}
A timeInterval. Ex: 1:00 to 13:30 : ['timeInterval'] = {{ 1,30},{13, 0}}
00:00 to 1:00 and 20:00 to 24:00 : {{20, 0},{ 1, 0}}
A wDays : Ex: only the weekends : ['wDays'] = {0,0,0,0,0,1,1}
Whether it is enabled or not (default is enabled). Ex : ['enabled'] = false
A value. May be anything. Will be sent to domoticz via the commandArray
A condition. True if this timer is supposed to be active. (I recommend writing a function.) Ex:['condition'] = checkPresence()
For some domoticz devices, the 'normal' way to control them does not work. If it is the case (for ex. thermostat devices) use a workaround
- UpdateDevice. Should specify the idx of the device. Ex : ['idx'] = '45', ['type'] = 'UpdateDevice'
If you want the manual override to work, create a uservariale named 'Timer-<deviceName>-LastSetPoint'. Ex : 'Timer-HallLight-LastSetPoint of type String.
If that variable does not exist, the timer will work but the manual override won't.
Assumptions : - the first day of the week is monday
- Device state in Domoticz == actual device state
- nil is used internally; should not be used by a device
The first timer to match all his criterias is applied, without going further. A criteria that is not set is considered to be true
If you don't set a value for a timer, the script won't update the device. Use if you want manual control.
Known bugs : - manual override only works if there were at least 2 minutes between automatic changes.
- manual override does not work with devices who use percentages
]]--
function initializeTimer()
timers = {
{['device'] = 'Boiler Aymeric', ['enabled'] = false, ['schedule'] = {
{['value'] = 'On', ['timeInterval'] = {{ 5,30},{ 8,30}}, ['condition'] = presence({'Aymeric', 'Quentin'})},
{['value'] = 'On', ['timeInterval'] = {{17, 0},{22, 0}}, ['condition'] = presence({'Aymeric', 'Quentin'})},
{['value'] = 'On', ['condition'] = presence({'Invités'})},
{['value'] = 'Off'}
}
},
{['device'] = 'Boiler Parents', ['enabled'] = false, ['schedule'] = {
{['value'] = 'On', ['condition'] = presence({'Invités'})},
{['value'] = 'On', ['timeInterval'] = {{ 4, 0},{ 7,15}}},
{['value'] = 'On', ['timeInterval'] = {{12, 0},{16, 0}}, ['wDays'] = {0,0,0,0,0,1,1}},
{['value'] = 'On', ['timeInterval'] = {{19, 0},{22, 0}}},
{['value'] = 'Off'}
}
},
{['device'] = 'Presence Quentin', ['enabled'] = false, ['schedule'] = {
{['value'] = 'On', ['timeInterval'] = {{19, 0},{ 8, 0}}},
{['value'] = 'Off'}
}
},
{['device'] = 'Presence Aymeric', ['schedule'] = {
{['value'] = 'On', ['wDays'] = {0,0,0,0,0,1,1}},
{['value'] = 'On', ['timeInterval'] = {{17, 0},{23,59}}, ['wDays'] = {0,0,0,0,1,0,0}},
{['value'] = 'Off'}
}
},
{['device'] = 'Ventilation', ['enabled'] = false, ['type'] = 'Dimmer', ['schedule'] = {
{['value'] = 90, ['condition'] = presence({'Quentin', 'Invités'})},
{['value'] = 50, ['timeInterval'] = {{ 6,45},{ 7,45}}},
{['value'] = 50, ['timeInterval'] = {{17, 0},{21, 0}}},
{['value'] = 10}
}
},
{['device'] = 'Consigne Salon', ['idx'] = '45', ['type'] = 'UpdateDevice', ['schedule'] = {
{['value'] = 20, ['timeInterval'] = {{ 9, 0},{20, 0}}, ['condition'] = presence({'Invités'})},
{['value'] = 20, ['condition'] = chauffageHeureDePointe()},
{['value'] = 18}
}
},
{['device'] = 'Consigne Cuisine', ['idx'] = '40', ['type'] = 'UpdateDevice', ['schedule'] = {
{['value'] = 19, ['timeInterval'] = {{15,30},{20, 0}}},
{['value'] = 17}
}
},
{['device'] = 'Consigne Bureau', ['idx'] = '261', ['type'] = 'UpdateDevice', ['schedule'] = {
{['value'] = 18, ['condition'] = chauffageHeureDePointe()},
{['value'] = 15}
}
},
{['device'] = 'Consigne Mezzanine', ['idx'] = '43', ['type'] = 'UpdateDevice', ['schedule'] = {
{['value'] = 19, ['condition'] = chauffageHeureDePointe()},
{['value'] = 18}
}
},
{['device'] = 'Consigne Ch Parents', ['idx'] = '38', ['type'] = 'UpdateDevice', ['schedule'] = {
{['value'] = 17, ['timeInterval'] = {{ 6, 0},{ 6,40}}},
{['value'] = 17, ['timeInterval'] = {{19, 0},{22, 0}}},
{['value'] = 15}
}
},
{['device'] = 'Consigne Ch Quentin', ['idx'] = '46', ['type'] = 'UpdateDevice', ['schedule'] = {
{['value'] = 19, ['timeInterval'] = {{ 7, 0},{19, 0}}, ['condition'] = presence({'Quentin'})},
{['value'] = 17, ['timeInterval'] = {{19, 0},{ 7, 0}}, ['condition'] = presence({'Quentin'})},
{['value'] = 10}
}
},
{['device'] = 'Consigne Hall', ['idx'] = '44', ['type'] = 'UpdateDevice', ['schedule'] = {
{['value'] = 16}
}
},
{['device'] = 'Consigne SdB Parents', ['idx'] = '262', ['type'] = 'UpdateDevice', ['schedule'] = {
{['value'] = 19, ['timeInterval'] = {{ 6, 0},{ 8, 0}}},
{['value'] = 18, ['timeInterval'] = {{21, 0},{22, 0}}},
{['value'] = 17}
}
}
}
end
-------------------
-- User functions
-------------------
function chauffageHeureDePointe()
return isInTimeInterval({{15, 0},{21, 0}});
end
-------------------
-- Variable declarations
-------------------
scriptName = 'Timers' --Used for debug messages
presencePrefix = 'Presence '
presenceSuffix = ''
thermostatGraphWorkaround = true -- Sets the temperature every 15 minutes, even if it has not changed.
-- Used to have better graphs in Domoticz.
-- Will work only if the value is continually set (use timeInterval instead of time)
-- !! Needs some uservariables
FirstDayOfWeekIsMonday = true --Maybe buggy. Has not been tested when false
domoticzIP='192.168.1.30'
now = {tonumber(os.date("%H")), tonumber(os.date("%M"))}
-------------------
-- Helper functions
-------------------
function needToActivateWorkaround(deviceName)
if not thermostatGraphWorkaround then
return false
end
if deviceName == nil then
return exitFailure('needToActivateWorkaround : received nil');
end
if timeDifferenceInMin(otherdevices_lastupdate[deviceName]) >= 45 then
return true
end
return false
end
function timeDifferenceInMin(s)
year = string.sub(s, 1, 4)
month = string.sub(s, 6, 7)
day = string.sub(s, 9, 10)
hour = string.sub(s, 12, 13)
minutes = string.sub(s, 15, 16)
seconds = string.sub(s, 18, 19)
t2 = os.time{year=year, month=month, day=day, hour=hour, min=minutes, sec=seconds}
difference = os.difftime (os.time(), t2)
return math.floor(difference/60)
end
function getLastSetPointUserVarFromDeviceName(string)
return 'Timer-' .. string .. '-LastSetPoint'
end
function setValueByUpdateDevice(deviceName, idx, value)
if (deviceName == nil or idx == nil or value == nil) then
return exitFailure('function setValueByUpdateDevice received some nil value')
end
value = tostring(value)
if tonumber(otherdevices[deviceName]) ~= tonumber(value) then
if tonumber(value) ~= tonumber(uservariables[getLastSetPointUserVarFromDeviceName(deviceName)]) then --SetPoint has just changed
commandArray['Variable:' .. getLastSetPointUserVarFromDeviceName(deviceName)] = value
commandArray[#commandArray + 1] = {['UpdateDevice']=idx .. '|' .. value .. '|' .. value}
elseif needToActivateWorkaround(deviceName) then --Manual override + graphWorkaround
value = tostring(otherdevices[deviceName])
commandArray[#commandArray + 1] = {['UpdateDevice']=idx .. '|' .. value .. '|' .. value}
end
elseif needToActivateWorkaround(deviceName) then --Graphworkaround
commandArray[#commandArray + 1] = {['UpdateDevice']=idx .. '|' .. value .. '|' .. value}
end
end
function checkDevice(device)
if device.enabled ~= nil and not (device.enabled == true or device.enabled == 1 or device.enabled == 'true') then
return true
end
local value = getCurrentValue(device.schedule)
if value == nil then
return true
end
if device.type == nil then
setValue(device.device, value)
return true
elseif device.type == 'UpdateDevice' then
if (device.idx == nil) then
return exitFailure('function checkDevice : device ' .. device.device .. ' does not have an idx')
end
setValueByUpdateDevice(device.device, device.idx, value)
return true
elseif device.type == 'Dimmer' then
setValueForDimmer(device.device, value);
return true
else
return exitFailure('function checkDevice : device ' .. device.device .. ' has an unknow type : ' .. device.type)
end
end
function setValue(device, value)
if value ~= uservariables[getLastSetPointUserVarFromDeviceName(device)] then --SetPoint has just changed
commandArray['Variable:' .. getLastSetPointUserVarFromDeviceName(device)] = value
if otherdevices[device] ~= value then
commandArray[device] = value
end
end
end
function setValueForDimmer(device, value)
if tostring(value) ~= tostring(uservariables[getLastSetPointUserVarFromDeviceName(device)]) then --SetPoint has just changed
commandArray['Variable:' .. getLastSetPointUserVarFromDeviceName(device)] = tostring(value)
if otherdevices[device] ~= value then
commandArray[device] = 'Set Level ' .. tostring(value)
end
end
end
-- May return nil (if there is no valid timer. In that case, the device won't be updated)
function getCurrentValue(schedule)
local value
for key, timer in pairs(schedule) do
local val = getCurrentValueFromTimer(timer)
if val ~= nil then
value = val
break
end
end
return value
end
function getWeekDayOfToday()
local weekDayOffSet = 1
if (FirstDayOfWeekIsMonday) then
weekDayOffSet = 0
end
wDay = tonumber(os.date('%w'))+weekDayOffSet
if (wDay < 0 or wDay > 6) then
return exitFailure('weekDayOffSet is wrongly set !')
end
return wDay
end
function isInTimeInterval(interval)
if not (type(interval) == "table" and #interval == 2 and isAValidTime(interval[1]) and isAValidTime(interval[2])) then
return exitFailure('function isInTimeInterval(interval) : interval is not valid or time is not valid')
end
if (compareTimes(interval[1], interval[2])) then
return compareTimes(interval[2], now) or compareTimes(now, interval[1])
else
return compareTimes(now, interval[1]) and compareTimes(interval[2], now)
end
end
function isItTime(time)
if not isAValidTime(time) then
return exitFailure('isItTime : received wrong time')
end
if time[2] == tonumber(os.date('%M')) and time[1] == tonumber(os.date('%H')) then
return true
else
return false
end
end
function isInWeekDay(wDays)
if wDays == nil or #wDays ~= 7 then
return exitFailure('wDays is declared incorrectly')
end
return wDays[getWeekDayOfToday()] == 1
end
function isAValidTime(time)
if not (type(time) == 'table' and #time == 2) then
return false
end
hour = time[1]
min = time[2]
if not (type(hour) == 'number' and type(min) == 'number') then
return false
elseif (hour < 0 or hour > 23 or min < 0 or min > 59) then
return false
end
return true
end
-- Returns true if time1 is greater or equal to time2, false otherwise
function compareTimes(time1, time2)
if (time1[1] > time2[1]) then
return true
elseif (time1[1] == time2[1]) then
if (time1[2] >= time2[2]) then
return true
end
end
return false
end
-- Does not exit on its own… use return exitFailure(…)
function exitFailure(string)
print(scriptName .. " failure ! Error : " .. string)
return nil
end
function isempty(s)
return s == nil or s == ''
end
-- Returns true if >= 1 person from the list is present; nil if there is an error
function presence(names)
if (isempty(names)) then
return exitFailure('function presence(...) : received nil')
elseif (type(names) == 'array') then
return exitFailure('function presence(...) : expecting a table')
end
for key, name in pairs(names) do
if otherdevices[presencePrefix .. name .. presenceSuffix] == nil then
return exitFailure('function presence(...) : switch does not exist for ' .. name)
end
if otherdevices[presencePrefix .. name .. presenceSuffix] == 'On' then
return true
end
end
return false
end
-- Returns the value or nil if the timer is not supposed to be active now
function getCurrentValueFromTimer(timer)
valueSet = false
for key, value in pairs(timer) do
if key == 'value' then
valueSet = true
elseif key == 'time' then
if not isItTime(timer.time) then
return nil
end
elseif key == 'timeInterval' then
if not isInTimeInterval(timer.timeInterval) then
return nil
end
elseif key == 'wDays' then
if not isInWeekDay(timer.wDays) then
return nil
end
elseif key == 'condition' then
if not timer.condition then
return nil
end
else
return exitFailure('function getValueFromTimer : error in a key name for a timer or too many conditions')
end
end
if valueSet then
return timer.value
else
return exitFailure('function getValueFromTimer : a timer has no value')
end
end
--------------
-- Main
--------------
initializeTimer()
for key, device in pairs(timers) do
if checkDevice(device) == nil then
return exitFailure('Main : checkDevice returned nil')
end
end
return commandArray