Using 'expire' as a timer

Beginners with rules often come up against the need for a timer function soon after starting out.
The timer features available in the various rules languages are quite powerful, but this frightens new users off.

Frequently the use of the ‘expire’ feature built into openHAB3 Items is suggested for basic timing, but this does have limitations. These notes show a way to work around some of the limitations. They are written with old fashioned file-based Item and rule definitions, because it’s easier to share, read and comprehend than screenshots. When you understand, it is easy to convert to GUI based methods.

Expire limitation - unexpected restarts / “reschedules”.
When used with an Item linked to a real device, periodic updates from the device can cause expire timing to start over.
That’s easy to avoid, create a standalone ‘dummy’ Item not linked to any device or binding and use that with expire.

Expire limitation - not able to cancel
There’s no direct way to cancel expire timer once it has started. But - we can use a cheat involving the difference between Item states and commands.
Expire timer will stop if the Item state is changed to the expire target state.
That’s no use on its own - there’s no difference between expire ‘cancel’ (no action required) and expire ‘complete, time is up’ (action wanted).
But if we make the expire target a command, it will be a different event, and we can carefully choose a rule trigger to act in that ‘expire completed’ case and not in the ‘expire cancelled’ case.

Demonstration

Let’s make a fridge door left open alarm.

// the dummy Item, just for expire use
Switch myTimer "my dummy Item" { expire="5m,command=OFF" }
// note that the expiring action is to command OFF

The rules

rule "Start alarm delay"
when
   Item FridgeDoor changed to OPEN
then
   myTimer.postUpdate(ON)
   // we could command, but postUpdate is less overhead
end

rule "Cancel alarm delay"
when
   Item FridgeDoor changed to CLOSED
then
   myTimer.postUpdate(OFF)
   // this will abort the expire timer, without it issuing a command
   if (AlarmBuzzer.state == ON) {
      AlarmBuzzer.sendCommand(OFF)
   }  // silence alarm if buzzing
end

rule "Activate alarm"
when
   Item myTimer received command OFF
      // this only happens if timer runs to end
then
   AlarmBuzzer.sendCommand(ON)
end

The limitation remains that you cannot change the actual expire runtime.
At least, not without deeply technical manipulation and if you can do that, you can use a ‘proper’ timer.

More detail here

6 Likes

It would be a great enhancement if that limitation could be removed.

Using expire as a general-purpose time in this way is an ‘abuse’ of what it is really designed for - expiring stale data after a failure to update.
It does what it does. I wouldn’t expect changes anytime soon.

If you want more flexible timers, they already exist.

There have been ideas floated before for alternative non-rules timer mechanisms - profiles, a binding with channels etc. - but nothing practical to date.

That is the thing that I was searching for, what data, present here at this site.

It is still seemingly unnecessarily complicated for a beginner. Of all the things someone may want to do with basic automation, changing the state of something after a time seems like one of the most basic and common. Every time X happens (e.g., towel heater turns on), wait n minutes and do Y (e.g., turn off the towel heater).

It would be nice if there was a “trigger” property or something built-in that could accomplish this without coding.

You should review what expire does by itself. “expire n mins command OFF” on your towel heater Item does exactly your example.

Unfortunately, in my case it’s controlled by a ZigBee switch that doesn’t work with expire. Updates its status too frequently.

A couple things worth mentioning I think based on the replies and discussion above.

  • Features in openHAB like Expire and Profiles are not and never were intended to be feature complete. They can not cover all use cases. They provide an easy way to avoid needing to write rules for some common use cases.

  • If your specific use case is not handled, it’s probably because it’s simply not technically feasible to handle or doing so will complicate the feature to a point it no longer is simple. It makes no sense to add a second complex way to do something that is already supported in an equally complex way. That just doubles the complexity over all.

How would that work without a rule? I can’t think of a way to manage that that won’t be every bit as complex as managing a timer in a rule. I’m mostly asking from the interface perspective. How, using the Expire Item metadata, would one add a way to dynamically change the expire time based on some criteria?

Some things are going to be complicated. Expire makes it as simple as possible for a couple of specific use cases. If your use case doesn’t fit then, for now, you have to fall back to using a Timer in a rule.

In the very near future, Blockly will support Timers so it’s not like setting up a Timer in a rule is going to remain all that hard.

In the more distant future hopefully there will be a way to install rules libraries as an add-on in which case managing Timers may become just a one liner.

There is also an issue open to request a way to add a delay to a UI rule as an Action without the need for any rule code at all. You’d just trigger the rule when it turns ON, add this delay Action for five minutes and then a second Action to command the Item back to OFF.

Having said all of that, it does seem like it wouldn’t add too much extra complexity to add a flag to the Expire metadata format so that it only resets on changes, not just updates. That seems like it could be an easy win.

I’ve opened [expire] Add an optional flag to have Expire reschedule on change as opposed to update · Issue #2542 · openhab/openhab-core · GitHub but it might be a duplicate of Expire does not work in some situations · Issue #1994 · openhab/openhab-core · GitHub

It would be great if items could have methods to be used by rules. Currently they have state and some other properties are setup using the UI. Could these be transformed into methods to be used in rules ?

Let me give you an example. My towel rails switch off automatically and the time depends on winter / summer. I need to use two different items, plus a group item, to manage this situation. Having a single item whose pulsetime (tasmota terminology) could be controlled programatically would be great.

Expire in particular is configured using Item Metadata. Metadata on Items is managed completely separately from Items (just like Links to Channels). And it’s much more complex than just a name and value. So adding a function to the Item class is not feasible.

However, except in Rules DSL and Blockly (for now) everything you see in the UI is available to you in rules.

If you are using Jython and the Helper Library see core.metadata — openHAB Helper Libraries documentation. Expire is using the “expire” namespace. You can pull and set metadata no problem.

If using JavaScript I don’t think the Helper Library has metadata support yet. Accessing metadata would look something like the following.

Get access to the metadata registry

// Accessing Item Metadata
var logger = Java.type("org.slf4j.LoggerFactory").getLogger("org.openhab.model.script.Rules.Examples");
logger.info("Trying to extract a metadata value")
var FrameworkUtil = Java.type("org.osgi.framework.FrameworkUtil");
var _bundle = FrameworkUtil.getBundle(scriptExtension.class);
var bundle_context = _bundle.getBundleContext()
var classname = "org.openhab.core.items.MetadataRegistry"
var MetadataRegistry_Ref = bundle_context.getServiceReference("org.openhab.core.items.MetadataRegistry");
var MetadataRegistry = bundle_context.getService(MetadataRegistry_Ref);
var methods = MetadataRegistry.class.getDeclaredMethods();
logger.info("There are " + methods.length + " methods");
for each (var m in methods) {
  logger.info(m.toString());
}
var Metadata = Java.type("org.openhab.core.items.Metadata");
var MetadataKey = Java.type("org.openhab.core.items.MetadataKey");

Get a given metadata namespace

var metadata = MetadataRegistry.get(new MetadataKey("namespace", "itemName"));

Remove a given metadata namespace

MetadataRegistry.remove(new MetadataKey("namespace", "itemName"))

Remove all metadata from an Item

MetadataRegistry.removeItemMetadata("itemName")

Adding metadata is a little complex because it isn’t just a String. It’s a whole data structure consisting of a:

  • namespace
  • value
  • configuration

The namespace is just a string. For Expire it’s “expire”.

The value is also just a string. For expire, that’s the stuff after the “=” sign.

The configuration is denoted by a “[ ]” and is made up of name value pairs. Expire doesn’t use a configuration.

So to change the Expire metadata you first want to delete the old:

MetadataRegistry.remove(new MetadataKey(namespace, itemName));

and then add the new.

var key = new MetadataKey("namespace", "itemName");
var metadata = new Metadata(key, value, {});
MetadataRegistry.add(metadata);

I’ve my own personal Item metadata library.

(function(context) {
  'use strict';

  // Use the logger from the caller if available, otherwise create a Metadata logger
  var log = (context.logger === undefined) ? Java.type("org.slf4j.LoggerFactory").getLogger("org.openhab.model.script.Rules.Metadata") : context.logger;

  // Get the metadata registry and relevant classes
  var FrameworkUtil = Java.type("org.osgi.framework.FrameworkUtil");
  var _bundle = FrameworkUtil.getBundle(scriptExtension.class);
  var bundle_context = _bundle.getBundleContext()
  var MetadataRegistry_Ref = bundle_context.getServiceReference("org.openhab.core.items.MetadataRegistry");
  var MetadataRegistry = bundle_context.getService(MetadataRegistry_Ref);
  var Metadata = Java.type("org.openhab.core.items.Metadata");
  var MetadataKey = Java.type("org.openhab.core.items.MetadataKey");

  /**
   * Merges two configruation maps.
   * param{object} origConf original configuration
   * param{object} newConf new configuration
   */ 
  context._mergeConf = function(origConf, newConf) {
        var merged = {};
        for (var property in origConf) {
            merged[property] = origConf[property];
        }

        for (var property in newConf) {
            merged[property] = newConf[property];
        }
        return merged;
    }

  /**
   * Deletes the specified metadata on the Item.
   * @param {string} itemName name of the item to delete the metadata from
   * @param {string} namespace namespace to delete, if not specificed delete all metadata
   */
  context.deleteMetadata = function(itemName, namespace){
    namespace = namespace || null;
    if(!namespace){
      log.warn("Deleting ALL metadata on Item " + itemName);
      MetadataRegistry.removeItemMetadata(itemName);
    }
    else {
      MetadataRegistry.remove(new MetadataKey(namespace, itemName));
    }       
  };

  /**
   * Sets the full metadata on the passed in Item.
   * @param {string} itemName the name of the Item to set the metadata on
   * @param {string} namespace metadata namespace
   * @param {string} value metadata value for the namespace
   * @param {object} configuration metadata configuration
   * @param {bool} overwrite, when true the namespace will be deleted before updated
   */
  context.setMetadata = function(itemName, namespace, value, configuration, overwrite) {
    overwrite = overwrite || false;
    value = value || null;
    if(overwrite){
      deleteMetadata(itemName, namespace);
    }
    var curr = getMetadata(itemName, namespace);

    var key = new MetadataKey(namespace, itemName);

    // If overwriting or there is no metadata, just add it
    if(overwrite || curr === null){
      var metadata = new Metadata(key, value, configuration);
      MetadataRegistry.add(metadata);
    }
    // Merge the changes with what already there
    else {
      if(!value){
        value = curr.value
      }
      var mergedConf = _mergeConf(curr.configuration, configuration);
      MetadataRegistry.update(new Metadata(key, value, mergedConf));
    }       
  };      

  /**
   * Sets or updates the Item metadata's value.
   * @param {string} itemName the name of the item
   * @param {string} namespace
   * @param {string} value
   */
  context.setMetadataValue = function(itemName, namespace, value) {
    var curr = getMetadata(itemName, namespace);
    if (curr) {
      setMetadata(itemName, namespace, value, curr.configuration, true);
    } else {
      setMetadata(itemName, namespace, value, {}, false);
    }
  };

  /**
   * Returns the metadata on the passed in item name with the given namespace.
   * @param {string} itemName name of the item to search the metadata on
   * @param {string} namespace namespace of the metadata to return
   * @return {Metadata} the value and configuration or null if the metadata doesn't exist
   */
  context.getMetadata = function(itemName, namespace) {
    return MetadataRegistry.get(new MetadataKey(namespace, itemName));
  };

  /**
   * Returns the value of the indicated namespace
   * @param {string} itemName name of the item to search the etadata on
   * @param {string} namespace namespace to get the value from
   * @return {string} The value of the given namespace on the given Item, or null if it doesn't exist
   */
  context.getMetadataValue = function(itemName, namespace) {
    var md = getMetadata(itemName, namespace);
    return (md === null) ? null : md.value;
  };

  /**
   * Returns the configuration of the given key in the given namespace
   * @param {string} itemName name of the item to search for metadata on
   * @param {string} namespace namespace of the metadata
   * @param {string} key name of the value from config to return
   * @return {string} the value assocaited with the key, null otherwise
   */
  context.getMetadataKeyValue = function(itemName, namespace, key){
    var md = getMetadata(itemName, namespace);
    if(md === null){
      return null;
    }
    return (md.configuration[key] === undefined) ? null : md.configuration[key];
  };

  /**
   * Returns the value of the name metadata, or itemName if name metadata doesn't exist on the item
   * @param {string} itemName name of the Item to pull the human friendly name metdata from
   */
  context.getName = function(itemName) {
    var name = getMetadataValue(itemName, "name");
    return (name === null) ? itemName : name;
  };

  /**
   * Filters the members of the passed in group and generates a comma separated list of
   * the item names (based on metadata if available).
   * @param {string} groupName name of the group to generate the list of names from
   * @param {function} filterFunc filtering function that takes one Item as an argument
   */
  context.getNames = function(groupName, filterFunc) {
    var Collectors = Java.type("java.util.stream.Collectors");
    return context.ir.getItem(groupName)
                     .members
                     .stream()
                     .filter(filterFunc)
                     .map(function(i) {
                       return context.getName(i.name);
                     })
                     .collect(Collectors.joining(", "));
  };

})(this)

But frankly, by the time you’ve gone through all that, why not just create a timer in the first place?

And even if the Item did have a method built into it to mess with the Expire, you’d have to do that from a rule too. The whole point of Expire is to not have to use a rule in the first place for some simple use cases.