Edit: Added a section to discuss Nashorn and JSScripting support
Now that I’ve written a number of rule templates I think I’m qualified to write this. @ysc, should this instead be in the Developers category?
I hesitated to write this now because rule templates and the marketplace are in their infancy and a lot of things are changing. I’ll try to come back and update this as things change and hopefully migrate this to the docs once the way it works settles down a bit.
However, I’d like to see more rule templates posted to the marketplace before the OH 3.2 full release so that the users get a good idea about what is possible and how using templates is so much easier than anything else we’ve had to date when it comes to rules.
How Is a Rule Template Different from a Rule?
Rule templates are not stand alone. They cannot be executed. As of this writing they cannot be added to openHAB except through the marketplace.
Instead, they are, as the name implies, a template. A rule template can be instantiated into one or more rules.
How To Use a Rule Template
The general process is:
- User finds a template they want to use in the marketplace in Settings → Automation.
- User “Adds” the template which basically installs it.
- User creates a new rule and chooses the template they want to base the new rule on.
- User supplies the values for the required and any optional parameters.
- If desired, the user can customize the rule beyond the parameters (e.g. adding a new trigger, modifying the code in the actions, etc.
Some important implications given this work flow:
- Once instantiated, the rule is severed from the template. To get updates implemented in the template a user must remove the template, add it again, remove the old rule and reinstantiate the rule from the new template.
- The user can instantiate any number of rules from one template. Some tricky problems can be solved in this way more easily than trying to make one rule handle everything.
- Parameters are key. These are used to customize the instance of a rule template. OH already supports a really robust ability to define parameters (but not well documented). You will likely spend as much time here as you do on the parts of the rule that do the work.
How to Write a Rule Template
This is the process I’ve developed. As new tools are built into OH to support rule templates I expect this section to radically change.
Create a rule
The first step will be to write your rule. Only UI based rules are suitable for rule templates. If you have a rule in a text based config, you can either post it as an example in Tutorials & Examples or you’ll have to convert your rule to a UI rule first.
As you create your rule, pay attention to the parts of the rule that can be configured. The following is a non-extensive list of things that might be customized:
- items that trigger a rule
- thresholds
- conditions (yes you can even set as a parameter the type of the comparison to use against the threshold values, see Threshold Alert)
- Things
- Actions
- times
- another script to call
- messages
- logger
- pretty much anything that can be replaced with a string substitution.
When done, the biggest thing you’ll want to ensure is that your rule template is:
-
generic: leave open to the users what problem the users may want to apply the template to
-
modular: don’t feel like one rule has to do everything; rules can call rules which can be very powerful, especially as a way to centralize the code the user has to write when the rule does something (see Alarm Clock Rule for an example, the rule handles all the timer stuff and then it calls a script/rule written by the user that implements the what to do at that time). Look for things that can be moved outside the rule template into its own template. For example, Time Based State Machine as well as the Alarm Clock Rule sometimes depends on something moving a DateTime’s state to today’s date so I pulled that out into it’s own rule template: To Today. Now not only do I have two rule templates that can work together with that rule to complete implementing something, other users can apply To Today with their own custom rules too. If the code were embedded in my rule templates they wouldn’t be able to do that.
-
configurable: try to make everything possible configurable through parameters. This provides the highest degree of customization available to the users and makes your template more usable in more situations.
-
self contained: until OH gets a better mechanism to find and install rules code libraries, avoid their use in rule templates. The goal should be to eliminate as much friction as possible between the user installing the template and instantiating working rules. For many non-technical users, cloning a git repo and copying the files to the right place on the file system is going to be too much.
-
centralized user changes: this one takes some explanation. Sometimes a rule template won’t do much on its own. For example, all an Alarm Clock Rule only handles scheduling a Timer to go off based on the state of a DateTime Item. But a Timer by itself doesn’t do anything. The end user gets to decide what actually happens at that time. One option is to add a comment to the code in the template along the lines of
// Make your changes here!!!!!
. But that is not great because it makes it harder for the user to update their rule as you change your template. Instead, have the user write their own rule or script and configure your rule template to call that when the Timer goes off. Note: you can pass data to the called rule/script (see below). -
well supported: as of this writing there are two JavaScript rules engines, Nashorn and JSScripting. Nashorn is the JavaScript that openHAB ships with. It follows the JSR223 standard documented at JSR223 Scripting | openHAB and it implements ECMAScript 5.1 which is quite old. JSScripting is a new add-on that implements the latest JavaScript but unlike JSR223 which has it’s Default Presets, JSScripting rule do not include anything by default. The Default Preset must be imported manually. See below for how to make one script compatible with both.
Convert the Rule to a Template
Make the Rule Work in Both Nashorn and JSScripting JavaScript
As previously mentioned, Nashorn and JSScripting present different environments in your rule. But it is possible to make the same code work in both environments.
-
Add the following to the top of each of your Script Actions/Conditions.
if(typeof(require) === "function") Object.assign(this, require('@runtime'));
The
require
function doesn’t exist in Nashorn. So if that function exits, we import all the JSR223 Default Presets into the Script’s context just like they exist by default in Nashorn. -
The way one compares types is different between the two environments. To compare the state of an Item to see if it’s an UnDefType use the following which will work in both environments. The problem is somehow the Java classes get imported differently when using
require
.
var undefType = (typeof(require) === "function") ? UnDefType : UnDefType.class;
if(event.itemState.class == undefType) {
-
You cannot use
Thread.sleep()
at all. Sleeps are not supported in JSScripting and the rule will throw an exception. -
You must be very careful to always use a String when calling
event.sendCommand()
orevent.postUpdate()
for the state. Nashorn is a little nice about this and will convert some things to String for you. JSScripting is strict on this point. -
This should happen very rarely. There really is no reason to do this, but it might crop up. In Nashorn it is possible to cancel a Timer inside the function that gets called by that Timer. It’s kind of pointless but ultimately harmless. In JSScripting trying to do so will result in an exception. So never try to cancel a Timer from inside it’s own Timer function.
Eventually, Nashorn will go away. It’s already deprecated and gone in versions of Java beyond Java 11. So the above only really needs to be managed for the time being. As we get closer to a version of OH that ships without Nashorn, those who write Rule Templates in JavaScript can start to take advantage of features provided by JSScripting more including:
- support for libraries installed via npm
- new language features like the
=>
operator - the Helper Library ships with the add-on which abstracts many of the interactions one has with openHAB, provides many helper utilities, and abstracts a lot of the Java stuff from you, presenting you with native JavaScript Objects instead
These should make your rule templates shorter, simpler, and easier to write in the long run.
Start a Forum Post
For now the only way to install, and therefore test a rule template is to publish it on the forum. So the first step will be to open a new thread on the forum in the Marketplace: Rule Templates section. There will be a template in the post with instructions on what to fill out.
Take note that your rule template will not be made available until the “published” tag is added to the post. The only other allowed tags are tags to indicate the maturity of the rule tempalte (“alpha”, “beta”, etc.).
You’ll want to come up with a logo or other graphic or image to represent your rule template. Try to pick something meaningful, unique, and that has a license approved for this use (e.g. public domain, some Creative Commons licenses, etc.). When you attach the image, replace the ![image
part with ![logo
so MainUI handles the image correctly.
The next section includes the documentation for how to use your rule template. These are the user docs. Be sure to cover what the rule does, how it does it, what the configuration parameters do, and any external dependencies.
There are other sections for screen shots and version tracking.
Finally under resources you can paste in the YAML or JSON of your rule template inline or you can post a link to the raw YAML or JSON formatted text hosted elsewhere (e.g. github). When initially developing your rule template, I recommend posting the YAML from your rule inline on the forum post. Once you’ve tested that the template can generate a valid rule then consider moving the code to github or where ever.
Convert the Rule to a Template
At this point you need to choose between JSON or YAML as the format of your rule template. I recommend YAML because the JSON has a limitation that all your code in Script Actions or Script Conditions must be on the same line. All the newlines in your code gets replaces with \n
which means all your code will end up on one really long line. That is not human maintainable.
So to start, open the “Code” tab of your rule and copy the full YAML. Paste that in YAML code fences under the “Resources” in the forum posting.
Structurally the main difference between a rule and a rule template is in the configuration
section. A rule template replaces that section with:
Parameter | Purpose | Advice |
---|---|---|
uid |
Unique identifier for the rule template | Make this unique. Some use <myname>:<template name> to avoid conflicts with other people’s template IDs. I’ve a repo where I’m hosting this called “openhab_rules_tools” so I use rules_tools:<template name> . |
label |
Human readable name of the template | This is the name of the template as it will appear in MainUI when creating a rule from a template. |
description |
Sentence of two describing the purpose of the rule template. | Cover the main points of what the rule does and the main config parameters at a very high level |
configDescriptions |
Where the config parameters are defined | See the next subsection. |
For example, from the To Today rule template:
uid: rules_tools:to_today
label: Move tagged DateTime Items to today's date
description: Moves DateTime Items' states with a defined tag that are in the past to today's date at midnight.
configDescriptions:
- name: itemTag
type: TEXT
context: tag
label: Tag identifying the Items
required: true
description: Tag on Items that should be processed by this rule.
configDescriptions
This is where you will spend most of your efforts making a rule into a template. All of those features that were identified above as something that can be configured need to be defined here. Almost anything you’ve seen anywhere in MainUI where there is a configuration parameter can be reproduced here. For example, there are date time pickers, sliders, well defined patterns, numbers, Thing pickers, Item pickers, Rule pickers and more.
What documentation exists for these are located at Configuration Descriptions | openHAB. However, that is a doc page for binding developers and it’s focused mainly on an XML format for these. We as rule template developers will need to translate that to XML which may take some trial and error.
The following table is an incomplete explanation of those config parameters that I’ve personally figured out.
Parameter | Purpose | Comments |
---|---|---|
name |
The unique ID of this parameter | Will be used as {{name}} in a String find and replace when the rule is instantiated. |
label |
The label for the parameter presented to the user | Be short. |
description |
Shown in smaller print under the parameter | Details about the purpose and edge cases go here. |
required |
Whether or not the parameter is required. |
true or false
|
type |
The type of the parameter. | One of BOOLEAN , DECIMAL , TEXT or INTEGER . |
context |
What the parameter represents. | This will determine what sort of widget is shown to the user. For example, using “item” will cause an Item Selector widget to be shown to the user where they can search for and select the Item. “time” will present a time selection widget. Configuration Descriptions | openHAB lists all the allowed values for “context”. |
defaultValue |
The value of the parameter if the user doesn’t select their own. | Be sure to use when required is false . Can be used when required is true . |
filterCriteria |
This can be used with certain context choices such as “item” or “thing”. A list of name and value pairs go under filterCriteria to define what type of criteria and the value. |
Multiple values can be defined in a comma separated list. See the examples below. |
options |
Used to present a list of predefined options for the user to select from. Under options there will be a list of label and value pairs to define each option. |
|
limitToOptions |
When present and true only allows the user to select from options and not allowed to type in their own value. |
There are tons more parameters available but these are the ones I’ve used as of this writing.
Here is what an example filterCriteria
would look like.
filterCriteria:
- name: type
value: Group
This will only show those Items that are type “Group”.
filterCriteria:
- name: type
value: Switch,Dimmer
- name: tag
value: Light,Heating
This will only show Switch or Dimmer Items with a Light or Heating tag.
filterCriteria:
- name: kind
value: TRIGGER
That will limit the user to select event channels (i.e. channels that can trigger a rule).
The following is an example of options
:
options:
- label: "== equals"
value: "=="
- label: "!= not equals"
value: "!="
- label: "< less than"
value: "<"
- label: "<= less than equal"
value: "<="
- label: "> greater than"
value: ">"
- label: ">= greater than equal"
value: ">="
In the UI the above renders to
For additional examples see [Do not install] Rule Template Parameters Experiments and Examples for additional examples.
Applying the Parameters to the Rule
Now that we have the parameters defined, how do we make our template code use them? openHAB uses a simple string find and replace to insert the value of a parameter into your code. To denote a place where a parameter should be inserted use {{name}}
where name
is the value of the name
parameter of the config.
This replace can occur anywhere after the configDescriptions. So it can be used in triggers, conditions, script actions, script conditions, etc. Because it’s a simple string substitution this means you can even have the user select or enter small pieces of code that executes (e.g. the comparison operator selector above). The code that uses a parameter for the operation and the threshold might look like:
var filterFunc = function(i) { return i.state.floatValue() {{comparison}} {{threshold}}; };
However, {{ }} is meaningful in JSON and the YAML converts to JSON so if using a parameter in a trigger or a condition, anywhere outside of a script really, be sure to put it in quotes.
Testing the Rule Template
Save the forum posting with a “published” tag applied. This will make it available on the marketplace. While working on it I tend to use “alpha” as the second tag to indicate it may be broken.
Open MainUI → Settings → Automation.
You might need to click “Show All” to see your new rule template.
Select the template and click “Add”. Watch the logs for errors. The errors are meaningful and informative most of the time.
If it worked you’ll see a toast saying that it was added and the “Add” button will change to “Remove”.
You will almost certainly encounter an error here, especially if you are trying to figure out one of the config properties I’ve not documented above. In this case:
- review the error
- open the forum posting for editing and make the change
- Save the forum posting, take note of the time
- Return to MainUI and navigate to Settings and then select Automation again. Do this even if you are already on the template’s page. This forces a refresh.
- Remove the template (if it was successfully added).
- Open the Template and scroll to the bottom and verify that the date and time match when you last saved it. You may need to refresh the whole page if it doesn’t.
- Add the template. If there is an error return to 1.
Once the configuration parameters are parsable and the template successfully added, go to MainUI → Rules and instantiate a new rule. Select the template from the list, set the parameters, and hit save. Open the code tab and verify that all the {{name}}
fields were replaced with the values entered in the template configuration fields. Test that the rule works as designed for the selected parameters.
Once everything is tested, return to the forum posting and change the tag to “beta” or “stable” or what ever is appropriate. If desired, copy the YAML from the bottom of the post and save it off somewhere publicly available, such as github. Replace the code with a link to the raw file at that new location in the forum posting.
The problem I’ve found when trying to code on github originally is that MainUI has a hard time knowing when to refresh the template. I have to change the forum posting too to force a refresh. If I have to do that anyway I may as well edit it on the forum post in the first place.
NOTE: If you store your template on GitHub and link to the raw source instead of posting the YAML into the forum post, be aware that it can be some minutes for the changes checked in to be reflected in the raw URL.
Conclusion
This is not how it’s supposed to work. This is not how it is planned to work long term. But this is how it works now and it took a good bit of trial and error to figure out so I wanted to document it in case others want to take the plunge.