Porting the Time of Day rule as a case example
Hi all,
Since i am currently trying to move my rules to a jsr223 based system, i would like to share my experience with you with a simple usecase which hopefully helps some of you with their own setup.
Maybe you are in the same situation like me.
- You have a running openHAB setup and more or less rules which have grown and evolved during the time that your setup exists.
- You already have read about JSR223 and its benefits/downsides and are interested in a future proof openHAB setup
- You are a bit scared/skeptical about the effort it will take to move your rules.
If at least two of those three points fit your personal situation, i can help you with that hopefully.
Table of contents
- General informations
- Set up jsr223 with javascript support and lewies library
- Check if jsr223 works as expected, aka “my first rule which just logs some stuff”
- Solve upcoming errors, when using lewies current state of the javascript library (2019-04)
- Analyze the Time of Day DSL rules structure
- Migrate the Time of Day rule in sub parts
1. General informations
This tutorial is based on some (popular) threads/things in the community.
- Design Pattern: Time of Day
- Experimental Next-Gen Rules Engine Documentation 4 of : Writing Scripts
- lewies Javascript library
It is also based on an openHABian setup, so you may have to adapt the OPENHAB_CONF
environment variable according to your setup, when you run openHAB in a adocker container for example.
I have choosen this setup, because i think that time of day is very popular and many users will have a running dsl-rules version in their setup.
So many will have a direct usecase, which lowers the entry hurdle.
Also the corresponding Design Pattern threads are well written and documented, so you will have a big source of good documentation in case of problems.
2. Set up jsr223 with javascript support and lewies library
Activate experimental rules support, if not done yet
Get lewies library, place it in the approbiate folder and check for errors
First of all you should log into the openHAB console and start a log tail (or use the log viewer), to get all relevant log messages.
After that you have to download lewies library and copy the contents to th automation\jsr223
folder like shown in the image below.
If you are not seeing any messages/errors you may need to reload the scripts/automation bundle.
I have simply restarted the whole openHAB service and got the following outpout in the console afterwards:
As you can see, everything loaded without throwing errors.
This is going to change in a moment, when we are trying to run our first demo rule.
3. Check if jsr223 works, aka “my first rule which just logs some stuff”
Now we will create our first javascript rule file example.js
, with some basic content:
load(Java.type("java.lang.System").getenv("OPENHAB_CONF")+'/automation/jsr223/jslib/JSRule.js');
logInfo("Hello JSR223");
The first line will load lewies library for us.
(Just as a side info: His library will make it easier for us to define rules, that’s why we use it.)
The second line will log an information exactly one, when the file is loaded.
The file needs to be placed in the jsr223
folder, beside the jslib folder.
It should look like this in your console:
Still no errors.
4. Solve upcoming errors, when using current library state(2019-04)
Let’s replace the log with our first rule:
load(Java.type("java.lang.System").getenv("OPENHAB_CONF")+'/automation/jsr223/jslib/JSRule.js');
JSRule({
name: "My first JS Rule",
description: "This rules is a simple example for getting started with JSR223 Javascript",
triggers: [
TimerTrigger("0/15 * * * * ?")//Enable/Disable Rule
],
execute: function( module, input){
logInfo("This is a example rule, which runs every 15 seconds.");
}
});
We should have a short look at the different parts of the JSRule
:
name and description shold be self explaining. You should add some usefull stuff here.
triggers can be compared to the when part of a dsl rule.
We will configure the triggers for our astro rule later in this area.
execute is the part that will run, when the javascript rule got triggered.
When you save example.js
now, the log should be a bit different:
You can see that theres something wrong on line 158
of the triggersAndConditions.js
file of the library.
If you open the file you will find the following function at that line:
var getTrName = function(trn){
return trn == undefined || trn == null || trn == "" ? uuid.randomUUID() + "-" + me.replace(/[^\w]/g, "-") : trn;
//return trn == undefined || trn == null || trn == "" ? uuid.randomUUID() : trn;
}
delete the first return
statement and uncomment the second one, which should solve this problem for now.
var getTrName = function(trn){
return trn == undefined || trn == null || trn == "" ? uuid.randomUUID() : trn;
}
If you save the example.js
file now, the rule should run and log the message every 15 seconds.
Congratulations.
You have set up jsr223 and lewies javascript library succesful.
Attention
The library is in a refactoring currently, so there may be other errors or even none, when something got fixed.
lewies waiting for some openHAB changes, until he can continue fixing/improving it.
I will try to keep this tutorial up to date to the library status.
Additional information about jsr223 and the next generation rules engine can be found on the website/docs and here.
I have linked one thread about n genereation rules engine scripting above and rich has written some more.
Since the post as already really long, i want to focus on my start topic now.
How do i migrate an existing dsl rule to javascript with the time of day rule as an example.
5. Analyze the Time of Day DSL rules structure
So we will first have a look at the dsl time of day rule and check what tasks have to be done within the rule.
I will not explain the process of implementing “Time of Day” in general.
Please read the design pattern Thread for deeper information about the concept and needed items.
Here we have the rule, like its documented in the Design Pattern Thread:
val logName = "Time Of Day"
rule "Calculate time of day state"
when
System started or // run at system start in case the time changed when OH was offline
Channel 'astro:sun:home:rise#event' triggered START or
Channel 'astro:sun:home:set#event' triggered START or
Channel 'astro:sun:minus90:set#event' triggered START or
Time cron "0 1 0 * * ? *" or // one minute after midnight so give Astro time to calculate the new day's times
Time cron "0 0 6 * * ? *" or
Time cron "0 0 23 * * ? *"
then
logInfo(logName, "Calculating time of day...")
// Calculate the times for the static tods and populate the associated Items
// Update when changing static times
// Jump to tomorrow and subtract to avoid problems at the change over to/from DST
val morning_start = now.withTimeAtStartOfDay.plusDays(1).minusHours(18)
vMorning_Time.postUpdate(morning_start.toString)
val night_start = now.withTimeAtStartOfDay.plusDays(1).minusHours(1)
vNight_Time.postUpdate(night_start.toString)
val bed_start = now.withTimeAtStartOfDay
vBed_Time.postUpdate(bed_start.toString)
// Convert the Astro Items to Joda DateTime
val day_start = new DateTime(vSunrise_Time.state.toString)
val evening_start = new DateTime(vSunset_Time.state.toString)
val afternoon_start = new DateTime(vEvening_Time.state.toString)
// Calculate the current time of day
var curr = "UNKNOWN"
switch now {
case now.isAfter(morning_start) && now.isBefore(day_start): curr = "MORNING"
case now.isAfter(day_start) && now.isBefore(afternoon_start): curr = "DAY"
case now.isAfter(afternoon_start) && now.isBefore(evening_start): curr = "AFTERNOON"
case now.isAfter(evening_start) && now.isBefore(night_start): curr = "EVENING"
case now.isAfter(night_start): curr = "NIGHT"
case now.isAfter(bed_start) && now.isBefore(morning_start): curr = "BED"
}
// Publish the current state
logInfo(logName, "Calculated time of day is " + curr)
vTimeOfDay.sendCommand(curr)
end
Thankfully, we don’t need to investigate too much here.
Everything is well documented and commented, so we have a nice todo-list for our javascript equivalent.
6. Migrate the Time of Day rule in sub parts
Ok, let us focus on the triggers later and get the functional part up.
I will for now add a 15 seconds trigger, for testing the rule, which i would suggest you to do similar.
JSRule({
name: "Time of day",
description: "Calculates the time of day, depending on several triggers",
triggers: [
TimerTrigger("0/15 * * * * ?")// 15 seconds for testing/debugging
],
execute: function( module, input){
logInfo("Calculating time of day...")
// Calculate the times for the static tods and populate the associated Items
// Update when changing static times
// Jump to tomorrow and subtract to avoid problems at the change over to/from DST
// Calculate the current time of day
// Publish the current state
}
});
I have directly added the dsl rule comments, as a to do list.
This way we can’t forget anything.
Check, if the test setup works with saving the example.js
file.
It should look like this:
Fine.
Now we will do some basic preparations.
- We will get the
vTimeOfDay
item, to read and write it.
(This has to be done with jsr223 and is different to rules-dsl, where we can simply adress item names.) - We will also get the current time, with a javascript Date object.
JSRule({
name: "Time of day",
description: "Calculates the time of day, depending on several triggers",
triggers: [
TimerTrigger("0/15 * * * * ?")// 15 seconds for testing/debugging
],
execute: function( module, input){
// Get the current time as number
var now = new Date().getTime();
//Get the vTimeOfDay item as json object
var cTimeOfDay = getItem("vTimeOfDay");
logInfo("Calculating time of day...")
// Calculate the times for the static tods and populate the associated Items
// Update when changing static times
// Jump to tomorrow and subtract to avoid problems at the change over to/from DST
// Calculate the current time of day
// Publish the current state
}
});
Did you notice the getItem()
method?
It will grab the specified item for you as a json object.
Lets see, how we can calculate the static times with javascript:
Info:
I am not using the “morning”, “night” and “bed” items, since they are static and i don’t want to view them in my personal setup.
If one wants to display them, it will be a bigger effort, because the js rule is working with number based timestamps.
JSRule({
name: "Time of day",
description: "Calculates the time of day, depending on several triggers",
triggers: [
TimerTrigger("0/15 * * * * ?")// 15 seconds for testing/debugging
],
execute: function( module, input){
// Get the current time as number
var now = new Date().getTime();
//Get the vTimeOfDay item as json object
var currentTimeOfDay = getItem("vTimeOfDay");
logInfo("Calculating time of day...")
// Calculate the times for the static times
var morning_start = new Date().setHours(6, 0, 0, 0); // .setHours(Hour, Minute, Second, Millisecond)
var bed_start = new Date().setHours(0, 0, 0, 0);
// Get the astro calculated times as numbers for comparison
var day_start = new Date(getItem("vSunrise_Time").state).getTime();
var afternoon_start = new Date(getItem("vEvening_Time").state).getTime();
var evening_start = new Date(getItem("vSunset_Time").state).getTime();
// Calculate the current time of day
// Publish the current state
}
});
Time for saving and checking again.
Your log should display no errors and run the rule every 15 seconds:
Time for some calculation.
The rule now has many times in variables as millisecond numbers, so we can easily do some math.
JSRule({
name: "Time of day",
description: "Calculates the time of day, depending on several triggers",
triggers: [
TimerTrigger("0/15 * * * * ?")// 15 seconds for testing/debugging
],
execute: function( module, input){
// Get the current time as number
var now = new Date().getTime();
//Get the vTimeOfDay item as json object
var currentTimeOfDay = getItem("vTimeOfDay");
logInfo("Calculating time of day...")
// Calculate the times for the static times
var morning_start = new Date().setHours(6, 0, 0, 0); // .setHours(Hour, Minute, Second, Millisecond)
var bed_start = new Date().setHours(0, 0, 0, 0);
// Get the astro calculated times as numbers for comparison
var day_start = new Date(getItem("vSunrise_Time").state).getTime();
var afternoon_start = new Date(getItem("vEvening_Time").state).getTime();
var evening_start = new Date(getItem("vSunset_Time").state).getTime();
// Calculate the current time of day
var curr = "UNKNOWN"
if(now > bed_start) curr = "BED";
if(now > morning_start) curr = "MORNING";
if(now > day_start) curr = "DAY";
if(now > afternoon_start) curr = "AFTERNOON";
if(now > evening_start) curr = "NIGHT";
// Publish the current state
logInfo("Current time of day is now " + curr);
}
});
These if conditions simply go through every configured Time of Day state and set it.
When the correct state is set, the left over if conditions will be false and left out.
Example (Time is after day_start):
The curr
variable will be set to BED
, then to MORNING
, then to Day
and after that the if conditions for afternoon and night will be false.
curr
will stay at DAY
and send as new state to the Item.
Since we are only writing one statement after each if-condition
, i have left the brackets.
But you can of course do it with brackets, if you like this more.
Example:
if((now - night_start > 0) || (now - bed_start > 0 && now - morning_start < 0)) {
curr = "BED";
}
Let’s save and check the logs again.
I have added some logging already which should output the current time of day,
according to our calculation.
Depending on the time of day, when you are doing this you should get some result for the calculation.
For me it’s DAY
while im writing this thread.
Well, looks like we are close to the end.
Just two steps left.
Updating the item and configuring the triggers.
Lets start with the item.
JSRule({
name: "Time of day",
description: "Calculates the time of day, depending on several triggers",
triggers: [
TimerTrigger("0/15 * * * * ?")// 15 seconds for testing/debugging
],
execute: function( module, input){
// Get the current time as number
var now = new Date().getTime();
//Get the vTimeOfDay item as json object
var currentTimeOfDay = getItem("vTimeOfDay");
logInfo("Calculating time of day...")
// Calculate the times for the static times
var morning_start = new Date().setHours(6, 0, 0, 0); // .setHours(Hour, Minute, Second, Millisecond)
var bed_start = new Date().setHours(0, 0, 0, 0);
// Get the astro calculated times as numbers for comparison
var day_start = new Date(getItem("vSunrise_Time").state).getTime();
var afternoon_start = new Date(getItem("vEvening_Time").state).getTime();
var evening_start = new Date(getItem("vSunset_Time").state).getTime();
// Calculate the current time of day
var curr = "UNKNOWN"
if(now > bed_start) curr = "BED";
if(now > morning_start) curr = "MORNING";
if(now > day_start) curr = "DAY";
if(now > afternoon_start) curr = "AFTERNOON";
if(now > evening_start) curr = "NIGHT";
// Publish the current state
if(currentTimeOfDay.state != curr) {
logInfo("Current time of day is now " + curr);
sendCommand(currentTimeOfDay, curr);
}
}
});
Simple as that.
We check, if the current item state is different from our calculation and update the item if that’s the case.
Should look like this in your logs:
Ok, since we don’t want to do this every 15 seconds, there is still one task left.
The triggers.
Info:
System startup trigger seems to have some problems (or at least the library) at the time this tutorial is written, so we have to leave this out for now.
We will do the cron triggers, which should not be that difficult since you have seen one ove the whole tutorial.
Also we will do the channel triggers.
JSRule({
name: "Time of day",
description: "Calculates the time of day, depending on several triggers",
triggers: [
ChannelEventTrigger("astro:sun:home:rise#event", "START", "astroSunriseTrigger"),
ChannelEventTrigger("astro:sun:home:set#event", "START", "astroSunsetTrigger"),
ChannelEventTrigger("astro:sun:minus90:set#event", "START", "astroSunsetDelayTrigger"),
TimerTrigger("0 1 0 * * ? *"), // one minute after midnight so give Astro time to calculate the new day's times
TimerTrigger("0 0 6 * * ? *"),
TimerTrigger("0 0 23 * * ? *")
],
execute: function( module, input){
// Get the current time as number
var now = new Date().getTime();
//Get the vTimeOfDay item as json object
var currentTimeOfDay = getItem("vTimeOfDay");
logInfo("Calculating time of day...")
// Calculate the times for the static times
var morning_start = new Date().setHours(6, 0, 0, 0); // .setHours(Hour, Minute, Second, Millisecond)
var bed_start = new Date().setHours(0, 0, 0, 0);
// Get the astro calculated times as numbers for comparison
var day_start = new Date(getItem("vSunrise_Time").state).getTime();
var afternoon_start = new Date(getItem("vEvening_Time").state).getTime();
var evening_start = new Date(getItem("vSunset_Time").state).getTime();
// Calculate the current time of day
var curr = "UNKNOWN"
if(now > bed_start) curr = "BED";
if(now > morning_start) curr = "MORNING";
if(now > day_start) curr = "DAY";
if(now > afternoon_start) curr = "AFTERNOON";
if(now > evening_start) curr = "NIGHT";
// Publish the current state
if(currentTimeOfDay.state != curr) {
logInfo("Current time of day is now " + curr);
sendCommand(currentTimeOfDay, curr);
}
}
});
Thanks to lewies library this is straight forward too and not to far from what you know through the rules-dsl.
The ChannelEventTrigger
needs the channel name and the trigger type, which should already be familiar for you from rules-dsl.
The trigger name (e.g. astroSunriseTrigger
) is optional, but a nice place to make your rules more readable.
The TimerTrigger
is straight forward too, since it just needs the cron expression.
Finish
That’s it.
It might look like a long tutorial, but the comple jsr setup part has to be done only once.
Also you may have recognized, that many commands and behaviors are similar to the rules-dsl with slightly differences.
I have done most of my migration due to copying the old rule and editing it to a valid javascript function.
Functions like postUpdate
and sendCommand
can be copied, pasted and adapted very fast.
I have also recognized that i got way faster after migrating some rules.
So hopefully this hels some of you with their start in jsr223.
Again attention
The js library may get some fixes/changes, when the openHAB build system migration is completed.
I will try to keep this tut up to date, but please poke me if i am late.
Also this may not be the best implementation of the time of day rule.
I am still starting with jsr223/javascript currently and so i am learning new thigns too every day.
Any feedback is welcome and i will try to improve the tutorial based on that.