Operating hours counter

You are switching devices and want the operating hours of these devices counted? This tutorial gets you covered.

You will get:

  • a precise operating hours counter
  • a live running counter when device is turned on

You need:

  • a persistence service set up (script will get exact times from there)

Limits:

  • not so suitable for switch on times < 1 minute as those might be missed

Granularity in minutes can be changed. Make sure to change granularity variable and cron period, both have to match! (e.g. to "0 0/5 * * * ? *" for 5 minutes granularity)

Tested with openHAB 3.4

items/uptime_demo.items:

Group GP_Persist_Change
Group GP_Persist_Restore // this is optional, configure if you want your item state to be restored after restart

Switch Item1_Switch "Fan" <fan>     (GP_Persist_Change) { expire="3h,command=OFF" }
Switch Item2_Online "Computer [%s]" (GP_Persist_Change) { channel="network:pingdevice:computer:online" }

Number:Time Item1_Hours "Operating Hours [%.1f s]" (GP_Persist_Restore)
Number:Time Item2_Hours "Operating Hours [%.1f s]" (GP_Persist_Restore)

persistence/influxdb.persist:

Strategies {
    default = everyChange
}

Items {
    GP_Persist_Change*  : strategy = everyChange
}

persistence/mapdb.persist: (optional)

Strategies {
    default = everyChange
}

Items {
    GP_Persist_Restore* : strategy = everyChange, restoreOnStartup
}

rules/uptime.rules:

import java.util.ArrayList              // openHAB does not import ArrayList.get() it by default
import org.openhab.core.types.UnDefType // openHAB does not import it by default
val granularity = 1                     // minutes; change cron period when you change this value!

rule "Calculate uptime of items"
when
    Time cron "0 0/1 * * * ? *"
then
    val uptime_items = newArrayList(
          newArrayList(Item1_Switch, Item1_Hours)
        , newArrayList(Item2_Online, Item2_Hours)
    )
    val now = ZonedDateTime.now()

    for (ArrayList<GenericItem> current_item : uptime_items) {
        var item = current_item.get(0)
        var target = current_item.get(1)
        var uptime = if (target.state instanceof UnDefType) 0 else (target.state as QuantityType<Number>).doubleValue
        val uptime_old = uptime
        if(item.changedSince(now.minusMinutes(granularity))) {
            var item_old = item.previousState()
            var duration = Duration.ZERO
            if(item.state == ON) {
                // item was switched on, take time from last persisted state (= switch on time) to now
                duration = Duration.between(item_old.getTimestamp, now)
            }
            if(item.state == OFF) {
                // item was switched off, take time from last cron call to last persisted state (= switch off time)
                duration = Duration.between(now.minusMinutes(granularity), item_old.getTimestamp)
            }
            uptime += duration.getSeconds()
            uptime += duration.getNano() / 1.0E9
        } else if(item.state == ON) {
            uptime += granularity * 60
        }
        if(uptime_old != uptime) {
            target.postUpdate(uptime.doubleValue)
        }
    }
end
1 Like

This is a great candidate for a rule template. Then users can just install it and instantiate a rule instead of copy/paste/edit.

Is there a reason you didn’t use a Group instead of hard coding the Items into the Rule? It should be more flexible to use Group membership for that as you can change which Items are processed by the rule without editing the rule.

    val uptime_Items  = OperatingHours.members

You can avoid the import of ArrayList if you use forEach and UnDefType should already be imported too as part of the rule preset. You can use Design Pattern: Associated Items to get at the “Hours” Item.

   OperatingHours.members.forEach[ item |
       val target = item.name.split('_')[0]+'_Hours'
       val uptime = if (target.state instanceof UnDefType) 0 else (target.state as QuantityType<Number>).doubleValue
        val uptime_old = uptime
        ...
    ])

In OH 5 (maybe 4.3, I forget when it was merged) the Item now has a getLastChanged() method. If the comparison were switched to using that then you wouldn’t need the expensive call to persistence. Note that if you have restoreOnStartup, this gets populated with the value from persistence so it survives a restart of OH. You won’t miss any of the time while OH was offline.

Also, you can use getStateAs(OnOffType) which should expand this rule to work with Dimmers and Color Items and other Items which can also be sued as a Switch.

        if(item.lastStateChange.isAfter(now.minusMinutes(graularity))) {
            var duration = Duration.ZERO
            if(item.getStateAs(OnOffType) == ON) {
                duration = Duration.between(item.lastStateChange, now)
            }
            else if(item.getStateAs(OnOffType) == OFF) {
                duration = Duration.between(now.minusMinutes(granularity, item.lastStateChange)
            }

If this were implemented as a rule template, the cron and the granularity would be set by a property entered when instantiating the rule.

In the other rules languages you have access to Item metadata which open opportunities to do stuff like set a different granularity on an Item by Item basis and provide the mapping between the time and the switch Item. But if this were a template, one could just instantiate different rules with different granularities.

You could keep using Duration to calculate uptime which makes the code a little easier to read and avoids some extra calculations.

With all of the above the rule could be come a little simpler and require fewer external dependencies (i.e. persistence), something like:

val granularity = 1

rule "Calculate uptime of items"
when
    Time cron "0 0/1 * * * ? *"
then
    OperatingHours.members.forEach[item |
        val target = item.name.split('_')[0]+'_Hours'
        var uptime = if (target.state instanceof UnDefType) Duration.ZERO else Duration.ofSeconds((target.state as QuantityType<Time>).asUnit('s').intValue)
        val uptime_old = uptime
        if(item.lastStateChange.isAfter(now.minusMinutes(graularity))) {
            var duration = Duration.ZERO
            if(item.getStateAs(OnOffType) == ON) {
                duration = Duration.between(item.lastStateChange, now)
            }
            else if(item.getStateAs(OnOffType) == OFF) {
                duration = Duration.between(now.minusMinutes(granularity, item.lastStateChange)
            }
            uptime = uptime.addTo(duration)
        } else if(item.state == ON) {
            uptime = uptime.plusMinutes(granularity)
        }
        if(uptime_old != uptime) {
            target.postUpdate(uptime.toSeconds() + ' s')
        }
    ])
end

The above version of the rule assumes:

  • Items to track are in the Group OperatingHours
  • Each Item to track’s tracking Item’s name starts the same way as the Item it tracks and ends in _Hours
  • The tracking Item is of type Number:Time, the units don’t matter
  • It’s ok that the calculation rounds to the nearest second
  • That I’m correct and UnDefType and OnOffType are both imported by default in Rules DSL.
  • Obviously this will only work in a version of OH after the introduction of last state and last change tracking was added to the Item.

If you are not up for converting this to a rule template, I’m willing to do it for you. But I’ll convert it to JS instead because there are a lot of things it will make easier. Though rule templates are not that hard to do.