hi,
i use a timer like this :
timer = {
'30 minutes after sunset',
}
but it doesnt start script, I think it hasn't worked for 1 week.
timer [SOLVED]
Moderator: leecollings
-
- Posts: 18
- Joined: Sunday 12 August 2018 11:25
- Target OS: Raspberry Pi / ODroid
- Domoticz version: Beta
- Location: Netherlands
- Contact:
Re: timer
I also have the same problem...
After upgrading to 13080 this weekend all DZVENT scripts that have a trigger with ...after or before sunset or sunrise stopped working...
For example..
I noticed that the last 2 days my lights did not turn on after sunset... so i started testing with some test scripts...
After upgrading to 13080 this weekend all DZVENT scripts that have a trigger with ...after or before sunset or sunrise stopped working...
For example..
Code: Select all
return {
active = true,
logging = {
level = domoticz.LOG_DEBUG, -- Select one of LOG_DEBUG, LOG_INFO, LOG_ERROR, LOG_FORCE to override system log level
marker = "TEST"
},
on = {
timer = {'10 minutes after sunset'},
},
execute = function(domoticz, item)
local ErkerLampLinks = domoticz.devices(2692)
ErkerLampLinks.switchOn().checkFirst()
end
}
- waaren
- Posts: 6028
- Joined: Tuesday 03 January 2017 14:18
- Target OS: Linux
- Domoticz version: Beta
- Location: Netherlands
- Contact:
Re: timer
Thx for reporting. Will be fixed in next build.
Debian buster, bullseye on RPI-4, Intel NUC.
dz Beta, Z-Wave, RFLink, RFXtrx433e, P1, Youless, Hue, Yeelight, Xiaomi, MQTT
==>> dzVents wiki
dz Beta, Z-Wave, RFLink, RFXtrx433e, P1, Youless, Hue, Yeelight, Xiaomi, MQTT
==>> dzVents wiki
- waaren
- Posts: 6028
- Joined: Tuesday 03 January 2017 14:18
- Target OS: Linux
- Domoticz version: Beta
- Location: Netherlands
- Contact:
Re: timer
Should be fixed in build 13081. If you need it now you can already test by replacing <domoticz dir>/dzVents/runtime/Time.lua with below code but only if you are using a version with the described bug.
Code: Select all
local utils = require('Utils')
local _MS -- kind of a cache so we don't have to extract ms every time
local gTimes --
local isEmpty = function(v)
return (v == nil or v == '')
end
local function getTimezone()
local diff = os.difftime(os.time(), os.time(os.date("!*t")))
return ( os.date('!*t').isdst and ( diff + 3600 ) ) or diff
end
local function getSMs(s)
local ms = 0
local parts = utils.stringSplit(s, '.') -- do string splitting instead of math stuff.. can't seem to get the floating points right
s = tonumber(parts[1])
if (parts[2] ~= nil) then
-- should always be three digits!!
ms = tonumber(parts[2])
end
return s, ms
end
local function parseDate(sDate)
return string.match(sDate, "(%d+)%-(%d+)%-(%d+)[%sT]+(%d+):(%d+):([0-9%.]+)")
end
local function _getMS()
if (_MS == nil) then
local dzCurrentTime = _G.globalvariables.currentTime
local y, mon, d, h, min, s = parseDate(dzCurrentTime)
local ms
s, ms = getSMs(s)
_MS = ms
end
return _MS
end
local getDiffParts = function(secDiff, ms, offsetMS)
-- ms is the ms part that should be added to secDiff to get 'sss.ms'
if (ms == nil) then
ms = 0
end
if (offsetMS == nil) then
offsetMS = 0
end
local secs = secDiff
local msDiff
msDiff = (secs * 1000) - ms + offsetMS
if (math.abs(msDiff) < 1000) then
secs = 0
end
local minDiff = math.floor(math.abs((secs / 60)))
local hourDiff = math.floor(math.abs((secs / 3600)))
local dayDiff = math.floor(math.abs((secs / 86400)))
local cmp
if (msDiff == 0) then
cmp = 0
elseif (msDiff > 0) then
cmp = -1
else
cmp = 1
end
return
math.abs(msDiff),
math.abs(secs),
minDiff,
hourDiff,
dayDiff,
cmp
end
-- week functions as taken from http://lua-users.org/wiki/WeekNumberInYear
-- Get day of a week at year beginning
--(tm can be any date and will be forced to 1st of january same year)
-- return 1=mon 7=sun
local function getYearBeginDayOfWeek(tm)
local yearBegin = os.time{year=os.date("*t",tm).year,month=1,day=1}
local yearBeginDayOfWeek = tonumber(os.date("%w",yearBegin))
-- sunday correct from 0 -> 7
if(yearBeginDayOfWeek == 0) then
yearBeginDayOfWeek = 7
end
return yearBeginDayOfWeek
end
-- tm: date (as returned from os.time)
-- returns basic correction to be add for counting number of week
-- weekNum = math.floor((dayOfYear + returnedNumber) / 7) + 1
-- (does not consider correction at begin and end of year)
local function getDayAdd(tm)
local yearBeginDayOfWeek = getYearBeginDayOfWeek(tm)
local dayAdd
if(yearBeginDayOfWeek < 5 ) then
-- first day is week 1
dayAdd = (yearBeginDayOfWeek - 2)
else
-- first day is week 52 or 53
dayAdd = (yearBeginDayOfWeek - 9)
end
return dayAdd
end
-- tm is date as returned from os.time()
-- return week number in year based on ISO8601
-- (week with 1st thursday since Jan 1st (including) is considered as Week 1)
-- (if Jan 1st is Fri,Sat,Sun then it is part of week number from last year -> 52 or 53)
local function getWeekNumberOfYear(tm)
local dayOfYear = os.date("%j",tm)
local dayAdd = getDayAdd(tm)
local dayOfYearCorrected = dayOfYear + dayAdd
if(dayOfYearCorrected < 0) then
-- week of last year - decide if 52 or 53
local lastYearBegin = os.time{year=os.date("*t",tm).year-1,month=1,day=1}
local lastYearEnd = os.time{year=os.date("*t",tm).year-1,month=12,day=31}
dayAdd = getDayAdd(lastYearBegin)
dayOfYear = dayOfYear + os.date("%j",lastYearEnd)
dayOfYearCorrected = dayOfYear + dayAdd
end
local weekNum = math.floor((dayOfYearCorrected) / 7) + 1
if( (dayOfYearCorrected > 0) and weekNum == 53) then -- check if it is not considered as part of week 1 of next year
local nextYearBegin = os.time{year=os.date("*t",tm).year+1,month=1,day=1}
local yearBeginDayOfWeek = getYearBeginDayOfWeek(nextYearBegin)
if(yearBeginDayOfWeek < 5 ) then
weekNum = 1
end
end
return weekNum
end
local function Time(sDate, isUTC, _testMS)
local LOOKUPDAYABBROFWEEK = { 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' }
local LOOKUPDAYNAME = { 'Sunday', 'Monday', 'Tuesday', 'WednesDay', 'Thursday', 'Friday', 'Saturday' }
local LOOKUPMONTHABBR = { 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec' }
local LOOKUPMONTH = { 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' }
local ms
local now
local time = {}
local localTime = {} -- nonUTC
local self
local getMS = function()
if (_testMS ~= nil) then
return _testMS
else
return _getMS()
end
end
if isUTC and isUTC == true then
now = os.date('!*t')
else
now = os.date('*t')
isUTC = false
end
local function makesDate()
local now
if (isUTC) then
now = os.date('!*t')
else
now = os.date('*t')
end
local ms = _testMS == nil and getMS() or _testMS
return ( now.year .. '-' .. now.month ..'-' .. now.day .. ' ' .. now.hour .. ':' .. now.min .. ':' .. now.sec .. '.' .. tostring(ms) )
end
if sDate == nil or sDate == '' then
sDate = makesDate()
end
local y, mon, d, h, min, s = parseDate(sDate)
if not(y and mon and d and h and min and s) then
sDate = makesDate()
y, mon, d, h, min, s = parseDate(sDate)
utils.log('sDate was invalid. Reset to ' .. sDate , utils.LOG_ERROR)
end
-- extract s and ms
s, ms = getSMs(s)
local dDate = os.time{year=y,month=mon,day=d,hour=h,min=min,sec=s }
time = os.date('*t', dDate)
local tToday = os.time{
day= now.day,
year= now.year,
month= now.month,
hour= now.hour,
min= now.min,
sec= now.sec
}
-- calculate how many minutes that was from now
local msDiff, secDiff, minDiff, hourDiff, dayDiff, cmp = getDiffParts(os.difftime(tToday, dDate), ms, getMS())
if (cmp > 0) then -- time is in the future so the xxAgo items should be negative
msDiff = -msDiff
secDiff = -secDiff
minDiff = -minDiff
hourDiff = -hourDiff
dayDiff = -dayDiff
end
if (isUTC) then
localTime = os.date('*t', os.time(time) + getTimezone())
self = localTime
self.utcSystemTime = now
self.utcTime = time
self.utcTime.minutes = time.min
self.utcTime.seconds = time.sec
else
self = time
end
self.rawDate = self.year .. '-' .. string.format("%02d", self.month) .. '-' .. string.format("%02d", self.day)
self.time = string.format("%02d", self.hour) .. ':' .. string.format("%02d", self.min)
self.minutesnow = self.hour * 60 + self.min
self.rawTime = self.time .. ':' .. string.format("%02d", self.sec)
self.rawDateTime = self.rawDate .. ' ' .. self.rawTime
self.milliSeconds = ms
self.milliseconds = ms
self.dayAbbrOfWeek = LOOKUPDAYABBROFWEEK[self.wday]
self.dayName = LOOKUPDAYNAME[self.wday]
self.monthAbbrName = LOOKUPMONTHABBR[self.month]
self.monthName = LOOKUPMONTH[self.month]
-- Note: %V doesn't work on Windows so we have to use a custom function here
-- doesn't work: self.week = tonumber(os.date('%V', dDate))
self.week = getWeekNumberOfYear(dDate)
self.raw = sDate
self.isToday = (now.year == time.year and now.month == time.month and now.day == time.day)
self.msAgo = math.floor(msDiff)
self.millisecondsAgo = self.msAgo
self.minutesAgo = minDiff
self.secondsAgo = math.floor(secDiff)
self.hoursAgo = hourDiff
self.daysAgo = dayDiff
self.minutes = self.min
self.seconds = self.sec
self.minutesSinceMidnight = self.hour * 60 + self.min
self.secondsSinceMidnight = self.minutesSinceMidnight * 60 + self.sec
self.utils = utils
self.isUTC = isUTC
self.dDate = dDate
self.isdst = time.isdst
if (_G.TESTMODE) then
_G = _G or {} -- Only used when testing testTime.lua
_G.timeofday = _G.timeofday or {} -- Only used when testing testTime.lua
function self._getUtilsInstance()
return utils
end
end
self.current = os.date('*t')
-- compares to self against the provided Time object (t)
function self.compare(t)
if (t.raw ~= nil) then
local msDiff, secDiff, minDiff, hourDiff, dayDiff, cmp =
getDiffParts(os.difftime(dDate, t.dDate), t.milliseconds, ms) -- offset is 'our' ms value
return {
mins = minDiff,
hours = hourDiff,
secs = math.floor(secDiff),
seconds = math.floor(secDiff),
minutes = minDiff,
days = dayDiff,
ms = math.floor(msDiff),
milliseconds = math.floor(msDiff),
compare = cmp -- 0 == equal, -1==(t earlier than self), 1=(t later than self)
}
else
utils.log('Invalid time format passed to diff. Should a Time object', utils.LOG_ERROR)
end
end
function self.localeMonths()
local months =
{
january = 1, february = 2, march = 3, april = 4, may = 5, june = 6, july = 7, august = 8,
september = 9, october = 10, november = 11, december = 12, jan = 1, feb = 2, mar = 3, apr = 4,
jun = 6, jul = 7, aug = 8, sep = 9, oct = 10, nov = 11, dec = 12
}
local monthSeconds = 2678400 -- accurate enough for this purpose
local startSeconds = 1577833200 -- 2020-1-1 00:00
for monthNumber = 1, 12 do
months[os.date('%B', startSeconds + ( monthNumber - 1 ) * monthSeconds ):sub(1,3):lower()] = monthNumber
months[os.date('%b', startSeconds + ( monthNumber - 1 ) * monthSeconds ):lower()] = monthNumber
end
return months
end
function self.dateToTimestamp(dateString, control)
local tm, dateTable, format = {}, {}, {}
local pattern
if control and control:find('%%') then
pattern = control
elseif control ~= nil then -- control = format
local index = 1
for word in control:gmatch("%w+") do table.insert(format, word) end
for value in dateString:gmatch("%w+") do
dateTable[format[index]] = value
index = index + 1
end
tm.year = dateTable.yyyy or ( dateTable.yy and ( dateTable.yy + 2000 )) or os.date('%Y')
tm.month = dateTable.mm or
( dateTable.mmm and self.localeMonths()[dateTable.mmm:lower()]) or
( dateTable.mmmm and self.localeMonths()[dateTable.mmmm:lower()]) or 1
tm.day = dateTable.dd or 1
tm.hour = dateTable.hh or 0
tm.min = dateTable.MM or 0
tm.sec = dateTable.ss or 0
return os.time(tm)
end
pattern = pattern or '(%d+)%D+(%d+)%D+(%d+)%D+(%d+)%D+(%d+)' -- yyyy-mm--dd hh:mm
tm.year, tm.month, tm.day, tm.hour, tm.min, tm.sec = dateString:match(pattern)
return os.time(tm)
end
function self.timestampToDate(timestamp, humanizedPattern, offSet)
local function convertMnemomic2FmtCode(humanizedPattern)
local humanizedPattern = humanizedPattern or 'yyyy-mm-dd hh:MM:ss'
mnemomics =
{
{'dddd' , '%A'}, -- full weekdayname(e.g. Wednesday) language depends on locale
{'ddd' , '%a'}, -- abbreviated weekdayname(e.g. Wed) language depends on locale
{'dd' , '%d'}, -- day of the month(16){[01-31]
{'mmmm' , '%B'}, -- full monthname(e.g September) language depends on locale
{'mmm' , '%b'}, -- abbreviated monthname(e.g. Sep) language depends on locale
{'mm' , '%m'}, -- month[01-12]
{'yyyy' , '%Y'}, -- 4-digit year
{'yy' , '%y'}, -- two-digityear(98){[00-99]
{'hh' , '%H'}, -- hour 24-hour clock){[00-23]
{'ii' , '%I'}, -- hour 12-hour clock[01-12]
{'MM' , '%M'}, -- minute{[00-59]
{'ss' , '%S'}, -- second [00-60]
{'W' , '%W'}, -- weeknumber [01-53]
{'w' , '%w'}, -- weekday{[0-6] Sunday-Saturday
{'datm' , '%c'}, -- date and time (e.g. 09/16/98 23:48:10) format depends on locale
{'mer' , '%p'}, -- either "am" or "pm" locale
{'date' , '%x'}, -- date(e.g. 09/16/98) format depends on locale
{'time' , '%X'}, -- time(e.g. 23:48:10)
}
for _, conversion in ipairs(mnemomics) do
humanizedPattern = string.gsub(humanizedPattern, conversion[1], '%' .. conversion[2])
end
return humanizedPattern
end
local timestamp = ( timestamp or os.time() ) + ( offSet or 0 )
local dateTimeString = os.date( convertMnemomic2FmtCode(humanizedPattern) , timestamp )
if dateTimeString:find('nZero') then -- remove leading zero's
return dateTimeString:gsub('nZero',''):gsub(' 0',' '):gsub('^0',''):gsub('%s*$','')
end
return dateTimeString:gsub('%s*$','')
end
function self.dateToDate(date, sourceFormat, targetFormat, offSet )
return self.timestampToDate(self.dateToTimestamp(date,sourceFormat), targetFormat, offSet)
end
function self.addSeconds(seconds, factor)
if type(seconds) ~= 'number' then
self.utils.log(tostring(seconds) .. ' is not a valid parameter to this function. Please change to use a number value!', utils.LOG_ERROR)
else
factor = factor or 1
return Time( os.date("%Y-%m-%d %X", self.dDate + factor * math.floor(seconds) ))
end
end
function self.addDays(days)
return self.addSeconds(days, 24 * 3600)
end
function self.addHours(hours)
return self.addSeconds(hours, 3600)
end
function self.addMinutes(minutes)
return self.addSeconds(minutes, 60)
end
function self.makeTime(oDate, isUTC)
local sDate = ( type(oDate) == 'table' and os.date("%Y-%m-%d %H:%M:%S", os.time(oDate)) ) or
( tonumber(oDate) and self.timestampToDate(oDate) ) or oDate
return Time(sDate, isUTC)
end
function self.toUTC(oDate, offset)
local sDate = ( type(oDate) == 'table' and os.date("%Y-%m-%d %H:%M:%S", os.time(oDate)) ) or oDate
local offset = offset or 0
return Time(sDate).addSeconds(-1 * getTimezone() + offset).raw
end
-- return ISO format
function self.getISO()
return os.date("!%Y-%m-%dT%TZ", os.time(time))
end
-- returns hours part and minutes part of the passed-in minutes amount
function minutesToTime(minutes)
local hh = math.floor(minutes / 60)
local mm = minutes - (hh * 60)
return hh, mm
end
-- returns true if the current time is within a time range: startH:startM and stopH:stopM
local function timeIsInRange(startH, startM, stopH, stopM)
local function getMinutes(hours, minutes)
return (hours * 60) + minutes
end
local currentMinutes = getMinutes(self.hour, self.min)
local startMinutes = getMinutes(startH, startM)
local stopMinutes = getMinutes(stopH, stopM)
if stopMinutes < startMinutes then -- add 24 hours (1440 minutes ) if endTime < startTime
if currentMinutes < stopMinutes then currentMinutes = currentMinutes + 1440 end
stopMinutes = stopMinutes + 1440
end
return ( currentMinutes >= startMinutes and currentMinutes <= stopMinutes )
end
-- returns true if self.day is on the rule: on day1,day2...
function self.ruleIsOnDay(rule)
local days = string.match(rule, '%s+on%s+(.+)$') or string.match(rule, '^%s*on%s+(.+)$')
if (isEmpty(days)) then
return nil
end
local isDayRule = false
for i,day in pairs({'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'}) do
if (string.find(days, day) ~= nil) then
isDayRule = true
break
end
end
if (not isDayRule) then
return nil
end
local days = string.match(rule, '%s+on%s+(.+)$') or string.match(rule, '^%s*on%s+(.+)$')
if (days ~= nil) then -- on <day>' was specified
local hasDayMatch = string.find(days, self.dayAbbrOfWeek)
if (hasDayMatch) then
return true
else
return false
end
end
return nil
end
-- returns true if self.week matches rule in week 1,3,4 / every odd-week, every even-week, in week 5-12,23,44
function self.ruleIsInWeek(rule)
if (string.find(rule, 'every odd week') and not ((self.week % 2) == 0)) then
return true
elseif (string.find(rule, 'every even week') and ((self.week % 2) == 0)) then
return true
elseif string.find(rule, 'every even week') or string.find(rule, 'every odd week') then
return false
end
local weeks = string.match(rule, 'in week% ([0-9%-%,% ]*)')
if (weeks == nil) then
return nil
end
-- from here on, if there is a match we return true otherwise false
-- remove spaces and add a comma
weeks = string.gsub(weeks, ' ', '') .. ',' -- remove spaces and add a ',' so we can do simple search for the number
-- do a quick scan first to see if we already have a match without needing to search for ranges
if (string.find(weeks, tostring(self.week) .. ',')) then
return true
end
-- now get the ranges
for set, from, to in string.gmatch(weeks, '(([0-9]*)-([0-9]*))') do
to = tonumber(to)
from = tonumber(from)
if (isEmpty(from) and not isEmpty(to) and self.week <= to ) then
return true
end
if (not isEmpty(from) and isEmpty(to) and self.week >= from ) then
return true
end
if (not isEmpty(from) and not isEmpty(to) and to ~= nil) then
if (self.week >= from and self.week <= to) then
return true
end
end
end
return false
end
function self.ruleIsOnDate(rule)
local dates = string.match(rule, 'on% ([0-9%*%/%,% %-]*)')
if (isEmpty(dates)) then
return nil
end
local _ = require('lodash')
local dateTable = utils.stringSplit(dates,',') -- get all date(ranges)
-- remove spaces and add a comma
dates = string.gsub(dates, ' ', '') .. ',' --remove spaces and add a , so we can do simple search for the number
-- do a quick scan first to see if we already have a match without needing to search for ranges and wildcards
for index, value in ipairs(dateTable) do
if tonumber(value:match('%d+')) == self.day and tonumber(value:match('/(%d+)')) == self.month then
return true
end
end
-- wildcards
for set, day, month in string.gmatch(dates, '(([0-9%*]*)/([0-9%*]*))') do
if (day == '*' and month ~= '*') then
if (self.month == tonumber(month)) then
return true
end
end
if (day ~= '*' and month == '*') then
if (self.day == tonumber(day)) then
return true
end
end
end
local getParts = function(set)
local day, month = string.match(set, '([0-9%*]+)/([0-9%*]+)')
return day and tonumber( day ), month and tonumber( month )
end
--now get the ranges
for fromSet, toSet in string.gmatch(dates, '([0-9%/%*]*)-([0-9%/%*]*)') do
local fromDay, toDay, fromMonth, toMonth
if (isEmpty(fromSet) and not isEmpty(toSet)) then
toDay, toMonth = getParts(toSet)
if ((self.month < toMonth) or (self.month == toMonth and self.day <= toDay)) then
return true
end
elseif (not isEmpty(fromSet) and isEmpty(toSet)) then
fromDay, fromMonth = getParts(fromSet)
if ((self.month > fromMonth) or (self.month == fromMonth and self.day >= fromDay)) then
return true
end
else
toDay, toMonth = getParts(toSet)
fromDay, fromMonth = getParts(fromSet)
if
(
( self.month > fromMonth and self.month < toMonth ) or
( fromMonth == toMonth and self.month == fromMonth and self.day >= fromDay and self.day <= toDay ) or
( self.month == fromMonth and toMonth < fromMonth and self.day >= fromDay ) or
( self.month == fromMonth and toMonth > fromMonth and self.day >= fromDay ) or
( self.month == toMonth and toMonth < fromMonth and self.day <= toDay ) or
( self.month == toMonth and toMonth > fromMonth and self.day <= toDay )
) then
return true
end
end
end
return false
end
function self.ruleIsBeforeAstrologicalMoment(rule)
local minutes = tonumber(string.match(rule, '%.*(%d+)%s+minutes%s+before%.*'))
local astronomicalString = rule:match('minutes%s+before%s+(%a+)') or ''
local moment = tonumber(gTimes[astronomicalString .. 'inminutes']) or ''
if minutes ~= nil and moment ~= '' then
return ( self.minutesnow + minutes ) == moment
end
return nil
end
function self.ruleIsAfterAstrologicalMoment(rule)
local minutes = tonumber(string.match(rule, '%.*(%d+)%s+minutes%s+after%.*'))
local astronomicalString = rule:match('minutes%s+after%s+(%a+)') or ''
local moment = tonumber(gTimes[astronomicalString .. 'inminutes']) or ''
if minutes ~= nil and moment ~= '' then
return ( self.minutesnow - minutes ) == moment
end
return nil
end
-- returns true if self.time is at a astronomical moment
-- sunset, sunrise, CivilTwilightEnd, NautTwilightStart, etc
function self.ruleIsAtAstronomicalMoment(rule)
local astronomicalString = rule:match('at%s+(%a+)') or ''
local moment = tonumber(gTimes[astronomicalString .. 'inminutes']) or -1
return moment == self.minutesnow or nil
end
-- returns true if self.time is at a astronomical range
-- daytime, nightime, etc
function self.ruleIsAtAstronomicalRange(rule)
local astronomicalString = rule:match('at%s+(%a+)') or ''
if type(gTimes[astronomicalString]) == 'boolean' then
return gTimes[astronomicalString] or nil
end
end
-- returns true if self.min fits in the every xx minute /every other minute/every minute rule
function self.ruleMatchesMinuteSpecification(rule)
local function fitsMinuteRule(m)
return (self.min / m == math.floor(self.min / m))
end
if (string.find(rule, 'every minute')) then
return true
end
if (string.find(rule, 'every other minute')) then
return fitsMinuteRule(2)
end
local minutes = tonumber(string.match(rule, 'every (%d+) minutes'))
if (minutes ~= nil) then
if ((60 / minutes) ~= math.floor(60 / minutes) or minutes >= 60) then
self.utils.log(rule .. ' is not a valid timer definition. Can only run every 1, 2, 3, 4, 5, 6, 10, 12, 15, 20 and 30 minutes.', utils.LOG_ERROR)
return false
end
return fitsMinuteRule(minutes)
end
return nil -- nothing specified for this rule
end
-- return true if self.hour fits the every hour/every other hour/every xx hours rule
function self.ruleMatchesHourSpecification(rule)
local function fitsHourRule(h)
-- always fit every whole hour (hence the self.min == 0)
return (self.hour / h == math.floor(self.hour / h) and self.min == 0)
end
if (string.find(rule, 'every hour')) then
return fitsHourRule(1)
end
if (string.find(rule, 'every other hour')) then
return fitsHourRule(2)
end
local hours = tonumber(string.match(rule, 'every% (%d+)% hours'))
if (hours ~= nil) then
if ((24 / hours) ~= math.floor(24 / hours) or hours >= 24) then
self.utils.log(rule .. ' is not a valid timer definition. Can only run every 1, 2, 3, 4, 6, 8, 12 hours.', utils.LOG_ERROR)
return false
end
return fitsHourRule(hours)
end
return nil -- nothing specified for this rule
end
-- return true if self.time is at hh:mm / *:mm/ hh:*
function self.ruleMatchesTime(rule)
local hh, mm
local timePattern = 'at% ([0-9%*]+):([0-9%*]+)'
hh, mm = string.match(rule, timePattern .. '% ')
if (hh == nil or mm == nil) then
-- check for end-of string
hh, mm = string.match(rule, timePattern .. '$')
end
if (hh ~= nil) then
if (mm == '*') then
return (self.hour == tonumber(hh))
elseif (hh == '*') then
return (self.min == tonumber(mm))
--elseif (hh ~= '*' and hh ~= '*') then
else
hh = tonumber(hh)
mm = tonumber(mm)
if (hh ~= nil and mm ~= nil) then
return (mm == self.min and hh == self.hour)
else
-- invalid
return false
end
end
end
return nil -- no at hh:mm found in rule
end
-- returns true if self.time is in time range: at hh:mm-hh:mm
function self.ruleMatchesTimeRange(rule)
local fromH, fromM, toH, toM = string.match(rule, 'at% ([0-9%*]+):([0-9%*]+)-([0-9%*]+):([0-9%*]+)')
if (fromH ~= nil) then
-- all will be nil if fromH is nil
fromH = tonumber(fromH)
fromM = tonumber(fromM)
toH = tonumber(toH)
toM = tonumber(toM)
if (fromH == nil or fromM == nil or toH == nil or toM == nil) then -- invalid format
return false
else
return timeIsInRange(fromH, fromM, toH, toM)
end
end
return nil
end
-- returns true if 'moment' matches with any of the moment-like time rules
function getMoment(moment)
local minutes, astrologicalMoment
-- first check if it in the form of hh:mm
hh, mm = string.match(moment, '([0-9]+):([0-9]+)')
if (hh ~= nil and mm ~= nil) then
return tonumber(hh), tonumber(mm)
end
-- check if it is before astroMoment
minutes, astrologicalMoment = string.match(moment, '%.*(%d+)%s+minutes%s+before%s+(%a+)')
if minutes and astrologicalMoment then
return minutesToTime(gTimes[astrologicalMoment .. 'inminutes'] - tonumber(minutes))
end
-- check if it is after astroMoment
minutes, astrologicalMoment = string.match(moment, '%.*(%d+)%s+minutes%s+after%s+(%a+)')
if minutes and astrologicalMoment then
return minutesToTime(gTimes[astrologicalMoment .. 'inminutes'] + tonumber(minutes))
end
-- check at astroMoment
if (gTimes[moment .. 'inminutes']) then
return minutesToTime(gTimes[moment .. 'inminutes'])
end
return nil
end
-- returns true if self.time is in a between xx and yy range
function self.ruleMatchesBetweenRange(rule)
local from, to = string.match(rule, 'between% (.+)% and% (.+)')
if (from == nil or to == nil) then
return nil
end
local fromHH, fromMM, toHH, toMM
fromHH, fromMM = getMoment(from)
toHH, toMM = getMoment(to)
if (fromHH == nil or fromMM == nil or toHH == nil or toMM == nil) then
return nil
end
return timeIsInRange(fromHH, fromMM, toHH, toMM)
end
-- remove seconds from timeStrings 'at 12:23:56-23:45:00 ==>> 'at 12:23-23:45'
-- to allow use of rawTime in matchesRule
local function sanitize(rule)
if not rule:match("(%w+%:%w+:%w+)") then return rule end
for strippedTime in rule:gmatch("(%w+%:%w+)") do
if strippedTime:match("(%w+%:%w+:%w+)") then rule = rule:gsub(rule:match("(%w+%:%w+:%w+)"),rule:match(strippedTime)) end
end
return rule
end
local function populateAstrotimes()
local LOOKUPASTRO =
{
CivTwilightEndInMinutes = 'civiltwilightendinminutes',
AstrTwilightStartInMinutes = 'astronomicaltwilightstartinminutes',
AstrTwilightEndInMinutes = 'astronomicaltwilightendinminutes',
SunAtSouthInMinutes = 'solarnooninminutes',
NautTwilightEndInMinutes = 'nauticaltwilightendinminutes',
NautTwilightStartInMinutes = 'nauticaltwilightstartinminutes',
CivTwilightStartInMinutes = 'civiltwilightstartinminutes',
Daytime = 'daytime',
SunsetInMinutes = 'sunsetinminutes',
Civildaytime = 'civildaytime',
Civilnighttime = 'civilnighttime',
SunriseInMinutes = 'sunriseinminutes',
Nighttime = 'nighttime',
}
gTimes = {}
for originalName, dzVentsName in pairs(LOOKUPASTRO) do
gTimes[dzVentsName] = _G.timeofday[originalName]
end
gTimes.sunatsouthinMinutes = gTimes.solarnooninminutes
gTimes.astronomicaldaytime = ( self.minutesnow <= (gTimes.astronomicaltwilightendinminutes or 0) and self.minutesnow >= (gTimes.astronomicaltwilightstartinminutes or 9999))
gTimes.nauticaldaytime = (self.minutesnow <= (gTimes.nauticaltwilightendinminutes or 0) and self.minutesnow >= (gTimes.nauticaltwilightstartinminutes or 9999))
gTimes.nauticalnighttime = not(gTimes.nauticaldaytime)
gTimes.astronomicalnighttime = not(gTimes.astronomicaldaytime)
return true
end
-- returns true if self.time matches the rule
function self.matchesRule(rule, processed)
if type(rule) == 'string' and ( string.len(rule == nil and "" or rule) == 0) then
return false
-- split into atomic time rules to simplify and speedup processing
elseif type(rule) == 'string' and not(processed) then
populateAstrotimes()
local rule = rule:lower()
local validPositiveRules = true
local negativeKeyword = 'except'
local function ruleSplit(rawRule)
local ruleKeywords = 'on, between, every, in'
local allRules = {}
local aRule = ''
for ruleWord in string.gmatch(rawRule, "[%S]+") do -- all "words" separated by spaces
if ruleKeywords:find(ruleWord) and aRule ~= '' then
table.insert(allRules, aRule)
aRule = ''
end
aRule = ( aRule == '' and ruleWord ) or ( aRule .. ' ' .. ruleWord )
end
if aRule ~= '' then table.insert(allRules, aRule) end
return allRules or {}
end
local positiveRules, negativeRules
local exceptPosition = rule:find(negativeKeyword)
if exceptPosition then
positiveRules = ruleSplit(rule:sub(1, exceptPosition - 1))
negativeRules = ruleSplit(rule:sub(exceptPosition + #negativeKeyword, #rule))
else
negativeRules = nil
positiveRules = ruleSplit(rule)
end
for _, aRule in ipairs(positiveRules) do
validPositiveRules = validPositiveRules and self.matchesRule(aRule, true)
if validPositiveRules == false then return false end
end
if exceptPosition then
for _, aRule in ipairs( negativeRules) do
if self.matchesRule(aRule, true) then return false end
end
end
return validPositiveRules
end
local res
local total = false
local function updateTotal(res)
total = res ~= nil and (total or res) or total
end
res = self.ruleIsInWeek(rule)
if (res == false) then --in week <weeks> was specified but 'now' is not on any of the specified weeks
return false
end
updateTotal(res)
res = self.ruleIsOnDay(rule) -- range
if (res == false) then -- on <days> was specified but 'now' is not on any of the specified days
return false
end
updateTotal(res)
res = self.ruleIsOnDate(rule)
if (res == false) then -- on date <dates> was specified but 'now' is not on any of the specified dates
return false
end
updateTotal(res)
local _between = self.ruleMatchesBetweenRange(rule) -- range
if (_between == false) then -- rule had between xxx and yyy is not in that range now
return false
end
res = _between
updateTotal(res)
if (_between == nil) then -- there was not a between rule.
-- A between-range can have before/after sunrise/set rules so it cannot be combined with these here
res = self.ruleIsBeforeAstrologicalMoment(rule) -- moment
if (res == false) then -- (sub)rule has before xxstart, xxend, sunset, sunrise or solarnoon
return false
end
updateTotal(res)
res = self.ruleIsAfterAstrologicalMoment(rule) -- moment
if (res == false) then -- (sub)rule has after xxstart, xxend, sunset, sunrise or solarnoon
return false
end
updateTotal(res)
end
res = self.ruleIsAtAstronomicalMoment(rule)
if (res == false) then -- rule has at xxstart, xxend, sunset, sunrise or solarnoon
return false
end
updateTotal(res)
res = self.ruleIsAtAstronomicalRange(rule)
if (res == false) then -- rule has at xxdaytime or xx nighttime
return false
end
updateTotal(res)
res = self.ruleMatchesHourSpecification(rule) -- moment
if (res == false) then -- rule has every xx hour but its not the right time
return false
end
updateTotal(res)
res = self.ruleMatchesMinuteSpecification(rule) -- moment
if (res == false) then -- rule has every xx minute but its not the right time
return false
end
updateTotal(res)
rule = sanitize(rule)
res = self.ruleMatchesTime(rule) -- moment / range
if (res == false) then -- rule has at hh:mm part but didn't match (or was invalid)
return false
end
updateTotal(res)
res = self.ruleMatchesTimeRange(sanitize(rule)) -- range
if (res == false) then -- rule has at hh:mm-hh:mm but time is not in that range now
return false
end
updateTotal(res)
return total
end
return self
end
return Time
Debian buster, bullseye on RPI-4, Intel NUC.
dz Beta, Z-Wave, RFLink, RFXtrx433e, P1, Youless, Hue, Yeelight, Xiaomi, MQTT
==>> dzVents wiki
dz Beta, Z-Wave, RFLink, RFXtrx433e, P1, Youless, Hue, Yeelight, Xiaomi, MQTT
==>> dzVents wiki
-
- Posts: 18
- Joined: Sunday 12 August 2018 11:25
- Target OS: Raspberry Pi / ODroid
- Domoticz version: Beta
- Location: Netherlands
- Contact:
Re: timer
Just updated, lets see what happends tonight
Ill let you know
Thanks again for the fast fix etc...
Ill let you know
Thanks again for the fast fix etc...
waaren wrote: ↑Monday 15 March 2021 23:52Should be fixed in build 13081. If you need it now you can already test by replacing <domoticz dir>/dzVents/runtime/Time.lua with below code but only if you are using a version with the described bug.
Code: Select all
local utils = require('Utils') local _MS -- kind of a cache so we don't have to extract ms every time local gTimes -- local isEmpty = function(v) return (v == nil or v == '') end local function getTimezone() local diff = os.difftime(os.time(), os.time(os.date("!*t"))) return ( os.date('!*t').isdst and ( diff + 3600 ) ) or diff end local function getSMs(s) local ms = 0 local parts = utils.stringSplit(s, '.') -- do string splitting instead of math stuff.. can't seem to get the floating points right s = tonumber(parts[1]) if (parts[2] ~= nil) then -- should always be three digits!! ms = tonumber(parts[2]) end return s, ms end local function parseDate(sDate) return string.match(sDate, "(%d+)%-(%d+)%-(%d+)[%sT]+(%d+):(%d+):([0-9%.]+)") end local function _getMS() if (_MS == nil) then local dzCurrentTime = _G.globalvariables.currentTime local y, mon, d, h, min, s = parseDate(dzCurrentTime) local ms s, ms = getSMs(s) _MS = ms end return _MS end local getDiffParts = function(secDiff, ms, offsetMS) -- ms is the ms part that should be added to secDiff to get 'sss.ms' if (ms == nil) then ms = 0 end if (offsetMS == nil) then offsetMS = 0 end local secs = secDiff local msDiff msDiff = (secs * 1000) - ms + offsetMS if (math.abs(msDiff) < 1000) then secs = 0 end local minDiff = math.floor(math.abs((secs / 60))) local hourDiff = math.floor(math.abs((secs / 3600))) local dayDiff = math.floor(math.abs((secs / 86400))) local cmp if (msDiff == 0) then cmp = 0 elseif (msDiff > 0) then cmp = -1 else cmp = 1 end return math.abs(msDiff), math.abs(secs), minDiff, hourDiff, dayDiff, cmp end -- week functions as taken from http://lua-users.org/wiki/WeekNumberInYear -- Get day of a week at year beginning --(tm can be any date and will be forced to 1st of january same year) -- return 1=mon 7=sun local function getYearBeginDayOfWeek(tm) local yearBegin = os.time{year=os.date("*t",tm).year,month=1,day=1} local yearBeginDayOfWeek = tonumber(os.date("%w",yearBegin)) -- sunday correct from 0 -> 7 if(yearBeginDayOfWeek == 0) then yearBeginDayOfWeek = 7 end return yearBeginDayOfWeek end -- tm: date (as returned from os.time) -- returns basic correction to be add for counting number of week -- weekNum = math.floor((dayOfYear + returnedNumber) / 7) + 1 -- (does not consider correction at begin and end of year) local function getDayAdd(tm) local yearBeginDayOfWeek = getYearBeginDayOfWeek(tm) local dayAdd if(yearBeginDayOfWeek < 5 ) then -- first day is week 1 dayAdd = (yearBeginDayOfWeek - 2) else -- first day is week 52 or 53 dayAdd = (yearBeginDayOfWeek - 9) end return dayAdd end -- tm is date as returned from os.time() -- return week number in year based on ISO8601 -- (week with 1st thursday since Jan 1st (including) is considered as Week 1) -- (if Jan 1st is Fri,Sat,Sun then it is part of week number from last year -> 52 or 53) local function getWeekNumberOfYear(tm) local dayOfYear = os.date("%j",tm) local dayAdd = getDayAdd(tm) local dayOfYearCorrected = dayOfYear + dayAdd if(dayOfYearCorrected < 0) then -- week of last year - decide if 52 or 53 local lastYearBegin = os.time{year=os.date("*t",tm).year-1,month=1,day=1} local lastYearEnd = os.time{year=os.date("*t",tm).year-1,month=12,day=31} dayAdd = getDayAdd(lastYearBegin) dayOfYear = dayOfYear + os.date("%j",lastYearEnd) dayOfYearCorrected = dayOfYear + dayAdd end local weekNum = math.floor((dayOfYearCorrected) / 7) + 1 if( (dayOfYearCorrected > 0) and weekNum == 53) then -- check if it is not considered as part of week 1 of next year local nextYearBegin = os.time{year=os.date("*t",tm).year+1,month=1,day=1} local yearBeginDayOfWeek = getYearBeginDayOfWeek(nextYearBegin) if(yearBeginDayOfWeek < 5 ) then weekNum = 1 end end return weekNum end local function Time(sDate, isUTC, _testMS) local LOOKUPDAYABBROFWEEK = { 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' } local LOOKUPDAYNAME = { 'Sunday', 'Monday', 'Tuesday', 'WednesDay', 'Thursday', 'Friday', 'Saturday' } local LOOKUPMONTHABBR = { 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec' } local LOOKUPMONTH = { 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' } local ms local now local time = {} local localTime = {} -- nonUTC local self local getMS = function() if (_testMS ~= nil) then return _testMS else return _getMS() end end if isUTC and isUTC == true then now = os.date('!*t') else now = os.date('*t') isUTC = false end local function makesDate() local now if (isUTC) then now = os.date('!*t') else now = os.date('*t') end local ms = _testMS == nil and getMS() or _testMS return ( now.year .. '-' .. now.month ..'-' .. now.day .. ' ' .. now.hour .. ':' .. now.min .. ':' .. now.sec .. '.' .. tostring(ms) ) end if sDate == nil or sDate == '' then sDate = makesDate() end local y, mon, d, h, min, s = parseDate(sDate) if not(y and mon and d and h and min and s) then sDate = makesDate() y, mon, d, h, min, s = parseDate(sDate) utils.log('sDate was invalid. Reset to ' .. sDate , utils.LOG_ERROR) end -- extract s and ms s, ms = getSMs(s) local dDate = os.time{year=y,month=mon,day=d,hour=h,min=min,sec=s } time = os.date('*t', dDate) local tToday = os.time{ day= now.day, year= now.year, month= now.month, hour= now.hour, min= now.min, sec= now.sec } -- calculate how many minutes that was from now local msDiff, secDiff, minDiff, hourDiff, dayDiff, cmp = getDiffParts(os.difftime(tToday, dDate), ms, getMS()) if (cmp > 0) then -- time is in the future so the xxAgo items should be negative msDiff = -msDiff secDiff = -secDiff minDiff = -minDiff hourDiff = -hourDiff dayDiff = -dayDiff end if (isUTC) then localTime = os.date('*t', os.time(time) + getTimezone()) self = localTime self.utcSystemTime = now self.utcTime = time self.utcTime.minutes = time.min self.utcTime.seconds = time.sec else self = time end self.rawDate = self.year .. '-' .. string.format("%02d", self.month) .. '-' .. string.format("%02d", self.day) self.time = string.format("%02d", self.hour) .. ':' .. string.format("%02d", self.min) self.minutesnow = self.hour * 60 + self.min self.rawTime = self.time .. ':' .. string.format("%02d", self.sec) self.rawDateTime = self.rawDate .. ' ' .. self.rawTime self.milliSeconds = ms self.milliseconds = ms self.dayAbbrOfWeek = LOOKUPDAYABBROFWEEK[self.wday] self.dayName = LOOKUPDAYNAME[self.wday] self.monthAbbrName = LOOKUPMONTHABBR[self.month] self.monthName = LOOKUPMONTH[self.month] -- Note: %V doesn't work on Windows so we have to use a custom function here -- doesn't work: self.week = tonumber(os.date('%V', dDate)) self.week = getWeekNumberOfYear(dDate) self.raw = sDate self.isToday = (now.year == time.year and now.month == time.month and now.day == time.day) self.msAgo = math.floor(msDiff) self.millisecondsAgo = self.msAgo self.minutesAgo = minDiff self.secondsAgo = math.floor(secDiff) self.hoursAgo = hourDiff self.daysAgo = dayDiff self.minutes = self.min self.seconds = self.sec self.minutesSinceMidnight = self.hour * 60 + self.min self.secondsSinceMidnight = self.minutesSinceMidnight * 60 + self.sec self.utils = utils self.isUTC = isUTC self.dDate = dDate self.isdst = time.isdst if (_G.TESTMODE) then _G = _G or {} -- Only used when testing testTime.lua _G.timeofday = _G.timeofday or {} -- Only used when testing testTime.lua function self._getUtilsInstance() return utils end end self.current = os.date('*t') -- compares to self against the provided Time object (t) function self.compare(t) if (t.raw ~= nil) then local msDiff, secDiff, minDiff, hourDiff, dayDiff, cmp = getDiffParts(os.difftime(dDate, t.dDate), t.milliseconds, ms) -- offset is 'our' ms value return { mins = minDiff, hours = hourDiff, secs = math.floor(secDiff), seconds = math.floor(secDiff), minutes = minDiff, days = dayDiff, ms = math.floor(msDiff), milliseconds = math.floor(msDiff), compare = cmp -- 0 == equal, -1==(t earlier than self), 1=(t later than self) } else utils.log('Invalid time format passed to diff. Should a Time object', utils.LOG_ERROR) end end function self.localeMonths() local months = { january = 1, february = 2, march = 3, april = 4, may = 5, june = 6, july = 7, august = 8, september = 9, october = 10, november = 11, december = 12, jan = 1, feb = 2, mar = 3, apr = 4, jun = 6, jul = 7, aug = 8, sep = 9, oct = 10, nov = 11, dec = 12 } local monthSeconds = 2678400 -- accurate enough for this purpose local startSeconds = 1577833200 -- 2020-1-1 00:00 for monthNumber = 1, 12 do months[os.date('%B', startSeconds + ( monthNumber - 1 ) * monthSeconds ):sub(1,3):lower()] = monthNumber months[os.date('%b', startSeconds + ( monthNumber - 1 ) * monthSeconds ):lower()] = monthNumber end return months end function self.dateToTimestamp(dateString, control) local tm, dateTable, format = {}, {}, {} local pattern if control and control:find('%%') then pattern = control elseif control ~= nil then -- control = format local index = 1 for word in control:gmatch("%w+") do table.insert(format, word) end for value in dateString:gmatch("%w+") do dateTable[format[index]] = value index = index + 1 end tm.year = dateTable.yyyy or ( dateTable.yy and ( dateTable.yy + 2000 )) or os.date('%Y') tm.month = dateTable.mm or ( dateTable.mmm and self.localeMonths()[dateTable.mmm:lower()]) or ( dateTable.mmmm and self.localeMonths()[dateTable.mmmm:lower()]) or 1 tm.day = dateTable.dd or 1 tm.hour = dateTable.hh or 0 tm.min = dateTable.MM or 0 tm.sec = dateTable.ss or 0 return os.time(tm) end pattern = pattern or '(%d+)%D+(%d+)%D+(%d+)%D+(%d+)%D+(%d+)' -- yyyy-mm--dd hh:mm tm.year, tm.month, tm.day, tm.hour, tm.min, tm.sec = dateString:match(pattern) return os.time(tm) end function self.timestampToDate(timestamp, humanizedPattern, offSet) local function convertMnemomic2FmtCode(humanizedPattern) local humanizedPattern = humanizedPattern or 'yyyy-mm-dd hh:MM:ss' mnemomics = { {'dddd' , '%A'}, -- full weekdayname(e.g. Wednesday) language depends on locale {'ddd' , '%a'}, -- abbreviated weekdayname(e.g. Wed) language depends on locale {'dd' , '%d'}, -- day of the month(16){[01-31] {'mmmm' , '%B'}, -- full monthname(e.g September) language depends on locale {'mmm' , '%b'}, -- abbreviated monthname(e.g. Sep) language depends on locale {'mm' , '%m'}, -- month[01-12] {'yyyy' , '%Y'}, -- 4-digit year {'yy' , '%y'}, -- two-digityear(98){[00-99] {'hh' , '%H'}, -- hour 24-hour clock){[00-23] {'ii' , '%I'}, -- hour 12-hour clock[01-12] {'MM' , '%M'}, -- minute{[00-59] {'ss' , '%S'}, -- second [00-60] {'W' , '%W'}, -- weeknumber [01-53] {'w' , '%w'}, -- weekday{[0-6] Sunday-Saturday {'datm' , '%c'}, -- date and time (e.g. 09/16/98 23:48:10) format depends on locale {'mer' , '%p'}, -- either "am" or "pm" locale {'date' , '%x'}, -- date(e.g. 09/16/98) format depends on locale {'time' , '%X'}, -- time(e.g. 23:48:10) } for _, conversion in ipairs(mnemomics) do humanizedPattern = string.gsub(humanizedPattern, conversion[1], '%' .. conversion[2]) end return humanizedPattern end local timestamp = ( timestamp or os.time() ) + ( offSet or 0 ) local dateTimeString = os.date( convertMnemomic2FmtCode(humanizedPattern) , timestamp ) if dateTimeString:find('nZero') then -- remove leading zero's return dateTimeString:gsub('nZero',''):gsub(' 0',' '):gsub('^0',''):gsub('%s*$','') end return dateTimeString:gsub('%s*$','') end function self.dateToDate(date, sourceFormat, targetFormat, offSet ) return self.timestampToDate(self.dateToTimestamp(date,sourceFormat), targetFormat, offSet) end function self.addSeconds(seconds, factor) if type(seconds) ~= 'number' then self.utils.log(tostring(seconds) .. ' is not a valid parameter to this function. Please change to use a number value!', utils.LOG_ERROR) else factor = factor or 1 return Time( os.date("%Y-%m-%d %X", self.dDate + factor * math.floor(seconds) )) end end function self.addDays(days) return self.addSeconds(days, 24 * 3600) end function self.addHours(hours) return self.addSeconds(hours, 3600) end function self.addMinutes(minutes) return self.addSeconds(minutes, 60) end function self.makeTime(oDate, isUTC) local sDate = ( type(oDate) == 'table' and os.date("%Y-%m-%d %H:%M:%S", os.time(oDate)) ) or ( tonumber(oDate) and self.timestampToDate(oDate) ) or oDate return Time(sDate, isUTC) end function self.toUTC(oDate, offset) local sDate = ( type(oDate) == 'table' and os.date("%Y-%m-%d %H:%M:%S", os.time(oDate)) ) or oDate local offset = offset or 0 return Time(sDate).addSeconds(-1 * getTimezone() + offset).raw end -- return ISO format function self.getISO() return os.date("!%Y-%m-%dT%TZ", os.time(time)) end -- returns hours part and minutes part of the passed-in minutes amount function minutesToTime(minutes) local hh = math.floor(minutes / 60) local mm = minutes - (hh * 60) return hh, mm end -- returns true if the current time is within a time range: startH:startM and stopH:stopM local function timeIsInRange(startH, startM, stopH, stopM) local function getMinutes(hours, minutes) return (hours * 60) + minutes end local currentMinutes = getMinutes(self.hour, self.min) local startMinutes = getMinutes(startH, startM) local stopMinutes = getMinutes(stopH, stopM) if stopMinutes < startMinutes then -- add 24 hours (1440 minutes ) if endTime < startTime if currentMinutes < stopMinutes then currentMinutes = currentMinutes + 1440 end stopMinutes = stopMinutes + 1440 end return ( currentMinutes >= startMinutes and currentMinutes <= stopMinutes ) end -- returns true if self.day is on the rule: on day1,day2... function self.ruleIsOnDay(rule) local days = string.match(rule, '%s+on%s+(.+)$') or string.match(rule, '^%s*on%s+(.+)$') if (isEmpty(days)) then return nil end local isDayRule = false for i,day in pairs({'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'}) do if (string.find(days, day) ~= nil) then isDayRule = true break end end if (not isDayRule) then return nil end local days = string.match(rule, '%s+on%s+(.+)$') or string.match(rule, '^%s*on%s+(.+)$') if (days ~= nil) then -- on <day>' was specified local hasDayMatch = string.find(days, self.dayAbbrOfWeek) if (hasDayMatch) then return true else return false end end return nil end -- returns true if self.week matches rule in week 1,3,4 / every odd-week, every even-week, in week 5-12,23,44 function self.ruleIsInWeek(rule) if (string.find(rule, 'every odd week') and not ((self.week % 2) == 0)) then return true elseif (string.find(rule, 'every even week') and ((self.week % 2) == 0)) then return true elseif string.find(rule, 'every even week') or string.find(rule, 'every odd week') then return false end local weeks = string.match(rule, 'in week% ([0-9%-%,% ]*)') if (weeks == nil) then return nil end -- from here on, if there is a match we return true otherwise false -- remove spaces and add a comma weeks = string.gsub(weeks, ' ', '') .. ',' -- remove spaces and add a ',' so we can do simple search for the number -- do a quick scan first to see if we already have a match without needing to search for ranges if (string.find(weeks, tostring(self.week) .. ',')) then return true end -- now get the ranges for set, from, to in string.gmatch(weeks, '(([0-9]*)-([0-9]*))') do to = tonumber(to) from = tonumber(from) if (isEmpty(from) and not isEmpty(to) and self.week <= to ) then return true end if (not isEmpty(from) and isEmpty(to) and self.week >= from ) then return true end if (not isEmpty(from) and not isEmpty(to) and to ~= nil) then if (self.week >= from and self.week <= to) then return true end end end return false end function self.ruleIsOnDate(rule) local dates = string.match(rule, 'on% ([0-9%*%/%,% %-]*)') if (isEmpty(dates)) then return nil end local _ = require('lodash') local dateTable = utils.stringSplit(dates,',') -- get all date(ranges) -- remove spaces and add a comma dates = string.gsub(dates, ' ', '') .. ',' --remove spaces and add a , so we can do simple search for the number -- do a quick scan first to see if we already have a match without needing to search for ranges and wildcards for index, value in ipairs(dateTable) do if tonumber(value:match('%d+')) == self.day and tonumber(value:match('/(%d+)')) == self.month then return true end end -- wildcards for set, day, month in string.gmatch(dates, '(([0-9%*]*)/([0-9%*]*))') do if (day == '*' and month ~= '*') then if (self.month == tonumber(month)) then return true end end if (day ~= '*' and month == '*') then if (self.day == tonumber(day)) then return true end end end local getParts = function(set) local day, month = string.match(set, '([0-9%*]+)/([0-9%*]+)') return day and tonumber( day ), month and tonumber( month ) end --now get the ranges for fromSet, toSet in string.gmatch(dates, '([0-9%/%*]*)-([0-9%/%*]*)') do local fromDay, toDay, fromMonth, toMonth if (isEmpty(fromSet) and not isEmpty(toSet)) then toDay, toMonth = getParts(toSet) if ((self.month < toMonth) or (self.month == toMonth and self.day <= toDay)) then return true end elseif (not isEmpty(fromSet) and isEmpty(toSet)) then fromDay, fromMonth = getParts(fromSet) if ((self.month > fromMonth) or (self.month == fromMonth and self.day >= fromDay)) then return true end else toDay, toMonth = getParts(toSet) fromDay, fromMonth = getParts(fromSet) if ( ( self.month > fromMonth and self.month < toMonth ) or ( fromMonth == toMonth and self.month == fromMonth and self.day >= fromDay and self.day <= toDay ) or ( self.month == fromMonth and toMonth < fromMonth and self.day >= fromDay ) or ( self.month == fromMonth and toMonth > fromMonth and self.day >= fromDay ) or ( self.month == toMonth and toMonth < fromMonth and self.day <= toDay ) or ( self.month == toMonth and toMonth > fromMonth and self.day <= toDay ) ) then return true end end end return false end function self.ruleIsBeforeAstrologicalMoment(rule) local minutes = tonumber(string.match(rule, '%.*(%d+)%s+minutes%s+before%.*')) local astronomicalString = rule:match('minutes%s+before%s+(%a+)') or '' local moment = tonumber(gTimes[astronomicalString .. 'inminutes']) or '' if minutes ~= nil and moment ~= '' then return ( self.minutesnow + minutes ) == moment end return nil end function self.ruleIsAfterAstrologicalMoment(rule) local minutes = tonumber(string.match(rule, '%.*(%d+)%s+minutes%s+after%.*')) local astronomicalString = rule:match('minutes%s+after%s+(%a+)') or '' local moment = tonumber(gTimes[astronomicalString .. 'inminutes']) or '' if minutes ~= nil and moment ~= '' then return ( self.minutesnow - minutes ) == moment end return nil end -- returns true if self.time is at a astronomical moment -- sunset, sunrise, CivilTwilightEnd, NautTwilightStart, etc function self.ruleIsAtAstronomicalMoment(rule) local astronomicalString = rule:match('at%s+(%a+)') or '' local moment = tonumber(gTimes[astronomicalString .. 'inminutes']) or -1 return moment == self.minutesnow or nil end -- returns true if self.time is at a astronomical range -- daytime, nightime, etc function self.ruleIsAtAstronomicalRange(rule) local astronomicalString = rule:match('at%s+(%a+)') or '' if type(gTimes[astronomicalString]) == 'boolean' then return gTimes[astronomicalString] or nil end end -- returns true if self.min fits in the every xx minute /every other minute/every minute rule function self.ruleMatchesMinuteSpecification(rule) local function fitsMinuteRule(m) return (self.min / m == math.floor(self.min / m)) end if (string.find(rule, 'every minute')) then return true end if (string.find(rule, 'every other minute')) then return fitsMinuteRule(2) end local minutes = tonumber(string.match(rule, 'every (%d+) minutes')) if (minutes ~= nil) then if ((60 / minutes) ~= math.floor(60 / minutes) or minutes >= 60) then self.utils.log(rule .. ' is not a valid timer definition. Can only run every 1, 2, 3, 4, 5, 6, 10, 12, 15, 20 and 30 minutes.', utils.LOG_ERROR) return false end return fitsMinuteRule(minutes) end return nil -- nothing specified for this rule end -- return true if self.hour fits the every hour/every other hour/every xx hours rule function self.ruleMatchesHourSpecification(rule) local function fitsHourRule(h) -- always fit every whole hour (hence the self.min == 0) return (self.hour / h == math.floor(self.hour / h) and self.min == 0) end if (string.find(rule, 'every hour')) then return fitsHourRule(1) end if (string.find(rule, 'every other hour')) then return fitsHourRule(2) end local hours = tonumber(string.match(rule, 'every% (%d+)% hours')) if (hours ~= nil) then if ((24 / hours) ~= math.floor(24 / hours) or hours >= 24) then self.utils.log(rule .. ' is not a valid timer definition. Can only run every 1, 2, 3, 4, 6, 8, 12 hours.', utils.LOG_ERROR) return false end return fitsHourRule(hours) end return nil -- nothing specified for this rule end -- return true if self.time is at hh:mm / *:mm/ hh:* function self.ruleMatchesTime(rule) local hh, mm local timePattern = 'at% ([0-9%*]+):([0-9%*]+)' hh, mm = string.match(rule, timePattern .. '% ') if (hh == nil or mm == nil) then -- check for end-of string hh, mm = string.match(rule, timePattern .. '$') end if (hh ~= nil) then if (mm == '*') then return (self.hour == tonumber(hh)) elseif (hh == '*') then return (self.min == tonumber(mm)) --elseif (hh ~= '*' and hh ~= '*') then else hh = tonumber(hh) mm = tonumber(mm) if (hh ~= nil and mm ~= nil) then return (mm == self.min and hh == self.hour) else -- invalid return false end end end return nil -- no at hh:mm found in rule end -- returns true if self.time is in time range: at hh:mm-hh:mm function self.ruleMatchesTimeRange(rule) local fromH, fromM, toH, toM = string.match(rule, 'at% ([0-9%*]+):([0-9%*]+)-([0-9%*]+):([0-9%*]+)') if (fromH ~= nil) then -- all will be nil if fromH is nil fromH = tonumber(fromH) fromM = tonumber(fromM) toH = tonumber(toH) toM = tonumber(toM) if (fromH == nil or fromM == nil or toH == nil or toM == nil) then -- invalid format return false else return timeIsInRange(fromH, fromM, toH, toM) end end return nil end -- returns true if 'moment' matches with any of the moment-like time rules function getMoment(moment) local minutes, astrologicalMoment -- first check if it in the form of hh:mm hh, mm = string.match(moment, '([0-9]+):([0-9]+)') if (hh ~= nil and mm ~= nil) then return tonumber(hh), tonumber(mm) end -- check if it is before astroMoment minutes, astrologicalMoment = string.match(moment, '%.*(%d+)%s+minutes%s+before%s+(%a+)') if minutes and astrologicalMoment then return minutesToTime(gTimes[astrologicalMoment .. 'inminutes'] - tonumber(minutes)) end -- check if it is after astroMoment minutes, astrologicalMoment = string.match(moment, '%.*(%d+)%s+minutes%s+after%s+(%a+)') if minutes and astrologicalMoment then return minutesToTime(gTimes[astrologicalMoment .. 'inminutes'] + tonumber(minutes)) end -- check at astroMoment if (gTimes[moment .. 'inminutes']) then return minutesToTime(gTimes[moment .. 'inminutes']) end return nil end -- returns true if self.time is in a between xx and yy range function self.ruleMatchesBetweenRange(rule) local from, to = string.match(rule, 'between% (.+)% and% (.+)') if (from == nil or to == nil) then return nil end local fromHH, fromMM, toHH, toMM fromHH, fromMM = getMoment(from) toHH, toMM = getMoment(to) if (fromHH == nil or fromMM == nil or toHH == nil or toMM == nil) then return nil end return timeIsInRange(fromHH, fromMM, toHH, toMM) end -- remove seconds from timeStrings 'at 12:23:56-23:45:00 ==>> 'at 12:23-23:45' -- to allow use of rawTime in matchesRule local function sanitize(rule) if not rule:match("(%w+%:%w+:%w+)") then return rule end for strippedTime in rule:gmatch("(%w+%:%w+)") do if strippedTime:match("(%w+%:%w+:%w+)") then rule = rule:gsub(rule:match("(%w+%:%w+:%w+)"),rule:match(strippedTime)) end end return rule end local function populateAstrotimes() local LOOKUPASTRO = { CivTwilightEndInMinutes = 'civiltwilightendinminutes', AstrTwilightStartInMinutes = 'astronomicaltwilightstartinminutes', AstrTwilightEndInMinutes = 'astronomicaltwilightendinminutes', SunAtSouthInMinutes = 'solarnooninminutes', NautTwilightEndInMinutes = 'nauticaltwilightendinminutes', NautTwilightStartInMinutes = 'nauticaltwilightstartinminutes', CivTwilightStartInMinutes = 'civiltwilightstartinminutes', Daytime = 'daytime', SunsetInMinutes = 'sunsetinminutes', Civildaytime = 'civildaytime', Civilnighttime = 'civilnighttime', SunriseInMinutes = 'sunriseinminutes', Nighttime = 'nighttime', } gTimes = {} for originalName, dzVentsName in pairs(LOOKUPASTRO) do gTimes[dzVentsName] = _G.timeofday[originalName] end gTimes.sunatsouthinMinutes = gTimes.solarnooninminutes gTimes.astronomicaldaytime = ( self.minutesnow <= (gTimes.astronomicaltwilightendinminutes or 0) and self.minutesnow >= (gTimes.astronomicaltwilightstartinminutes or 9999)) gTimes.nauticaldaytime = (self.minutesnow <= (gTimes.nauticaltwilightendinminutes or 0) and self.minutesnow >= (gTimes.nauticaltwilightstartinminutes or 9999)) gTimes.nauticalnighttime = not(gTimes.nauticaldaytime) gTimes.astronomicalnighttime = not(gTimes.astronomicaldaytime) return true end -- returns true if self.time matches the rule function self.matchesRule(rule, processed) if type(rule) == 'string' and ( string.len(rule == nil and "" or rule) == 0) then return false -- split into atomic time rules to simplify and speedup processing elseif type(rule) == 'string' and not(processed) then populateAstrotimes() local rule = rule:lower() local validPositiveRules = true local negativeKeyword = 'except' local function ruleSplit(rawRule) local ruleKeywords = 'on, between, every, in' local allRules = {} local aRule = '' for ruleWord in string.gmatch(rawRule, "[%S]+") do -- all "words" separated by spaces if ruleKeywords:find(ruleWord) and aRule ~= '' then table.insert(allRules, aRule) aRule = '' end aRule = ( aRule == '' and ruleWord ) or ( aRule .. ' ' .. ruleWord ) end if aRule ~= '' then table.insert(allRules, aRule) end return allRules or {} end local positiveRules, negativeRules local exceptPosition = rule:find(negativeKeyword) if exceptPosition then positiveRules = ruleSplit(rule:sub(1, exceptPosition - 1)) negativeRules = ruleSplit(rule:sub(exceptPosition + #negativeKeyword, #rule)) else negativeRules = nil positiveRules = ruleSplit(rule) end for _, aRule in ipairs(positiveRules) do validPositiveRules = validPositiveRules and self.matchesRule(aRule, true) if validPositiveRules == false then return false end end if exceptPosition then for _, aRule in ipairs( negativeRules) do if self.matchesRule(aRule, true) then return false end end end return validPositiveRules end local res local total = false local function updateTotal(res) total = res ~= nil and (total or res) or total end res = self.ruleIsInWeek(rule) if (res == false) then --in week <weeks> was specified but 'now' is not on any of the specified weeks return false end updateTotal(res) res = self.ruleIsOnDay(rule) -- range if (res == false) then -- on <days> was specified but 'now' is not on any of the specified days return false end updateTotal(res) res = self.ruleIsOnDate(rule) if (res == false) then -- on date <dates> was specified but 'now' is not on any of the specified dates return false end updateTotal(res) local _between = self.ruleMatchesBetweenRange(rule) -- range if (_between == false) then -- rule had between xxx and yyy is not in that range now return false end res = _between updateTotal(res) if (_between == nil) then -- there was not a between rule. -- A between-range can have before/after sunrise/set rules so it cannot be combined with these here res = self.ruleIsBeforeAstrologicalMoment(rule) -- moment if (res == false) then -- (sub)rule has before xxstart, xxend, sunset, sunrise or solarnoon return false end updateTotal(res) res = self.ruleIsAfterAstrologicalMoment(rule) -- moment if (res == false) then -- (sub)rule has after xxstart, xxend, sunset, sunrise or solarnoon return false end updateTotal(res) end res = self.ruleIsAtAstronomicalMoment(rule) if (res == false) then -- rule has at xxstart, xxend, sunset, sunrise or solarnoon return false end updateTotal(res) res = self.ruleIsAtAstronomicalRange(rule) if (res == false) then -- rule has at xxdaytime or xx nighttime return false end updateTotal(res) res = self.ruleMatchesHourSpecification(rule) -- moment if (res == false) then -- rule has every xx hour but its not the right time return false end updateTotal(res) res = self.ruleMatchesMinuteSpecification(rule) -- moment if (res == false) then -- rule has every xx minute but its not the right time return false end updateTotal(res) rule = sanitize(rule) res = self.ruleMatchesTime(rule) -- moment / range if (res == false) then -- rule has at hh:mm part but didn't match (or was invalid) return false end updateTotal(res) res = self.ruleMatchesTimeRange(sanitize(rule)) -- range if (res == false) then -- rule has at hh:mm-hh:mm but time is not in that range now return false end updateTotal(res) return total end return self end return Time
-
- Posts: 18
- Joined: Sunday 12 August 2018 11:25
- Target OS: Raspberry Pi / ODroid
- Domoticz version: Beta
- Location: Netherlands
- Contact:
Re: timer [SOLVED]
Problem has been solved! Case can be closed
Thanks again for the fast fix!
Thanks again for the fast fix!
Who is online
Users browsing this forum: No registered users and 1 guest