Calculate duration of switch is ON

Hey there,

  • Platform information:
    • openHAB version: 3.4.2
  • Issue of the topic:

I would like to calculate the duration a switch is ON.
Therefore I’m wondering why the following code won’t work (especially for short durations):

.items - File

Switch Debug (Group_Persistence)
Number:Time Debug_Runtime (Group_Persistence)

.Rules - File

var Number runtime
val ZonedDateTime Start = now().withHour(0).withMinute(0).withSecond(0)

runtime = (Debug.averageSince(Start) * Duration.between(Start, now).getSeconds()).doubleValue
Debug_Runtime.postUpdate(runtime)

What database?

What’s “short durations”?

What do you mean by “won’t work?” What result are you getting and what are you expecting?

Why this matters is rrd4j only saves one record or second, even if the item changes faster than that.

I doubt averageSince is the right function call to get the total duration. Let’s say the switch is saved as 1 when ON and it’s been on for an hour. The average value for the switch over the last hour will be 1.

Database is jdbc (MariaDB).

Asuuming we have the following rule:

rule "Runtime - Test"
when
    Time cron "0 40 7 1/1 * ? *"
then
    var Number runtime
    val ZonedDateTime Start = now

    Thread::sleep(60000)

    Debug.sendCommand(ON)

    Thread::sleep(10000)

    runtime = (Debug.averageSince(Start) * Duration.between(Start, now).getSeconds()).doubleValue
    Debug_Runtime.postUpdate(runtime)
    // expected: 10s

    Thread::sleep(10000)

    runtime = (Debug.averageSince(Start) * Duration.between(Start, now).getSeconds()).doubleValue
    Debug_Runtime.postUpdate(runtime)
    // expected: 20s

    Thread::sleep(10000)

    runtime = (Debug.averageSince(Start) * Duration.between(Start, now).getSeconds()).doubleValue
    Debug_Runtime.postUpdate(runtime)
    // expected: 30s 

end
2023-05-30 07:41:00.053 [INFO ] [...] - Item 'Debug' received command ON
2023-05-30 07:41:00.053 [INFO ] [...] - Item 'Debug' changed from OFF to ON
2023-05-30 07:41:10.055 [INFO ] [...] - Item 'Debug_Runtime' changed from 2300 s to 42.003782042302845 s
2023-05-30 07:41:20.056 [INFO ] [...] - Item 'Debug_Runtime' changed from 42.003782042302845 s to 53.33978057158102 s
2023-05-30 07:41:30.057 [INFO ] [...] - Item 'Debug_Runtime' changed from 53.33978057158102 s to 64.29389067524116 s

I would expect to have a “ON” - time of 10seconds, 20seconds, … and not 40+ seconds.

I’d like do something similar with my boiler call relay, to know how long it’s been active for.

I would approach this with a

when boilerCall changed

I’d save the Epoch time of the change to a variable and compare that to the next change.

The problem I see with using a Cron trigger is that you could miss many changes between triggers.

What’s the persistence strategy?

averageSince calculates a time weighted average and if there is only one entry to work with I’m not sure it’s capable of generating a valid calculation. The code can be found at openhab-core/PersistenceExtensions.java at 5550ea79b2ed661d4d3aa5f0a35178f475d6969a · openhab/openhab-core · GitHub

I think to make this work using this approach, you’ll have to use a periodic strategy (e.g. every second) in your persistence so that when the time period is small there’s more than one entry for averageSince to work with.

A more direct approach would be something like @MDAR describes.

rule "Calculate On Time"
when
    Item MySwitch changed from ON to OFF
then
    val lastUpdate = MySwitch.lastUpdate
    val delta = Duration.between(lastUpdate, now)
    var oldOnTime = if (MySwitchOnTime.state == NULL || MySwitchOnTime.state === UNDEF) MySwitchOnTime.state as QuantityType else 0 | s
    MySwitchOnTime.update(oldOnTime.plus(delta.toSeconds()))
end

I’m not 100% positive on the cast to QuantityType. Working with units in Rules DSL is a pain.

In JS Scripting it would look something like:

var lastUpdate = items.MySwitch.history.lastUpdate();
var delta = time.Duration.between(lastUpdate, time.toZDT());
var mySwitchRuntime = items.MySwitch
var oldOnTime = (!mySwitchRuntime.isUninitialized) ? mySwitchRuntime.quantityState : Quantity('0 s');
mySwitchRuntime.postUpdate(oldOnTime.plus(Quantity(delta.seconds(), 's'));

In both cases, the way the code works is when the Switch changes from ON to OFF we pull when it turned ON from the database and calculate the number of seconds between now and then and add it to the running total.

If you are looking for the amount time it was ON for the full day, you can create a rule to reset at midnight.

Thanks for the different approaches.

Regarding @MDAR approach: I would like to have the runtime available while the item is in ON state. This would only be available if the item is turned OFF.

I think to make this work using this approach, you’ll have to use a periodic strategy (e.g. every second) in your persistence so that when the time period is small there’s more than one entry for averageSince to work with.

I only had a every5Minutes Persistence strategy, therefore it couldn’t work. I’ve changed over to a presistence strategy with everySecond. With this I get the following results:

2023-06-01 22:11:00.027 [INFO ] [openhab.event.ItemCommandEvent      ] - Item 'Debug' received command ON
2023-06-01 22:11:00.027 [INFO ] [openhab.event.ItemStateChangedEvent ] - Item 'Debug' changed from OFF to ON
2023-06-01 22:11:10.029 [INFO ] [openhab.event.ItemStateChangedEvent ] - Item 'Debug_Runtime' changed from 0 s to 34.76296497809341 s
2023-06-01 22:11:20.031 [INFO ] [openhab.event.ItemStateChangedEvent ] - Item 'Debug_Runtime' changed from 34.76296497809341 s to 39.76303792395171 s
2023-06-01 22:11:30.032 [INFO ] [openhab.event.ItemStateChangedEvent ] - Item 'Debug_Runtime' changed from 39.76303792395171 s to 44.763091991763595 s

If I make the times between the calculations bigger, I get the following results:

2023-06-01 22:25:00.864 [INFO ] [openhab.event.ItemCommandEvent      ] - Item 'Debug' received command ON
2023-06-01 22:25:00.916 [INFO ] [openhab.event.ItemStateChangedEvent ] - Item 'Debug' changed from OFF to ON
2023-06-01 22:26:00.870 [INFO ] [openhab.event.ItemStateChangedEvent ] - Item 'Debug_Runtime' changed from 0 s to 61.94773108544652 s
2023-06-01 22:27:00.875 [INFO ] [openhab.event.ItemStateChangedEvent ] - Item 'Debug_Runtime' changed from 61.94773108544652 s to 121.8762837580829 s
2023-06-01 22:28:00.880 [INFO ] [openhab.event.ItemStateChangedEvent ] - Item 'Debug_Runtime' changed from 121.8762837580829 s to 181.8226757794214 s

Looks like that these formula won’t work for very short durations. For longer durations everythings works as expected.

It gets more complicated but you could use a Design Pattern: Looping Timers to calculate the time ON instead of persistence.

rule "MySwitch turned ON"
when
    Item MySwitch changed to ON
then
    // Do nothing if this happens. It means the switch was turned OFF and then ON again 
    // in less than a second.
    if(sharedCache.get('MySwitchTimer') !== null) {
        logDebug('MySwitch', 'Timer already exists! Ignoring as the timer is already running.')
        return;
    }

    privateCache.put('MySwitch_timer', createTimer(now.plusSeconds(1), [ |
        val state = MySwitch_OnTime.state as QuantityType

        // Initialize the count Item if it's NULL or UNDEF
        if(state == NULL || state == UNDEF) {
            MySwitch_OnTime.postUpdate('1 s')
        }
        // Add one second to the total time
        else {
            MySwitch_OnTime.postUpdate(state.plus('1 s'))
        }
        // Reschedule if it's still ON, exit the timer if not
        if(state == ON) {
            privateCache.get('MySwitch_timer').reschedule(now.plusSeconds(1)
        }
        else {
            privateCache.put('MySwitch_timer', null)
        }
    ]))
end

This will be accurate to within a second. Additional book keeping and an another rule would be required to track to the millisecond or nanosecond. You’ll need to use the sharedCache for the second rule to see the Timer though.

Instead of using a global variable, this uses the privateCache which will cancel the timer for us when the rule is unloaded instead of leaving it orphaned to create an exception in the logs later. We create a timer for one second from now which adds one to the Time Item and if the Switch is still ON it reschedules the timer for another second. Once the Switch is no longer ON the loop will exit.