Grumble… Rules DSL and it’s crappy type inference.
We need to cast newState to DateTimeType. That’s not required in JavaScript. It also occurs to me that we need this rule to run at System started
and when the rule reloads so the timer gets recreated. Until we have a rule reloaded trigger though, we’ll need to manually trigger the rule when it’s changed which can be done through MainUI or the Karaf console. I’ve updated the rule to support that as well.
var Timer timerAlarm = null
rule "Alarm Clock"
when
Item AlarmClock changed or
System started
then
// RLK: Fail fast and avoid the extra indentations later so it will be easier to read
if(!(AlarmClock.state instanceof DateTimeType)){
return;
}
// RLK: Since epoch is only used to log and it's essentially meaningless from a human point of view
// let's leave it out.
val alarmTime = AlarmClock.state as DateTimeType
logInfo("alarm", "Scheduling alarm for {}", alarmTime.toString)
if(timerAlarm !== null) {
logInfo("alarm", "Rescheduling alarm")
// RLK: It's unnecessary to get the local date time, you can get the ZonedDateTime straight from
// the DateTimeType.
timerAlarm.reschedule(alarmTime.getZonedDateTime)
} else {
logInfo("alarm", "New alarm")
// RLK: I think one second would be more than enough to avoid the problem
timerAlarm = createTimer(alarmTime.toZonedDateTime.minusSeconds(1), [ |
--- YOUR ACTIONS HERE ----
logInfo("alarm", "Alarm expired")
timerAlarm = null
])
}
end
Ultimately this will be a good candidate for a Rule Template that can be installed through the up coming marketplace. In that case it’ll need to be written as a managed rule and I would write it in JavaScript and the YOUR ACTIONS HERE would be a call to a UI managed Script that is configured when installing the rule template.
Anyway, in JavaScript as a managed rule (i.e in the UI) it would look something like:
triggers:
- id: "1"
configuration:
itemName: AlarmClock
type: core.ItemStateChangeTrigger
- id: "3"
configuration:
startlevel: 100
type: core.SystemStartlevelTrigger
conditions: []
actions:
- inputs: {}
id: "2"
configuration:
type: application/javascript
script: >-
// Change to ID of the Script Rule
var alarmScriptId = "alarm_script";
var FrameworkUtil = Java.type("org.osgi.framework.FrameworkUtil");
var ScriptExecution = Java.type("org.openhab.core.model.script.actions.ScriptExecution");
var ZDT = Java.type("java.time.ZonedDateTime");
var logger = Java.type("org.slf4j.LoggerFactory").getLogger("org.openhab.model.script.Alarm");
// Declaring the variable like this preserves the value from one run to the next
this.timer = (this.timer === undefined) ? null : this.timer;
// Body of the timer, calls a UI Script with the ID "alarm_script"
var callScript = function() {
logger.info("About to call script action");
// Get the RuleManager
var _bundle = FrameworkUtil.getBundle(scriptExtension.class);
var bundle_context = _bundle.getBundleContext()
var classname = "org.openhab.core.automation.RuleManager"
var RuleManager_Ref = bundle_context.getServiceReference(classname);
var RuleManager = bundle_context.getService(RuleManager_Ref);
RuleManager.runNow(alarmScriptId);
}
// No alarm schedculed
if(items["AlarmClock"].class != DateTimeType.class || items["AlarmClock"].getZonedDateTime().isBefore(ZDT.now())) {
logger.info("No alarm scheduled");
if(this.timer !== null) {
this.timner.cancel();
}
}
// create or schedule a timer to run at the configured time
else {
logger.info("Scheduling alarm for " + items["AlarmClock"]);
if(this.timer !== null) {
logger.info("Rescheduling alarm");
this.timer.reschedule(items["AlarmClock"].getZonedDateTime());
}
else {
logger.info("Setting a new alarm");
this.timer = ScriptExecution.createTimer(items["AlarmClock"].getZonedDateTime(), callScript)
}
}
type: script.ScriptAction
This part handles the timer. Until it’s a rule template with parameters the only thing the user may need to change is the first line of the rule to use the ID of the rule to run when the alarm goes off. Once it becomes a rule template that can be a parameter.
The Script looks like the following:
var logger = Java.type("org.slf4j.LoggerFactory").getLogger("org.openhab.model.script.Alarm");
// Alarm clock code goes here
logger.info("The alarm went off!");
Click on Settings → Scripts → +
Enter “alarm_script” as the “Unique ID” and something meaningful for Name and Description. Choose your language of choice (even Rule DSL) for what to do when the alarm goes off. If you enter something other than “alarm_script”, update the main rule.
For users that want to use text based rules, pay attention to the UID of the rule as it appears in MainUI and update the alarmScriptId as appropriate. Hint: in .rules files the ID is the name of the .rules file with a one up number based on the order the rules appear in the file.
I separated the code that handles the AlarmCode Item’s changes and the management of the Timer from the code the user needs to provide because:
- all the users need to care about is their code and they don’t need to mess with timers or error checking
- the users can implement the code that executes in any language they are comfortable with; they are not forced to use just the one in the example
- when it becomes a rule template, it will allow for an easier upgrades/updates that won’t wipe out the user’s custom code.
Note: I’ve tested the JavaScript stuff. I did not test the Rules DSL rule so there may still be typos.