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