Count ON time, sometimes negative value

Hi all,
I am trying to sum the ON time of an Item.
I found some code here and tried to change it to my needs.

The rule is working, but sometimes the the calculated time is a negative value. But only sometimes.
I am not a programmer, all my scripts are some kind of copy and paste.
Maybe someone canexplain why the calculated value is sometimes positive and sometimes negative?


1 triggers:
2  - id: "1"
3   configuration:
4     itemName: Solar
5    type: core.ItemStateChangeTrigger
6 conditions: []
7  actions:
8  - inputs: {}
9    id: "2"
10    configuration:
11      type: application/vnd.openhab.dsl.rule
12      script: >-
13        var Number Epoch_Time = 0
14​
15        var Number Heizung_Runtime = 0
16            Epoch_Time = now.toInstant.toEpochMilli
17            if (Solar.state == OFF) Heizung_Stop_Time.postUpdate(Epoch_Time)
18            if (Solar.state == ON) {
19                    Heizung_Start_Time.postUpdate(Epoch_Time)
20                    Heizung_Stop_Time.postUpdate(NULL)
21                }
22            if ((Heizung_Start_Time.state > 0) && (Heizung_Stop_Time.state) > 0) {
23                Heizung_Runtime = ((Heizung_Stop_Time.state as Number) - (Heizung_Start_Time.state as Number)) / 60000
24                Heizung_Runtime_Minutes.postUpdate(Heizung_Runtime)
25                logInfo("Heizung", "Heizung Laufzeit in Minuten - " + Heizung_Runtime)
26                var Number totalruntime = (Heizung_Runtime_Minutes_Total.state as Number)+(Heizung_Runtime)
27                Heizung_Runtime_Minutes_Total.postUpdate(totalruntime)
28                }
29    type: script.ScriptAction
30
​

Thank you!

Edit:
With the anylayze function you can see that there are sometimes negative and sometimes positiv values calculated:


The problem is that postUpdate might take some time to finish (mostly just a few milliseconds, but that is enough), so when you read the state later in the rule, you still get the old value. The solution is either to use variables in the calculation, or put a short sleep after the postUpdate calls.

1 Like

There are a couple of other ways to achieve this as well:

  1. Separate out your logic into two rules. One rule that just updates the start and stop time with the solar item changes and a second rule that triggers when the start or stop time update to calculate the values. Then you don’t have to worry about waiting a random amount of time for the postUpdate to complete.

  2. Calculate the value directly from persistence. OH can treat persisted data of OnOffType as OFF = 0 and ON = 1, so the time-weighted average of persisted data over a time span is the percent that item was on (e.g., on the whole time the average is 1 or 100%, off the whole time the average is 0 or 0%, on half the time the average is .5 or 50%, etc.). The averageSince persistence function gives you that time-weighted average directly, so if you just want the percentage you don’t have to do anything to that value and if you want it in time units you just have to multiple the percentage by the time span used. This has the advantages of being a far more streamlined rule (just a few lines) and giving you realtime data if you want it because you can call the rule whenever, not just when the item changes.

3 Likes

That is very true for commands. That is not true for changes and updates. The rule does not trigger until after the Item has changed state. It could be the case that the Item changed state after the rule triggered so you might be getting a new state, but you won’t be getting the old state.

To get for sure the state that triggered the rule use newState (in Rules DSL) or event.itemState (in Python or JavaScript). That stores the actual state that caused the rule to trigger and will exists for all Item received update or Item changed triggered rules.

For rules triggered by a command, use receivedCommand (in Rules DSL) or event.itemCommand (in Python or JavaScript) to see the actual command that triggered the rule to run. Note that a command may not necessarily correspond to the Item’s state. For example you can send an INCREASE command to a Dimmer but a Dimmer can only have a PercentType state (integer between 0 and 100). If you want to see what state a Dimmer Item changed to in response to a command, your best bet is to trigger the rule with a changed trigger an not received command. The rule does not trigger until after the Item changed.

This is the approach I would use. Far more flexible and far less complicated.

As for @dirkdirk’s original rule, I don’t know that we have enough information to understand why the value is sometimes negative. The indentation is a little off but that’t probably because of a mix of using tabs and spaces (always use one or the other, spaces are more stable).

If you want to persist with this approach you’ll need to log out the Item states and log out the result of each value and each step of the calculation. That should tell you where it’s dipping to negative territory. In particular, why Heizung_Start_Time somehow ends up being later than Heizung_Stop_Time (which is the only way I can see that a negative value would come about).

1 Like

But in this case it’s not the item that triggered the rule (Solar), but two different items (Heizung_Start_Time and Heizung_Stop_Time) that gets updated and then accessed within the same rule run. This also (if I’m not mistaken?) takes a round trip via the event bus, so it isn’t immediately available.

2 Likes

You are absolutely right. With the indentation I got turned around. But that is absolutely correct, you have to wait a bit after the call to postUpdate before the Item will reflect that state.

Instead of a sleep though we already know what state we sent to the two Items so just keep those values locally instead of relying on calling the .state.

val startTime = Heizung_Start_Time.state
val endTime = Heizung_End_Time.state
var nowMillis = now.toInstant.toEpochMilli
var runtime = 0
var totalRuntime = Heizung_Runtime_Minutes_Total.state
if(totalRuntime instanceof UnDefType) {
    totalRuntime = 0
}

if(newState == OFF) {
    endTime = nowMillis
    Heizung_Stop_Time.postUpdate(nowMillis)
}
else if(newState == ON) {
    startTime = nowMillis
    endTime = 0
    Heizung_Start_Time.postUpdate(nowMillis)
    Heizung_Stop_Time.postUpdate(UNDEF) // undef is a more appropriate than NULL
}
else {
    // Solar changed to NULL or UNDEF
    // You can check for this and prevent the rule from running with a but only if coindition
    return;
}

if(startTime < endTtime) {
    runtime = (endTime - startTime) / 60000 // in minutes
    totalRuntime = totalRuntime + runtime
    logInfo("Heizung", "Heizung Laufzeit in Minuten - " + runtime)
    Heizung_Runtime_Minutes_Total.postUpdate(totalRuntime)
}
  • Never rely on the state of an Item immediately after you updated it. You know what you updated it to already so just save that as a local variable and use that.
  • I don’t see the point of Heizung_Runtime_Minutes so I don’t use it.
  • NULL means never initialized. UNDEF means the Item has been initialized but we don’t know what state it is in now. UNDEF is more appropriate to use here. But in this case I just ignored updating the Item because we handle that by only calculating a new total when the start time occurs before the end time.
  • I added some error checking because as it was originally written if Solar or any of the other Items were NULL or UNDEF the rule would explode with errors.

OK, now that we’ve fixed the original rule, what about improvements? I have come to love Duration. So how would we use Duration here?

Change your Heizung_Start_Time and Heizung_End_Time Items to DateTime Items instead of Number Items. Change Heizung_Runtime_Minutes_Total to be a Number:time (you might want to change the name to remove the “Minutes” part, that will be clear in a moment why).

var startTime = Heizung_Start_Time.state
var endTime = Heizung_End_Time.state

var totalRuntime = 0
if(Heizung_Runtime_Total instanceof QuantityType){
    totalRuntime = (Heizung_Runtime_Total.state as QuantityType).toUnit("s").intValue // get the time in number of seconds
}

if(newState == OFF){
    endTime = now
    Heizung_End_Time.postUpdate(endTime)
}
else if(newState == ON){
    startTime = now
    Heizung_Start_Time.postUpdate(startTime)
}
else {
    return;
}

if(startTime.isBefore(endTime)){
    var runtime = java.time.Duration.between(startTime, endTime)
    logInfo("Heizung", "Heizung Laufzeit in Minuten - " + runtime.toMinutes)
    totalRuntime = totalRuntime + runtime.toSeconds
    Heizung_Runtime_Total.postUpdate(totalRuntime.toString+" s")
}

So why use Number:time here? Because we can apply standard DateTime label formatting to it to have it split into days:hours:minutes:seconds for us. For example, here is the Number:time Item I have for the runtime of my UPS.

I’ve set the Pattern in the State Display Item metadata to %1$tH:%1$tM:%1$tS and it looks like this:

If you prefer to ignore the seconds, replace the s units above with min and replace toSeconds with toMinutes at the calculation of the total duration.

In my opinion this version is a little more flexible and a lot more self documenting. It doesn’t require one to translate mathematical expressions to their meanings.

Having gone through all that though, the best approach, using persistence would be…

val minsInDay = 24*60
var totalRuntimeDay = Solar.averageSince(now.minusDays(1)) * minsInDay
var totalRuntimeWeek = Solar.averageSince(now.minusDays(7)) * (minsInDay * 7)
var totalRuntimeMonthSoFar = Solar.averageSince(now.minusDays(now.getDayInMonth) * (minsInDay * now.getDayInMonth)

You can see how this is so much better. We don’t care what state Solar actually is in. As long as we have the data we can calculate how long it’s been on for any arbitrary amount of time. I’ve calculated time in minutes but you should be able to see how to use any units you want to. You’d want to add some checking in case averageSince doesn’t return a value for some reason (DB is unavailable, no data for the time period, etc).

3 Likes

Thank you all for your help and explanation.
I learned a lot!

I will adapt my rule script like the one posted here.
If there are still negative values, what I don’t belive :stuck_out_tongue_winking_eye: , i will report back here.

Thank you!