Jump to content

Scheduling scenes. The missing %% time trigger...


Recommended Posts

Edit: Sept.7. Updated to v1.1 - thanks to @10der for finding bugs and suggesting improvements.

Edit: Sept.7. Updated to v1.2 - bug fixes and support for catchup

Edit: Sept.7. Updated to v1.3 - tabs in time rules changed to space

Edit: Sept.7. Updated to v1.4 - more bug fixes, thanks @10der

Edit: Sept.8. Updated to v1.5 - support for "long date" formats and day numbers

Edit: Sept.8. Updated to v1.6 - Dang. Cut and paste error in v1.5. fixed.

Edit: Sept.9. Updated to v1.7 Beta - Code restructuring, faster conditions, sunset/sunrise implementation to fix midnight "bug". Support for dawn and dusk.

Edit: Sep.14. Updated to v1.8B - Fix split and tabs bugs

Edit: Sep.15. Updated to v1.9B - sunset calculations broken - fixed

Edit: Sep.29. Updated to v2.0 - no major bugs for 2 weeks :-)

Edit: Oct.6. Updated to v2.1 - bug was introduced for keyword weekends - fixed.

Edit: Oct.6. Updated to v2.2 - 2.1 caused another bug - fixed.

Edit: Oct.10. Updated to v2.3 - Improved time format. HH:MM+/-hh:mm allowed. Ex. 10:00-00:05. Good to have if used with variables. Ex. <TIME>+00:05

 

Scenes can trigger on devices changing status (%% properties), when globals change values (%% globals) and events (%% events) etc.

However, there is no trigger for time or timers - to allow a scene to be started at a specified time.

Well, here is a fix for that... and it makes it very easy to start scenes at specified times of day including sunrise/sunset, at regular intervals, at specified weekdays, and at specified months...

 

The scene below watches other scenes and allows them to declare "%% time" headers that they will be triggered on

Timer scene v2.3: Timer2_3B.lua

 

When the Timer.lua scene is installed and started (no configuration needed) we should be able to forget about it and turn our attention to scenes that we want to be triggered at given times.

It's a perfect "tool" to have running on the HC2 as it makes it really easy to add a scene and schedule it to run at a given time. A simple added header to the scene is all that is needed:

--[[
%% properties
%% events
%% globals
%% time
15:00
--]] 

print("Scene started at 15:00")

This scene will be run at 15:00 every day. To stop it from being scheduled, just remove the "%% time" lines from the header and save the scene again.

 

A more extensive example with a scene declaring multiple triggers and retrieving the trigger when the scene gets invoked;

--[[
%% properties
%% events
%% globals
%% time
log
15:00 bar
*00:15 test
--]] 

print("Scene started")

-- Redefine fibaro:getSourceTrigger
do local a,b=fibaro;b=a.getSourceTrigger;function a:getSourceTrigger()local c=b(a)local d=a:args()if type(d)=='table'and d[1]and type(d[1])=='table'then if d[1].type~= nil then return d[1]end end;return c end end

local st=fibaro:getSourceTrigger()

if st.type=='time' and st.time then -- do something when we get a time trigger...
  fibaro:debug(string.format("Triggered:%s, tag:'%s'",st.time,st.tag))
end

if st.type=='time' and st.tag=='log' then -- write out log messages from the time scene
  fibaro:debug("Time log:")
  fibaro:debug(st.log) 
end 

if st.type=='time' and st.tag=='error' then -- write out error messages from the time scene
  fibaro:debug("Time error:"..st.log)
end 

In the scene we redefine fibaro:getSourceTrigger() to return our timer as an "standard" source trigger. It's not strictly necessary as time triggers are of type 'other' with the arguments coming from fibaro:args(). However, this makes it more streamlined and plays well with standard fibaro source triggers. The timer scene is "drift free" so if you declare a timer on the hour you will be called exactly on the hour, and not slowly start to drift as is very common in many home made Lua timer loops.

Standard 'time' triggers look like

{type='time', tag=<tag>, time=<str>}

tag=<tag> is the (optional) word provided last in the time rule and helps us to identify what time rule triggered our scene.

time=<str> is the time the rule is invoked (in HH:MM format)

 

Here we also add the keyword 'log' under "%% time" to instruct the time scene to send us log statements.

Log statements come in two versions

{type='time', tag='log', log=<msg>}

Standard log messages are sent at startup and at midnight with the log msg containing information about the time rules scheduled. It's usually nice to get some feedback that the Timer scene has scheduled our scene and for what times.

{type='time', tag='error', log=<msg>}

Error messages are always sent (even without the 'log' keyword). Usually due to time rules being faulty.

 

When you enable log messages your scene will also be triggered by log messages and not only timers. You need to tell them apart. A good way to test for a time trigger that is not a log message is to test if there is a .time field in the source trigger

if sourceTrigger.type=='time' and sourceTrigger.time then
   --- do whatever
end

IN that case we know that it is a proper time trigger and not a log message, as they lack the .time field.

 

Ok, the time rules in the example are

15:00 bar

This means that the scene is called 15:00 every day, with the identifier tag "bar"

In the scene we get an sourceTrigger at 15:00 of type

{type='time', time='15:00', tag='bar'}

In the above example we just print out the tag.

*00:15 test

creates a repeating timer that triggers the scene every 15 minutes, and with the tag "test". We get a sourceTrigger of type

{type='time', time='*00:15', tag='test'}

 

The generic version of a time description looks like

%% time
<time list> <conditions> <tag>
  :
<time list> <conditions> <tag>

Examples

  • 15:00,17:00 test
    creates two timers at 15 and 17 with the same tag "test".
  • sunrise test
    'sunrise' is a valid time descriptor and evaluates to todays sunrise hour.  'sunset', "dawn", and "dusk" are available too.
  • sunrise-00:10 test
    We can do simple arithmetic on times (+/-) to create offsets.
  • 15:00 wednesday test
    timers at 15 but only on wednesday, tag "test".
  • 15:00 wed test
    days can be shortened
  • 15:00 wed,fri test
    or listed.
  • 15:00 wed..fri test
    or intervals.
  • 15:00 1..7 test
    interval with first to seventh day of the month
  • 15:00 lastday test
    Only on the last day of the month
  • 15:00 lastweek test
    only in the last week of the month (number_of_days_in_month-6..number_of_days_in_month)
  • 15:00 monday lastweek test
    conditions can be combined. True for Monday of the last week
  • 15:00 monday lastweek may..aug test
    month conditions also available.
  • *00:15 test
    interval, every 15min starting immediately (when the script starts). Ex. 12:08:33, 12:23:33, 12:38:33 ...
  • +00:15 test
    interval, every 15min, starting on next even 15min interval. Ex. 12:15:00, 12:30:00, 12:45:00 ...
  • +00:15 weekends 10:00..15:00 test
    combined with weekend test (sat..sun) and time interval (10:00 to 15:00)
  • +01:00 sunrise..sunset may..sep water
    every hour between sunrise and sunset and between May and September call scene with tag 'water'
  • 20:30 2020/05/28,2021/05/27,2022/05/26 earth_hour
    "long date" format
  • +01:00 oct/28/10:00..dec/30/07:00 tag
    Long date intervals. Year can be excluded and month can be the name of the month, and time can optionally be added at the end (not sunrise/sunset)

 

The <time list> should be seen as the times we want to schedule and the <conditions> as filters, excluding some times from the <time list>

The other way to look at the list of <conditions> is that the spaces are ANDs and commas are ORs. Ex."10:00 thu,sat lasweek tag" is "10:00 ((thu OR sat) AND lastweek), tag"

 

The complete set of conditions are:

  • <time>..<time>, time interval in HH:MM or HH:MM:SS (but also 'sunset' and 'sunrise')
  • <week day>..<week day>, day interval. mon..wed,  Thursday..Saturday
  • <day number>..<day number> - 1..7, first to second day in current month
  • <month>..<month>, month interval
  • <time>,<time> -- One or more time specifiers. Ex. 10:00,11:00
  • <week day>,<week day> -- One or more day specifiers. Ex. monday
  • <day number>,<day number> -- Ex. 1,8,15,22
  • <month>,<month> -- One or more month specifiers. Ex. june,july,august
  • <long date>..<long date> - YYYY/MM/DD/HH:MM. Year cane be left out and time part is optional
  • <long date>,<long date> - 2020/may/11/10:00, may/11/10:00, may/11
  • alldays - same as mon..sun
  • weekends - same as sat..sun
  • weekdays - same as mon..sun
  • lastday - true if last day of the month
  • lastweek - true if last week in the month
  • true - always return true. See example below using fibaro global to disable rules
  • false - always return false. Effectively disabling the rule.

...and they can be combined.

 

It's allowed to end a time rule with a comment '--'. It's just removed before parsing the rule. (Wouldn't it be nice to be able to add comments to all headers?)

Ex.

15:00 monday test -- Trigger scene every Monday at 3 PM

 

A simple example turning on a lamp (with deviceID 55) at sunset-10min and turning off the lamp at sunrise+10min on weekdays

--[[
%% time
log
sunset-00:10 weekdays turnOn
sunrise+00:10 weekdays turnOff
--]] 

print("Scene started")

-- Redefine fibaro:getSourceTrigger
do local a,b=fibaro;b=a.getSourceTrigger;function a:getSourceTrigger()local c=b(a)local d=a:args()if type(d)=='table'and d[1]and type(d[1])=='table'then if d[1].type~=nil then return d[1]end end;return c end end

local st=fibaro:getSourceTrigger()

if st.type=='time' and st.tag='turnOn' then
  fibaro:call(55,"turnOn")
end

if st.type=='time' and st.tag='turnOff' then
  fibaro:call(55,"turnOff")
end

--[[
-- Another solution
if st.type=='time' and st.time then fibaro:call(55,st.tag) end
--]]

 

The tag is useful to identify what timer you get so you don't have to test against time again (or you can use it as in the example above, as an argument to a function, fibaro:call in the above case).

We can also let the tag be the name of a function called in our scene at that time. .

--[[
%% properties
%% events
%% globals
%% time
log
15:00 bar
*00:15 test
%% autostart
--]] 

print("Scene started")

-- Redefine fibaro:getSourceTrigger
do local a,b=fibaro;b=a.getSourceTrigger;function a:getSourceTrigger()local c=b(a)local d=a:args()if type(d)=='table'and d[1]and type(d[1])=='table'then if d[1].type~=nil then return d[1]end end;return c end end

local st=fibaro:getSourceTrigger()

function bar()
  print("Bar called")
end

function test()
  print("Test called")
end

if st.type=='time' and st.time then
  fibaro:debug(string.format("Triggered:%s, tag:'%s'",st.time,st.tag))
  if _ENV[st.tag] then _ENV[st.tag]() end
end

 

A more advanced feature is that time rules allows for substituting in values from fibaro globals.

Ex.

<myTime> monday test

This will fetch the value from the the fibaro global "myTime" and insert whatever value it has instead of <myTest>. If the value was "10:00" the rule would be

10:00 monday test

The substitution can be anywhere in the rule and contain anything so be careful.

We also watch if the value of "myTime" changes and if it does it will update the timers for the scene.

One way to use this is to have a global, ex "Stop" that is set to "true" or "false". If we include that in a rule

10:00 monday <Stop> test

We can easily enable/disable the rule depending on what we set "Stop" to.

 

The scene could be improved and I'm open for suggestions. (and of course there are most likely bugs to be fixed...)

Edited by jgab
  • Like 1
  • Thanks 5
Link to post
Share on other sites

Here is a bonus hack.

We know that Lua variables are not preserved between invocations of scene instances and if we need to save a value we use fibaro globals.

 

The function "globals" defined in the scene below list the Lua globals we want to have preserved between scene invocations (and does the saving to a fibaro global automatically)

Example (using the "%% time" trigger mechanism in the scene above}

--[[
%% properties
%% events
%% globals
%% time
*00:15 test
%% autostart
--]] 

print("Scene started")

-- Redefine fibaro:getSourceTrigger and define globals()
do local a,b=fibaro;b=a.getSourceTrigger;function a:getSourceTrigger()local c=b(a)local d=a:args()if type(d)=='table'and d[1]and type(d[1])=='table'then if d[1].type=='time'or d[1].type=='timeLog'then return d[1]end end;return c end end
function globals(a)local b,c="GLOBALS"..__fibaroSceneId,fibaro;if c:getGlobalModificationTime(b)==nil then api.post("/globalVariables/",{name=b,value="{}"})end;local d=c:getGlobalValue(b)d=d and d~=""and json.decode(d)or{}for e,f in ipairs(a)do if d[f]then _ENV[f]=d[f]end end;setTimeout(function()local d={}for e,f in ipairs(a)do d[f]=_ENV[f]end;c:setGlobal(b,json.encode(d))end,1)end

globals{"counter","lastTrigger"} -- Lua globals we want to have preserved between scene invocations

counter = counter or 1 -- initialize 'counter' if not already initialized
lastTrigger = lastTrigger or 'nil' -- initialize 'lastTrigger' if not already initialized

local st=fibaro:getSourceTrigger()

if st.type=='time' then
  fibaro:debug("We have been called "..counter.." times, and previous trigger was "..json.encode(lastTrigger))
  fibaro:debug(string.format("New trigger:%s, tag:'%s'",st.time,st.tag))
  counter=counter+1
  lastTrigger=st
end

Here we keep the state of two Lua globals, 'counter' and 'lastTrigger' between invocations. We could have used a table and stored/restored ala "HomeTable", but this looks nicer :-) 

This works with "simple" scenes that triggers and terminates.

Edited by jgab
  • Like 1
Link to post
Share on other sites

I was just informed that the included code for the Time scene didn't works. It seems like some brackets got lost when pasted into the "spoiler" field.

I have attached a file in the original post with the scene code now - that should work better.

 

Edited by jgab
Link to post
Share on other sites

New version 1.1

-Improved log handling - I think...

-Time rules can end with comments

-Fibaro globals can be used to substitute in values in time rules. 

-A bit better documentation in the first post...

  • Thanks 1
Link to post
Share on other sites

Use case. Simple TimeOfDay scene that sets a couple of fibaro globals to time of day.

The fibaro global 'TOD' will be set to 'Morning','Day' etc

The fibaro global 'TIME' will be set to 'Sunrise','Sunset' and 'Hour' on even hours

The fibaro global 'SEASON' will be set to 'Summer','Winter' etc

--[[
%% properties
%% events
%% globals
%% time
log
sunrise-00:10 weekdays catch:1 TOD:Morning     -- set morning
08:00         weekdays catch:1 TOD:Day         -- set day
17:00         weekdays catch:1 TOD:Evening     -- set evening
23:30         weekdays catch:1 TOD:Night       -- set night
08:00         weekends catch:1 TOD:Morning     -- set morning
10:00         weekends catch:1 TOD:Day         -- set day
18:00         weekends catch:1 TOD:Evening     -- set evening
00:00         weekends catch:1 TOD:Night       -- set night
00:00         dec..mar catch:2 SEASON:Winter   -- set season
00:00         apr..may catch:2 SEASON:Spring   -- set season
00:00         jun..sep catch:2 SEASON:Autumn   -- set season
00:00         oct..nov catch:2 SEASON:Spring   -- set season
sunrise                        TIME:Sunrise    -- set sunrise
sunset                         TIME:Sunset     -- set sunset
+01:00                         TIME:Hour       -- set even hours
--]] 

print("TimeOfDay scene started")

-- Redefine fibaro:getSourceTrigger
do local a,b=fibaro;b=a.getSourceTrigger;function a:getSourceTrigger()local c=b(a)local d=a:args()if type(d)=='table'and d[1]and type(d[1])=='table'then if d[1].type~= nil then return d[1]end end;return c end end

local st=fibaro:getSourceTrigger()

if st.type=='time' and st.time then -- time trigger, update global
    local var,value = st.tag:match("([%w_]+):([%w_]+)")
    fibaro:debug(string.format("Setting global '%s' to '%s'",var,value))
    fibaro:setGlobal(var,value)
end

if st.type=='time' and not st.time then -- write out log messages from the time scene
  fibaro:debug("Time "..st.tag..":")
  fibaro:debug(st.log) 
end 

Here we use the tag field to tell us what global should be updated to what value. However, you are free to use the tag field in any way you want and invent your own format to tell you what the script should do.

 

In this example we also introduce catchup logic.

Each time rule can optionally be assigned a "catchup group" with catch:<N>.

The latest rule within a catch group that was not scheduled bacuse time had passed will be executed anyway.

 

I do realise it would be nice with complex time expression like max(sunrise,07:00)... maybe next version

Edited by jgab
  • Like 2
Link to post
Share on other sites
On 9/7/2019 at 7:34 AM, jgab said:

New version 1.1

-Improved log handling - I think...

-Time rules can end with comments

-Fibaro globals can be used to substitute in values in time rules. 

-A bit better documentation in the first post...

 

New version 1.5

- Catchup support for rules. See post

- Long date format. Ex. 2020/may/15/10:00 or may/12, april/4/07:00 (most useful for intervals, but also list of specific dates like the Earth Hour example in the previous post)

- Day numbers. Ex. 1..7 to test for the first seventh days of the month.

-Bug fixes.

The scene has only been around for 2 days but v1.4 has been ticking on my HC2 delivering triggers on the second for 12+ hours now :-) 

Edited by jgab
  • Thanks 1
Link to post
Share on other sites
On 9/8/2019 at 10:13 AM, jgab said:

 

New version 1.5

- Catchup support for rules. See post

- Long date format. Ex. 2020/may/15/10:00 or may/12, april/4/07:00 (most useful for intervals, but also list of specific dates like the Earth Hour example in the previous post)

- Day numbers. Ex. 1..7 to test for the first seventh days of the month.

-Bug fixes.

The scene has only been around for 2 days but v1.4 has been ticking on my HC2 delivering triggers on the second for 12+ hours now :-) 

 

New version 1.7B (Beta as it is not very well tested yet)

- More efficient scheduling engine - conditions are much faster to execute (they were fast before, but now even more so...)

- The condition parser is better structured so new condition types should be easier to add in the future

- Support for 'dawn' and 'dusk' - like 'sunset' and 'sunrise' just a little before and after :-) 

- Own sunset calculation to avoid midnight bug <link>. This was an issue as times were scheduled at midnight.

- 'lastday' can be used in day intervals. Ex. 20..lastday

 

Edited by jgab
  • Thanks 1
Link to post
Share on other sites

Jan, can u add macros for global vars like 

$dark where dark is 18:58 in global parameters 

Or $night is global value what I can setup as 23:00

 

sorry, I am in work trip just now and may pass your description for new features, please skip my comments  if this already implemented 

 

thanks 

Link to post
Share on other sites
5 minutes ago, 10der said:

Jan, can u add macros for global vars like 

$dark where dark is 18:58 in global parameters 

Or $night is global value what I can setup as 23:00

 

sorry, I am in work trip just now and may pass your description for new features, please skip my comments  if this already implemented 

 

thanks 

It's supported but the syntax is <varName>

Ex.

%% time
<dark> weekends test
10:00 <specialDay> tada

If global 'dark' is "18:58" and global 'specialDay' is "friday" it will be translated to

%% time
18:58 weekends test
10:00 friday tada

...and I watch the globals so if they change value  the scene will be rescheduled with the new values.

Edited by jgab
  • Thanks 1
Link to post
Share on other sites
7 hours ago, amilanov said:

Are you now using this solely as a replacement to a scheduler?

For me it’s a complement when I need to quickly schedule a scene. My main scheduling tasks (and all home automation rules) are run with ER

  • Thanks 1
Link to post
Share on other sites
  • 9 months later...

Thanks @jgab Very nice that through your code I have this option which should have been there in the first place. Works perfect!

 

Next step for me is checking out EventRunner3. I assume that ER4 is only for the HC3.

Edited by Jeroen_
Typos
Link to post
Share on other sites
6 minutes ago, Jeroen_ said:

Thanks @jgab Very nice that through you code I have this option which should have been there in the first place. Works perfect!

 

Next step for me is checking out EventRunner3. I assume that ER4 is onlyfor the HC3.

Yes, ER4 is only for HC3. However, ER3 is much more mature and used by many people.

Link to post
Share on other sites
  • 4 weeks later...

Can this be used in a scene for example I want a scene to only operate between two times?

 

Say between 23:00-5:00 if sensor detects motion light turns on.

 

If so how would I incorporate that into my other scene? Currently I have it working but it operates all the time I can't specify a time range. 

Edited by puma
Link to post
Share on other sites
6 minutes ago, puma said:

Can this be used in a scene for example I want a scene to only operate between two times?

 

Say between 23:00-5:00 if sensor detects motion light turns on.

 

If so how would I incorporate that into my other scene? Currently I have it working but it operates all the time I can't specify a time range. 

The job of the timer scene is to run a scene at specific times.

Your scene is run when a sensor triggers it - and you want to limit the time interval when it should trigger.

That's a different use case.

 

--[[

%% properties

66 value

--]]

 

local time = os.date("%H:%M")

local light = 77

local sensor = 66

 

if time >= "23:00" or time <= "05:00" then

    if fibaro:getValue(sensor,"value")>0 then

      fibaro:call(light,"turnOn")

    end

end

  • Thanks 1
Link to post
Share on other sites
17 minutes ago, jgab said:

The job of the timer scene is to run a scene at specific times.

Your scene is run when a sensor triggers it - and you want to limit the time interval when it should trigger.

That's a different use case.

 

--[[

%% properties

66 value

--]]

 

local time = os.date("%H:%M")

local light = 77

local sensor = 66

 

if time >= "23:00" or time <= "05:00" then

    if fibaro:getValue(sensor,"value")>0 then

      fibaro:call(light,"turnOn")

    end

end

 

It doesn't work as the sensor I am using doesn't have an id it is a variable from a VD 

Link to post
Share on other sites
20 minutes ago, puma said:

 

It doesn't work as the sensor I am using doesn't have an id it is a variable from a VD 

 

Ok, you need to know the value of the global when the sensor is "breached". Example below assumes string "true".

--[[
%% globals
mySensor
--]]
 
local time = os.date("%H:%M")
local light = 77
 
if time >= "23:00" or time <= "05:00" then
    local sensorValue = fibaro:getGlobalValue("mySensor")
    if sensorValue=='true' then   -- depends on what value the global is set to when breached
      fibaro:call(light,"turnOn")
    end
end

 

  • Thanks 1
Link to post
Share on other sites

@jgabPerfect that did the trick. The value for the sensors is Unsealed or sealed.

 

Thanks!

Edited by puma
Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...