Design Pattern(?): Graceful Retry Actions

Design Pattern: Graceful Retry Actions

Problem: An actuator sometimes does not respond to commands (e.g., MQTT device offline).
Standard rules then produce error messages or get stuck.

Solution:

  • Define a rule that sends commands with a retry mechanism.
  • Use timers to control repetitions.
  • After a configurable timeout, an alternative action (e.g., notification) is triggered.

Advantages:

  • Robust handling of network issues or unstable devices.
  • Decouples error handling from normal workflow.

1. Concept

Goal: A command should be sent to a device. If the device does not respond (e.g., offline), we retry the command until either:

  • the device responds, or
  • a maximum number of attempts / timeout is reached.

Optionally, an alternative action can occur after the timeout (e.g., notification, logging, alarm).


2. Implementation in Rule DSL

// Example: Turn on light with retry
var Timer retryTimer = null
var int retryCount = 0
var final int MAX_RETRIES = 3
var final int RETRY_INTERVAL = 10 // seconds

rule "Graceful Retry LightSwitch"
when
    Item SomeTrigger changed to ON
then
    retryCount = 0
    
    // Function to send command with retry
    retryTimer = createTimer(now.plusSeconds(0), [ |
        try {
            // Try sending the command
            LightSwitch.sendCommand(ON)
            logInfo("Retry", "Command sent successfully!")
            if(retryTimer !== null) {
                retryTimer.cancel()
            }
        } catch(Throwable t) {
            retryCount = retryCount + 1
            logWarn("Retry", "Error sending command, attempt #" + retryCount)
            
            if(retryCount < MAX_RETRIES) {
                // Reschedule timer
                retryTimer.reschedule(now.plusSeconds(RETRY_INTERVAL))
            } else {
                logError("Retry", "Maximum number of attempts reached!")
                // Alternative action, e.g., notification
                sendNotification("admin@example.com", "LightSwitch could not be turned on")
            }
        }
    ])
end

DSL Explanation:

  • createTimer creates a timer to execute the retry function.
  • try/catch captures errors when sending the command.
  • reschedule sets the timer again if further attempts are allowed.
  • MAX_RETRIES and RETRY_INTERVAL are configurable.
  • After reaching maximum retries, an alternative action is executed.

3. Implementation in Python (Jython)

from core.rules import rule
from core.triggers import when
from core.log import logging
import threading

log = logging.getLogger("GracefulRetry")

MAX_RETRIES = 3
RETRY_INTERVAL = 10  # seconds

def send_command_with_retry(item_name, command, retries=0):
    try:
        events.sendCommand(item_name, command)
        log.info(f"Command '{command}' sent to {item_name} successfully!")
    except Exception as e:
        retries += 1
        log.warn(f"Failed to send command to {item_name}, attempt #{retries}")
        if retries < MAX_RETRIES:
            threading.Timer(RETRY_INTERVAL, send_command_with_retry, [item_name, command, retries]).start()
        else:
            log.error(f"Max retries reached for {item_name}. Performing alternative action.")
            events.sendCommand("NotificationItem", f"{item_name} could not be switched ON!")

@rule("Graceful Retry Action Python")
@when("Item SomeTrigger changed to ON")
def graceful_retry_action(event):
    send_command_with_retry("LightSwitch", "ON")

Python Explanation:

  • Recursive function send_command_with_retry sends the command and retries on error.
  • threading.Timer schedules a retry after RETRY_INTERVAL seconds.
  • Maximum attempts are limited by MAX_RETRIES.
  • Alternative actions can include notifications or logging.

Advantages of this Implementation

  • Robust handling of unstable devices or temporary network issues.
  • Rules do not block normal workflow.
  • Flexible configuration of interval, number of attempts, and alternative actions.

Graceful Retry Pattern with Exponential Backoff

1. Rule DSL with Exponential Backoff

var Timer retryTimer = null
var int retryCount = 0
var final int MAX_RETRIES = 5
var int retryInterval = 5 // initial interval in seconds
var final int MAX_INTERVAL = 60 // max interval in seconds

rule "Graceful Retry LightSwitch with Backoff"
when
    Item SomeTrigger changed to ON
then
    retryCount = 0
    retryInterval = 5
    
    retryTimer = createTimer(now.plusSeconds(0), [ |
        try {
            LightSwitch.sendCommand(ON)
            logInfo("RetryBackoff", "Command successfully sent!")
            if(retryTimer !== null) retryTimer.cancel()
        } catch(Throwable t) {
            retryCount = retryCount + 1
            logWarn("RetryBackoff", "Failed attempt #" + retryCount)

            if(retryCount < MAX_RETRIES) {
                retryInterval = Math.min(retryInterval * 2, MAX_INTERVAL)
                logInfo("RetryBackoff", "Next attempt in " + retryInterval + " seconds")
                retryTimer.reschedule(now.plusSeconds(retryInterval))
            } else {
                logError("RetryBackoff", "Max retries reached!")
                sendNotification("admin@example.com", "LightSwitch could not be switched ON!")
            }
        }
    ])
end

Explanation:

  • Interval doubles on each failure (retryInterval * 2) until MAX_INTERVAL.
  • MAX_RETRIES limits the number of attempts.
  • reschedule uses the current interval.

2. Python (Jython) with Exponential Backoff

from core.rules import rule
from core.triggers import when
from core.log import logging
import threading

log = logging.getLogger("GracefulRetryBackoff")

MAX_RETRIES = 5
INITIAL_INTERVAL = 5  # seconds
MAX_INTERVAL = 60     # seconds

def send_command_with_backoff(item_name, command, retries=0, interval=INITIAL_INTERVAL):
    try:
        events.sendCommand(item_name, command)
        log.info(f"Command '{command}' sent to {item_name} successfully!")
    except Exception as e:
        retries += 1
        log.warn(f"Attempt #{retries} failed for {item_name}: {e}")
        
        if retries < MAX_RETRIES:
            next_interval = min(interval * 2, MAX_INTERVAL)
            log.info(f"Next retry in {next_interval} seconds")
            threading.Timer(next_interval, send_command_with_backoff, [item_name, command, retries, next_interval]).start()
        else:
            log.error(f"Max retries reached for {item_name}. Triggering alternative action.")
            events.sendCommand("NotificationItem", f"{item_name} could not be switched ON!")

@rule("Graceful Retry Action Python with Backoff")
@when("Item SomeTrigger changed to ON")
def graceful_retry_backoff(event):
    send_command_with_backoff("LightSwitch", "ON")

Explanation:

  • Start interval: INITIAL_INTERVAL.
  • After each failure, interval doubles but does not exceed MAX_INTERVAL.
  • Recursive timer function automatically retries.
  • Alternative action is triggered if MAX_RETRIES is reached.

Advantages of Exponential Backoff

  • Reduces network load for repeated failures.
  • Gives devices more time to reconnect.
  • Error handling remains elegant and decoupled from normal workflow.
  • Parameters like INITIAL_INTERVAL, MAX_INTERVAL, and MAX_RETRIES are easy to adjust.

Generic Multi-Device Retry Function

1. Rule DSL: Multi-Device Retry with Backoff

var Map<String, Timer> retryTimers = newHashMap
var Map<String, Integer> retryCounts = newHashMap
var final int MAX_RETRIES = 5
var final int INITIAL_INTERVAL = 5 // seconds
var final int MAX_INTERVAL = 60    // seconds

rule "Graceful Retry Multiple Devices"
when
    Item RetryTrigger changed to ON
then
    val devices = newArrayList("LightSwitch", "HeaterSwitch", "FanSwitch")

    devices.forEach [ device |
        retryCounts.put(device, 0)
        val int initialInterval = INITIAL_INTERVAL

        val Timer t = createTimer(now.plusSeconds(0), [ |
            try {
                events.sendCommand(device, ON)
                logInfo("MultiRetry", device + " command sent successfully!")
                retryTimers.get(device)?.cancel()
            } catch(Throwable t) {
                val int count = retryCounts.get(device) + 1
                retryCounts.put(device, count)
                logWarn("MultiRetry", device + " attempt #" + count + " failed.")

                if(count < MAX_RETRIES) {
                    val int nextInterval = Math.min(initialInterval * (2^count), MAX_INTERVAL)
                    logInfo("MultiRetry", "Next attempt for " + device + " in " + nextInterval + " seconds")
                    retryTimers.get(device)?.reschedule(now.plusSeconds(nextInterval))
                } else {
                    logError("MultiRetry", device + " max retries reached!")
                    sendNotification("admin@example.com", device + " could not be switched ON!")
                }
            }
        ])
        
        retryTimers.put(device, t)
    ]
end

Explanation:

  • devices contains the list of actuators to retry.
  • Each actuator gets its own timer and retry count.
  • Exponential backoff: nextInterval = initialInterval * (2^count) up to MAX_INTERVAL.
  • Alternative action (notification) triggered at max retries.

2. Python (Jython): Multi-Device Retry with Backoff

from core.rules import rule
from core.triggers import when
from core.log import logging
import threading

log = logging.getLogger("MultiDeviceRetryBackoff")

MAX_RETRIES = 5
INITIAL_INTERVAL = 5  # seconds
MAX_INTERVAL = 60     # seconds

def send_command_with_backoff(item_name, command, retries=0, interval=INITIAL_INTERVAL):
    try:
        events.sendCommand(item_name, command)
        log.info(f"Command '{command}' sent to {item_name} successfully!")
    except Exception as e:
        retries += 1
        log.warn(f"Attempt #{retries} failed for {item_name}: {e}")
        
        if retries < MAX_RETRIES:
            next_interval = min(interval * 2, MAX_INTERVAL)
            log.info(f"Next retry for {item_name} in {next_interval} seconds")
            threading.Timer(next_interval, send_command_with_backoff, [item_name, command, retries, next_interval]).start()
        else:
            log.error(f"Max retries reached for {item_name}. Performing alternative action.")
            events.sendCommand("NotificationItem", f"{item_name} could not be switched ON!")

@rule("Graceful Retry Multiple Devices Python")
@when("Item RetryTrigger changed to ON")
def multi_device_retry(event):
    devices = ["LightSwitch", "HeaterSwitch", "FanSwitch"]
    for device in devices:
        send_command_with_backoff(device, "ON")

Explanation:

  • devices contains all actuators to retry.
  • Recursive function handles each actuator individually.
  • Exponential backoff applied automatically.
  • Alternative actions are triggered on max retries.

Advantages of Multi-Device Version

  • Scalable for any number of devices.
  • Error handling remains decoupled from normal workflow.
  • Exponential backoff reduces network load on repeated failures.
  • Easy configuration: MAX_RETRIES, INITIAL_INTERVAL, MAX_INTERVAL.
  • Retry function can be easily adapted per device (different intervals or alternative actions).

Multi-Device Retry Pattern with Online Check

1. Rule DSL: Multi-Device Retry with Online Check

var Map<String, Timer> retryTimers = newHashMap
var Map<String, Integer> retryCounts = newHashMap
var final int MAX_RETRIES = 5
var final int INITIAL_INTERVAL = 5 // seconds
var final int MAX_INTERVAL = 60    // seconds

rule "Graceful Retry Multiple Devices with Online Check"
when
    Item RetryTrigger changed to ON
then
    val devices = newArrayList("LightSwitch", "HeaterSwitch", "FanSwitch")

    devices.forEach [ device |
        retryCounts.put(device, 0)
        val int initialInterval = INITIAL_INTERVAL

        val Timer t = createTimer(now.plusSeconds(0), [ |
            try {
                // Online check: device is reachable only if it has a valid state
                if(items.get(device) !== NULL) {
                    events.sendCommand(device, ON)
                    logInfo("MultiRetryOnline", device + " command sent successfully!")
                    retryTimers.get(device)?.cancel()
                } else {
                    throw new IllegalStateException("Device " + device + " is offline")
                }
            } catch(Throwable t) {
                val int count = retryCounts.get(device) + 1
                retryCounts.put(device, count)
                logWarn("MultiRetryOnline", device + " attempt #" + count + " failed: " + t.message)

                if(count < MAX_RETRIES) {
                    val int nextInterval = Math.min(initialInterval * (2^count), MAX_INTERVAL)
                    logInfo("MultiRetryOnline", "Next attempt for " + device + " in " + nextInterval + " seconds")
                    retryTimers.get(device)?.reschedule(now.plusSeconds(nextInterval))
                } else {
                    logError("MultiRetryOnline", device + " max retries reached!")
                    sendNotification("admin@example.com", device + " could not be switched ON!")
                }
            }
        ])
        
        retryTimers.put(device, t)
    ]
end

DSL Explanation:

  • items.get(device) !== NULL checks if the actuator is online (alternatively, DeviceStatus items could be used).
  • Offline errors trigger a retry.
  • Exponential backoff is applied as before.

2. Python (Jython): Multi-Device Retry with Online Check

from core.rules import rule
from core.triggers import when
from core.log import logging
import threading

log = logging.getLogger("MultiDeviceRetryOnline")

MAX_RETRIES = 5
INITIAL_INTERVAL = 5  # seconds
MAX_INTERVAL = 60     # seconds

def send_command_with_online_check(item_name, command, retries=0, interval=INITIAL_INTERVAL):
    try:
        # Online check: verify the item has a valid state
        if str(items[item_name]) != "NULL":
            events.sendCommand(item_name, command)
            log.info(f"Command '{command}' sent to {item_name} successfully!")
        else:
            raise Exception(f"{item_name} is offline")
    except Exception as e:
        retries += 1
        log.warn(f"Attempt #{retries} failed for {item_name}: {e}")
        
        if retries < MAX_RETRIES:
            next_interval = min(interval * 2, MAX_INTERVAL)
            log.info(f"Next retry for {item_name} in {next_interval} seconds")
            threading.Timer(next_interval, send_command_with_online_check, [item_name, command, retries, next_interval]).start()
        else:
            log.error(f"Max retries reached for {item_name}. Performing alternative action.")
            events.sendCommand("NotificationItem", f"{item_name} could not be switched ON!")

@rule("Graceful Retry Multiple Devices Python with Online Check")
@when("Item RetryTrigger changed to ON")
def multi_device_retry_online(event):
    devices = ["LightSwitch", "HeaterSwitch", "FanSwitch"]
    for device in devices:
        send_command_with_online_check(device, "ON")

Python Explanation:

  • str(items[item_name]) != "NULL" checks whether the actuator has a valid state.
  • If the device is offline, the retry mechanism is triggered.
  • Exponential backoff reduces unnecessary retries.
  • Max retries trigger an alternative action (e.g., notification).

Advantages of Online-Check Version

  • Avoids unnecessary retry attempts for offline devices.
  • Saves network traffic and reduces actuator load.
  • Works for any number of devices simultaneously.
  • Exponential backoff ensures robust and flexible retries.
  • Alternative actions reliably notify if devices remain offline.

Dynamic Multi-Device Retry with Online Check and Exponential Backoff

Advantage: You no longer need to manually maintain a device list—the rule dynamically retrieves actuators from a group or a configured item list.


1. Rule DSL: Dynamic Devices with Retry, Backoff, and Online Check

var Map<String, Timer> retryTimers = newHashMap
var Map<String, Integer> retryCounts = newHashMap
var final int MAX_RETRIES = 5
var final int INITIAL_INTERVAL = 5 // seconds
var final int MAX_INTERVAL = 60    // seconds

rule "Dynamic Multi-Device Retry with Backoff and Online Check"
when
    Item RetryTrigger changed to ON
then
    // Dynamically fetch all devices from a group
    val devices = GroupDevices.members.filter[i | i !== NULL].map[i | i.name]

    devices.forEach [ device |
        retryCounts.put(device, 0)
        val int initialInterval = INITIAL_INTERVAL

        val Timer t = createTimer(now.plusSeconds(0), [ |
            try {
                // Online check: item must have a valid state
                if(items.get(device) !== NULL) {
                    events.sendCommand(device, ON)
                    logInfo("DynamicRetry", device + " command sent successfully!")
                    retryTimers.get(device)?.cancel()
                } else {
                    throw new IllegalStateException("Device " + device + " is offline")
                }
            } catch(Throwable t) {
                val int count = retryCounts.get(device) + 1
                retryCounts.put(device, count)
                logWarn("DynamicRetry", device + " attempt #" + count + " failed: " + t.message)

                if(count < MAX_RETRIES) {
                    val int nextInterval = Math.min(initialInterval * (2^count), MAX_INTERVAL)
                    logInfo("DynamicRetry", "Next attempt for " + device + " in " + nextInterval + " seconds")
                    retryTimers.get(device)?.reschedule(now.plusSeconds(nextInterval))
                } else {
                    logError("DynamicRetry", device + " max retries reached!")
                    sendNotification("admin@example.com", device + " could not be switched ON!")
                }
            }
        ])
        
        retryTimers.put(device, t)
    ]
end

DSL Explanation:

  • GroupDevices is an openHAB group containing all retry-capable actuators.
  • members.filter[i | i !== NULL].map[i | i.name] generates a list of active devices.
  • Exponential backoff is calculated individually per device.
  • Alternative action (notification) is triggered on max retries.

2. Python (Jython): Dynamic Devices with Retry, Backoff, and Online Check

from core.rules import rule
from core.triggers import when
from core.log import logging
import threading

log = logging.getLogger("DynamicMultiDeviceRetry")

MAX_RETRIES = 5
INITIAL_INTERVAL = 5  # seconds
MAX_INTERVAL = 60     # seconds

def send_command_with_online_check(item_name, command, retries=0, interval=INITIAL_INTERVAL):
    try:
        if str(items[item_name]) != "NULL":
            events.sendCommand(item_name, command)
            log.info(f"Command '{command}' sent to {item_name} successfully!")
        else:
            raise Exception(f"{item_name} is offline")
    except Exception as e:
        retries += 1
        log.warn(f"Attempt #{retries} failed for {item_name}: {e}")
        if retries < MAX_RETRIES:
            next_interval = min(interval * 2, MAX_INTERVAL)
            log.info(f"Next retry for {item_name} in {next_interval} seconds")
            threading.Timer(next_interval, send_command_with_online_check,
                            [item_name, command, retries, next_interval]).start()
        else:
            log.error(f"Max retries reached for {item_name}. Performing alternative action.")
            events.sendCommand("NotificationItem", f"{item_name} could not be switched ON!")

@rule("Dynamic Multi-Device Retry Python")
@when("Item RetryTrigger changed to ON")
def dynamic_multi_device_retry(event):
    # Dynamically fetch all devices from an openHAB group
    devices = [i.name for i in items["GroupDevices"].members if i is not None]
    
    for device in devices:
        send_command_with_online_check(device, "ON")

Python Explanation:

  • items["GroupDevices"].members dynamically retrieves all group members.
  • Each actuator is individually handled with online check and exponential backoff.
  • Max retries trigger the alternative action (notification).
  • No manual device list maintenance is required.

Advantages of Dynamic Multi-Device Version

  • Automatic device detection via groups.
  • Exponential backoff reduces unnecessary attempts.
  • Online check prevents useless retries for offline devices.
  • Scalable to any number of actuators.
  • Alternative actions reliably notify for devices that remain offline.

Fully Configurable Template Version for openHAB (Jython)

Features offered:

  • All parameters configurable via Items: MAX_RETRIES, INITIAL_INTERVAL, MAX_INTERVAL, device group, alternative action.
  • Supports multi-device, online check, and exponential backoff.
  • No code changes required; fully configurable via Items.

1. Python (Jython) Template Version

from core.rules import rule
from core.triggers import when
from core.log import logging
import threading

log = logging.getLogger("ConfigurableMultiDeviceRetry")

# Configuration Items in openHAB
# Number RetryMaxAttempts "Max Retries"
# Number RetryInitialInterval "Initial Interval"
# Number RetryMaxInterval "Max Interval"
# Group RetryDevices
# String RetryAlternativeAction

def send_command_with_online_check(item_name, command, retries=0, interval=None):
    if interval is None:
        interval = int(items["RetryInitialInterval"])
    max_retries = int(items["RetryMaxAttempts"])
    max_interval = int(items["RetryMaxInterval"])
    
    try:
        if str(items[item_name]) != "NULL":
            events.sendCommand(item_name, command)
            log.info(f"Command '{command}' sent to {item_name} successfully!")
        else:
            raise Exception(f"{item_name} is offline")
    except Exception as e:
        retries += 1
        log.warn(f"Attempt #{retries} failed for {item_name}: {e}")
        
        if retries < max_retries:
            next_interval = min(interval * 2, max_interval)
            log.info(f"Next retry for {item_name} in {next_interval} seconds")
            threading.Timer(next_interval, send_command_with_online_check,
                            [item_name, command, retries, next_interval]).start()
        else:
            log.error(f"Max retries reached for {item_name}. Executing alternative action.")
            alt_action = str(items["RetryAlternativeAction"])
            if alt_action:
                events.sendCommand("NotificationItem", f"{item_name}: {alt_action}")
            else:
                events.sendCommand("NotificationItem", f"{item_name} could not be switched ON!")

@rule("Configurable Multi-Device Retry")
@when("Item RetryTrigger changed to ON")
def configurable_multi_device_retry(event):
    devices = [i.name for i in items["RetryDevices"].members if i is not None]
    for device in devices:
        send_command_with_online_check(device, "ON")

2. Explanation

  • RetryDevices (Group): Contains all actuators to retry. Evaluated dynamically.
  • RetryMaxAttempts (Number): Maximum retries (e.g., 5).
  • RetryInitialInterval (Number): Start interval in seconds (e.g., 5).
  • RetryMaxInterval (Number): Maximum allowed interval (e.g., 60).
  • RetryAlternativeAction (String): Message or action executed on max retries.
  • Exponential Backoff: Interval doubles after each failure until RetryMaxInterval.
  • Online Check: Verifies item has a valid state before sending command.
  • Multi-Device: All devices in the group are handled concurrently.

3. Advantages

  • Fully configurable without changing code.
  • Supports any number of devices dynamically.
  • Robust handling of unstable devices or network issues.
  • Exponential backoff reduces unnecessary retries.
  • Alternative actions reliably notify on failed commands.

Rule DSL Version as Fully Configurable Template for openHAB


1. Rule DSL: Configurable Multi-Device Retry Template

var Map<String, Timer> retryTimers = newHashMap
var Map<String, Integer> retryCounts = newHashMap

rule "Configurable Multi-Device Retry"
when
    Item RetryTrigger changed to ON
then
    // Read configuration from Items
    val int MAX_RETRIES = RetryMaxAttempts.state as Number
    val int INITIAL_INTERVAL = RetryInitialInterval.state as Number
    val int MAX_INTERVAL = RetryMaxInterval.state as Number
    val String ALT_ACTION = RetryAlternativeAction.state.toString

    // Dynamically fetch all devices from group
    val devices = RetryDevices.members.filter[i | i !== NULL].map[i | i.name]

    devices.forEach [ device |
        retryCounts.put(device, 0)
        val int startInterval = INITIAL_INTERVAL

        val Timer t = createTimer(now.plusSeconds(0), [ |
            try {
                // Online check: item must have a valid state
                if(items.get(device) !== NULL) {
                    events.sendCommand(device, ON)
                    logInfo("DynamicRetryDSL", device + " command sent successfully!")
                    retryTimers.get(device)?.cancel()
                } else {
                    throw new IllegalStateException("Device " + device + " is offline")
                }
            } catch(Throwable ex) {
                val int count = retryCounts.get(device) + 1
                retryCounts.put(device, count)
                logWarn("DynamicRetryDSL", device + " attempt #" + count + " failed: " + ex.message)

                if(count < MAX_RETRIES) {
                    // Exponential backoff
                    val int nextInterval = Math.min(startInterval * (2^count), MAX_INTERVAL)
                    logInfo("DynamicRetryDSL", "Next attempt for " + device + " in " + nextInterval + " seconds")
                    retryTimers.get(device)?.reschedule(now.plusSeconds(nextInterval))
                } else {
                    logError("DynamicRetryDSL", device + " max retries reached!")
                    if(ALT_ACTION != "") {
                        sendNotification("admin@example.com", device + ": " + ALT_ACTION)
                    } else {
                        sendNotification("admin@example.com", device + " could not be switched ON!")
                    }
                }
            }
        ])
        
        retryTimers.put(device, t)
    ]
end

2. Configuration Items

Item Type Purpose
RetryTrigger Switch Trigger for retry event
RetryDevices Group Contains all devices to retry
RetryMaxAttempts Number Maximum retry attempts
RetryInitialInterval Number Start interval for retry (seconds)
RetryMaxInterval Number Maximum allowed interval (seconds)
RetryAlternativeAction String Message/action on max retries (e.g., notification)

3. Advantages

  • Fully configurable via Items; no code changes needed.
  • Supports any number of devices in a group.
  • Exponential backoff reduces unnecessary retries.
  • Online check prevents useless retries for offline devices.
  • Alternative actions reliably notify if devices remain offline.

Step-by-Step Guide: Configurable Multi-Device Retry in openHAB

1. Create required Items

You need Items for:

  • Trigger
  • Device group
  • Retry parameters
  • Alternative action

1.1 Trigger Item

Switch RetryTrigger "Retry Trigger"
  • Set to ON to start the retry process.

1.2 Device Group

Group RetryDevices "Devices to Retry"
  • Collect all actuators to retry in this group.
  • Example:
Switch LightSwitch "Living Room Light" (RetryDevices)
Switch HeaterSwitch "Heater" (RetryDevices)
Switch FanSwitch "Fan" (RetryDevices)

1.3 Retry Parameters

Number RetryMaxAttempts "Max Retries" <number>
Number RetryInitialInterval "Initial Interval (s)" <time>
Number RetryMaxInterval "Max Interval (s)" <time>
String RetryAlternativeAction "Alternative Action"
  • RetryMaxAttempts e.g., 5
  • RetryInitialInterval e.g., 5 seconds
  • RetryMaxInterval e.g., 60 seconds
  • RetryAlternativeAction e.g., "Notify admin"

2. Python (Jython) Version

  1. Save the Python template code in conf/automation/jsr223/python/.
  2. Example filename: multi_device_retry.py
  3. Content: see Python Template above.

Test:

  • Turn RetryTrigger ON
  • Observe logs (openhab.log) and notifications (NotificationItem)

3. Rule DSL Version

  1. Save the DSL template code in conf/automation/rules/.
  2. Example filename: multi_device_retry.rules
  3. Content: see DSL Template above.

Test:

  • Turn RetryTrigger ON
  • Check logs and notifications

4. Adjust Parameters

Item Purpose
RetryMaxAttempts Number of retries per device
RetryInitialInterval Start interval for retry in seconds
RetryMaxInterval Maximum interval for exponential backoff
RetryAlternativeAction Message on max retries (e.g., "Notify admin")
  • Adjust directly via Items; no code modification required.

5. Extension Possibilities

  • Device-specific intervals: Create additional Items or maps for different retry parameters per device.
  • Conditional retry: Check status items to retry only specific devices.
  • Logging & monitoring: Use dashboard or notifications to immediately identify failed commands.

6. Advantages

  • Fully dynamic and configurable.
  • Supports any number of devices simultaneously.
  • Online check prevents unnecessary retries.
  • Exponential backoff reduces network load.
  • Alternative actions reliably notify for devices permanently offline.

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.

This topic was automatically closed 41 days after the last reply. New replies are no longer allowed.