Cannot identify null value in rule

finally back to trying to get my heater rules to work. struggling to get this rule to work. it is a null issue but not seeming to find the problem child.

i am on 3.4.2.

I get the following error when i run this rule:

Script execution of rule with UID 'heater_new-1' failed: An error occurred during the script execution: null in heater_new```

i ran a rule at startup to update HeaterTotalRuntime to 0. It runs just fine:

```yaml
Item 'HeaterTotalRuntime' changed from NULL to 0 s

I have it persisted as well.

Here is the code:

rule "Heater Runtime"
when
    System started or
    Item main_heater_power changed to ON or
    Time cron "0 0 0 * * ?" // At midnight every day
then
    val DateTime now = new DateTime()
    val lastRun = (MainHeater_timestamp.state as DateTimeType)?.zonedDateTime.toEpochSecond()
    val runtimeToday = if (lastRun == null) 0 else now.withTimeAtStartOfDay().toEpochSecond() - lastRun
    val totalRuntime = (HeaterTotalRuntime.state as NumberState)?.doubleValue() ?: 0
    val newTotalRuntime = totalRuntime + runtimeToday
    HeaterTotalRuntime.postUpdate(newTotalRuntime)
    MainHeater_timestamp.postUpdate(now.toString)
    logInfo("Heater Runtime", "HeaterTotalRuntime: {}", HeaterTotalRuntime.state)
end```

thanks for any help!

Which persistence backend do you use? Mapdb, rrd4j or influx?

influx

OK, so you want to keep track of the heater’s total runtime? Assuming that influxdb is your default persistence service:

In JRuby:

rule "Heater runtime" do
  changed main_heater_power, to: OFF
  run do
    new_duration = Time.now - main_heater_power.previous_state(skip_equal: true).timestamp
    HeaterTotalRuntime << ((HeaterTotalRuntime.state || 0) + new_duration.to_f)
  end
end

You don’t need to deal with startup / cron. The only gotcha is if you shutdown openhab while main_heater_power is ON, you would lose that duration between ON → OFF, or between oh shutdown → oh startup, because on startup, the item would change from UNDEF to ON, and I think it would persist, thus marking a new start. If this is important, we can try to handle such a case.

thanks for helping!

ok, this is a little different. no familiar at all with ruby. but i loaded it up and got this error on load:

Configuration model 'heater_ruby.rules' has errors, therefore ignoring it: [1,23]: mismatched input 'do' expecting 'when'

Firstly your original rule: There’s no NumberState. Try DecimalType maybe?

To use Ruby, you’d need to:

  • Install jrubyscripting addon
  • Create the rule inside <OPENHAB_CONF>/automation/ruby/myrule.rb - the file needs to have a .rb extension, inside that path.

In Rules DSL that error usually means that it couldn’t figure out how to make both operands to an operations (e.g. +, ==, et al) compatible with each other. What it usually does is take the type of the first operand and try to force the second one to match.

Is that valid syntax for Rules DSL? It looks like you are trying to combine the null check operator ? with a ternary operator from another language. Or maybe it’s just something I didn’t know Rules DSL could do?

But given all the back and forth and all the types involved in creating runtimeToday and totalRuntime, the line that creates newTotalRuntime is the primary candidate for the problem. I’d log out those two operands before the addition just to see what they are.

Also note that NULL is not the same as null. The ? on (MainHeater_timestamp.state as DateTimeType)?.zonedDateTime isn’t doing anything. The state of an Item is never null and ? doesn’t work with NULL. Consequently your runtimeToday will never be 0 and if the Item’s state were NULL you’d get an error on the as operation (which could be happening here but usually that’s a different exception/error).

There is no such thing as NumberState. A Number Item carries either a DecimalType or a QuantityType depending on whether or not the Item carries units. I’ll assume it’s not carrying units.

It would probably be easier, even in Rules DSL, to use Duration instead of messing with epoch.

val lastRun = if (MainHeater_timestamp.state instanceof UnDefType) null else (MainHeater_timestamp.state as DateTimeType).zonedDateTime
val runtimeToday = if(lastRun === null) 0 else Duration.between(now.withTimeAtStartOfDay, lastRun)
val totalRuntime = if(HeaterTotalRuntime.state instanceof UnDefType) 0 else HeaterTotalRuntime.state as Number
val newTotalRuntime = runtimeToday.plus(Duration.ofMillis(totalRuntime)).toMillis;

That should fix the glaring errors in the code.

To implement @JimT’s approach in Rules DSL using Persistence it would look something like:

rule 'Heater runtime"
when
    Item main_heater_power changed to ON
then
    val newDuration = Duration.between(now, main_heater_power.previousState(true).timestamp
    HeaterTotalRuntime.postUpdate(newDuration.plus(newDuration).toMillis())
end

Note that once you have the value as a Duration, you can get the time rounded to any time unit, or get it in HH:MM:SS format, ISO8601 duration format (e.g. PT5M25S), etc. I kept it as millis in both of my reimplementations above to be consistent with your current code.

Hey @rlkoshak, thanks for the response and detail. Always very helpful.

I tried the consolidated code and it throws this error.

 cannot invoke method public default java.time.Instant java.time.chrono.ChronoZonedDateTime.toInstant() on null 

I keep running across that working on other code. Googling has not been my friend. Not sure if I am missing something on the openhab install.

Hmmm. java.time.Instant is the parent class to ZonedDateTime. I just assumed that HistoricState.timestamp would be a ZonedDateTime and looking at the code that is the case so that’s not the problem.

However, if the call to previousState failed for some reason, it will return null. So maybe that’s the problem. Log out what gets returned from the call to previousState.

Updating the current code with your clean up still throws an error which gives like no detail unless I guess you have a deeper understanding:

failed: An error occurred during the script execution: null
rule "Heater Runtime"
when
    System started or
    Item main_heater_power changed to ON or
    Time cron "0 0 0 * * ?" // At midnight every day
then
    val DateTime now = new DateTime()
	val lastRun = if (MainHeater_timestamp.state instanceof UnDefType) null else (MainHeater_timestamp.state as DateTimeType).zonedDateTime
	val runtimeToday = if(lastRun === null) 0 else Duration.between(now.withTimeAtStartOfDay, lastRun)
	val totalRuntime = if(HeaterTotalRuntime.state instanceof UnDefType) 0 else HeaterTotalRuntime.state as Number
	val newTotalRuntime = runtimeToday.plus(Duration.ofMillis(totalRuntime)).toMillis;
    HeaterTotalRuntime.postUpdate(newTotalRuntime)
    MainHeater_timestamp.postUpdate(now.toString)
    logInfo("Heater Runtime", "HeaterTotalRuntime: {}", HeaterTotalRuntime.state)
end

I very deliberately left out val DateTime now = new DateTime() in my cleaned up code. We don’t want a DateTime, we want a ZonedDateTime and now is a shortcut already available in your rule to get the instant in time as a ZonedDateTime. You don’t need to and should not be replacing it with your own variable.

That could be the problem. It might not be. Add logging, even to the point where you have a log statement every other line of code. When debugging, a log statement at the end of the rule is just about useless. You need to find exactly which line is failing and you want to know the value of each and every variable used on that line of code.

I don’t get anything to log. This is the way I set it up. Tried with the prefix and without for previousState.

rule 'Heater runtime'
when
    Item main_heater_power changed to ON
then
    val newDuration = Duration.between(now, main_heater_power.previousState(true).timestamp)
	logInfo("main_heater_power.previousState", "Previous State value is: {}")
    HeaterTotalRuntime.postUpdate(newDuration.plus(newDuration).toMillis())
	
end

Ok, so for the longer code, at least I got a little more detail. I took out the val Date line and got this back:

failed: 'withTimeAtStartOfDay' is not a member of 'java.time.ZonedDateTime'; line 8, column 66, length 24

You are trying to log out the result of several operations. Break it up. Log each and every step.

val previousState = main_heater_power.previousState(true) 
logInfo("heater runtime", "Previous state result is " + previousState);
logInfo("heater runtime", "Previous state timestamp is " + previousState.timestamp)
val newDuration = Duration.between(now, previousState.timestmap)
logInfo("heater runtime", "Duration is " + newDuration);

and so on.

I don’t use Rules DSL any more so often forget what’s old and what’s new. ZonedDateTime no longer has a withTimeAtStartOfDay method like existed in OH 2. You need to set the hours, minutes, and seconds independently. I think there is a way to do this with a temporal adjuster but this is easy enough/

now.withHour(0).withMinute(0).withSecond(0)

ok, thats a little better. Will file that away in my notes too. In any case that previous state result comes back as null.

For the longer code, it’s making a little progress. Down to an issue in line 10. Does not like “plus”.

'plus' is not a member of 'java.lang.Comparable<? extends java.lang.Object> & java.io.Serializable'; line 10, column 24, length 50

The error doesn’t make a lot of sense but if this is the line it’s complaining about, it probably doesn’t like the primitive int 0 later on. Change it to be a zero length Duration instead of just 0.

val runtimeToday = if(lastRun === null) Duration.ofSeconds(0) else Duration.between(now.withTimeAtStartOfDay, lastRun)

Duration between takes (older, newer) to produce a positive duration

ok, still issues:

An error occurred during the script execution: Could not invoke method: java.time.Duration.ofMillis(long) on instance: null

just as update, here is the latest code:

rule "Heater Runtime"
when
    System started or
    Item main_heater_power changed to ON or
    Time cron "0 0 0 * * ?" // At midnight every day
then
    val lastRun = if (MainHeater_timestamp.state instanceof UnDefType) null else (MainHeater_timestamp.state as DateTimeType).zonedDateTime
	val runtimeToday = if(lastRun === null) Duration.ofSeconds(0) else Duration.between(now.withHour(0).withMinute(0).withSecond(0), lastRun)
	val totalRuntime = if(HeaterTotalRuntime.state instanceof UnDefType) 0 else HeaterTotalRuntime.state as Number
	val newTotalRuntime = runtimeToday.plus(Duration.ofMillis(totalRuntime)).toMillis;
    HeaterTotalRuntime.postUpdate(newTotalRuntime)
    MainHeater_timestamp.postUpdate(now.toString)
    logInfo("Heater Runtime", "HeaterTotalRuntime: {}", HeaterTotalRuntime.state)
end

Hey @JimT not sure what you mean?