Design Pattern(?): Graceful Retry Actions

I think these examples are based on a false assumption. Did you test this code?

You will never get an exception when you send a command to an Item from a rule (unless the Item doesn’t exist in the first place). So you will never get into the catch where the retry is implemented.

What you have to do is wait for a little bit (i.e. create a timer) to see if your Item changed to the expected state in response to the command and only if it didn’t to retry. This also requires the Item to have autoupdate set to false so that it doesn’t just automatically change in response to the command and instead waits for the device to report back the changed state.

It gets even more complicated for Item types where the commands and the states do not match. For example, if you send INCREASE as a command to a Dimmer Item, the Item’s state will never be INCREASE, it will be an integer between 0 and 100. You can possibly infer that if the state changed at all it changed in response to the INCREASE command. But if the Dimmer was already at 100, it won’t change even if the command was received.

This definitely could become a design pattern but as currently implemented this code does nothing. Neither the Rules DSL nor the Jython examples will ever retry the command.

If implemented correctly, it would have somewhat limited use to cases where the device will always change state in response to every command. So Dimmers and Rollershutters and Color Items would only work most of the time.

This code is untested, but the following JS Transformation theoretically could be used in a Profile in the Item command to Channel direction to implement a retry, with the limitations discussed above. Note this code requires OHRT.

(function(data, item, delay, maxRetries) {
  const {LoopingTimer} = require('openhab_rules_tools');
  const timer = cache.private.get(item+'_timer', () => LoopingTimer());
  const retryCommand = cache.private.get(item+'_command', () => data);
  const beforeState = cache.private.get(item+'_beforeState', () => items[item].state;)
  
  // timer is runnining, this is a retry
  // pass the data through
  if(!timer.hasTerminated()) {
    return data;
  }

  // timer is running but data is different from retryCommand, this is an override
  // cancel the timer
  else if(!timer.hasTerminated() && data != retryCommand) {
    timer.cancel();
  }

  // start the looping timer to retry the command
  timer.loop(() => {
    let numTries = cache.private.get(item+'_numTries', 0);
    
    // Item has not changed and we haven't reached max retries
    if(items[item].state == beforeState && numTries < maxRetries) {
      items[item].sendCommand(data);
      cache.private.put(item+'_numTries', nuTries++);
      return delay;
    }
    
    // Item state finally changed or we reached max retries, exit the loop
    else {
      cache.private.remove(item+'_numTries');
      cache.private.remove(item+'_command');
      cache.private.remove(item+'_beforeState');
      return null;
    }  
  }, delay, item);

})(input, item, delay, maxRetries)

You need to pass item, delay (anything supported by time.toZDT()), and maxRetries as command line arguments using URL encoded carguments. See the docs for details. For example, if the above were added through the UI:

config:js:retry?item='MyItem'&delay='PT1S'&maxRetries=3

The above will retry the command sent to MyItem every second up to three times.

Again, I’ve not tested this code. If I get a chance to I’ll publish it to the marketplace.