Debugging rule error "null"

Hi guys,

One of my rules sometimes churn out this error message:

2018-11-12 20:24:23.218 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule 'Turn on light when motion sensor triggered': null

I am a bit baffled by this one. No line number displayed. I’ve audited the code, but can’t say what cause it. Has anyone seen something similar?

Thanks,

you need to post the rule and the related items for us to be able to help with this null error… (also OH2 version)
I haven’t seen this one before and you are right… the log message doesn’t give you enough info to debug :frowning:

edit: check [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule <rulename>': null

Ps: Use code fences when posting the configs

This type of error is almost always caused by a type error.Rules DSL is weakly typed but the underlying Java is strongly typed. When the Rules DSL fails to convert an Object to the type needed it generates a null exception.

The most common cause of type errors is when you assume that an Item’s state is not NULL or UNDEF and try to use it as if it were a Number or DateTimeType.

But as Dim said, we’d only be guessing without the Rule and Items.

@Dim and @rlkoshak I am running OH 2.3. Please find the code below; sorry it is quite long.

After seeing Rich’s message, I’ve added some UNDEF and NULL checks. I will have to observe if it helps, as these errors only happen occasionally.

Also, do you know if there is a difference between: if (ON == item.state) vs if (item.state == ON)? Does the first one involve any type conversion?

Thanks,

rule "Turn on light when motion sensor triggered"
when
 Member of gWallSwitchMotionSensor changed to ON
then
  synchronized(switchLock) {
    val localIdx = triggeringItem.name.lastIndexOf("_")
    val switchName = triggeringItem.name.substring(0, localIdx)

    val switchItem = gWallSwitch.members.findFirst[ t | t.name == switchName ]
    if ( null === switchItem ) {
      return
    }

    if ( ON != switchItem.state ) {
      // Is this a fan switch?
      val isFanSwitch = switchName.endsWith("FanSwitch")

      // This check needs to be here rather than in the outter scope because
      // the user might have turned on the light before the programmed 
      // light on time. In such case, we continue to maintain the timer.
      if (ON != VT_Time_LightOn.state && ! isFanSwitch ) {
        // check if there is a valid illuminance value
        val illuminanceName = switchName + "_Illuminance"
        val illuminanceItem = gIlluminance.members.findFirst[ 
          t | t.name == illuminanceName  
        ]

        if ( null === illuminanceItem || NULL == illuminanceItem.state
               || UNDEF == illuminanceItem.state ) {
          return
        }
        else {
          val value = illuminanceItem.state as Number
          if ( null === value ) { // value not available yet
            return
          }
          else {
            val int intValue = value.intValue
            if ( intValue > ILLUMINANCE_THRESHOLD_IN_LUX ) {
              return
            }
            // else pass through to turn on the light
          }
        }
      }
      
      // If a wall switch was just turned off, ignore the motion sensor event.
      if ( lastOffTimes.containsKey(switchItem.name) ) {
        val long timestamp = lastOffTimes.get(switchItem.name)
        if (now.getMillis() - timestamp <= DELAY_AFTER_LAST_OFF_TIME_IN_MS) {
          return
        }
      }

      // An open area might have multiple lights with a shared motion sensor
      // (e.g. in the security system motion sensor where the motion sensor
      // tends to be in a corner and cover the whole lobby). In this case, we
      // only want to trigger a single light if all lights were off. However,
      // if any light is already on, we still want to renew the timer to keep
      // the light on.
      //
      // Check to see if the motion sensor is allowed to trigger the light.
      val disableAlwaysItemName = triggeringItem.name + "_DisableTriggeringAlways"
      val disableTriggeringAlwaysItem = gMotionSensorDisableTriggeringAlways.members.findFirst[ 
          t | t.name == disableAlwaysItemName 
      ]
      if (null !== disableTriggeringAlwaysItem 
            && ON == disableTriggeringAlwaysItem.state) {
        return
      }

      // Check to see if there is a dependent relationship between lights.
      // I.e. if light B is already on, then don't turn on light A if its
      // motion sensor is triggered.
      val disableIfItemName = triggeringItem.name + "_DisableTriggeringIf"
      val disableTriggeringIfItem = gMotionSensorDisableTriggeringIf.members.findFirst[ 
          t | t.name == disableIfItemName 
      ]
      if (null !== disableTriggeringIfItem
          && NULL !== disableTriggeringIfItem.state
          && UNDEF !== disableTriggeringIfItem.state) {
        // see if the other light is on
        val theOtherLight = gWallSwitch.members.findFirst[ 
            t | t.name == disableTriggeringIfItem.state.toString ]

        if ( isSwitchOn.apply(theOtherLight) ) { 
          return
        }
        else if ( switchItem.hasTag(TAG_SHARED_MOTION_SENSOR) ) {
          // If it was just turned off, then don't trigger this light yet.
          // This might be the case that the user is getting out of this zone
          if ( lastOffTimes.containsKey(theOtherLight.name) ) {
            val long timestamp = lastOffTimes.get(theOtherLight.name)
            if (now.getMillis() - timestamp <= DELAY_AFTER_LAST_OFF_TIME_IN_MS) {
              return
            }
          } // has OFF timestamp
        } // the other light is of
        // else - pass through
      }

      switchItem.sendCommand(ON)
    }
    else { // renew timer
      val timerName = switchName + "_Timer"
      val timerItem = gWallSwitchTimer.members.findFirst[ t | t.name == timerName ]
      if ( null !== timerItem ) {
        timerItem.sendCommand(ON)
      }
    }
  }
end

for me teh easiest way to find this, is add logging .
if needed between every code.

logInfo(“rule test”, “Line 1”)
line 1
logInfo(“rule test”, “Line 2”)
line 2

Theoretically there shouldn’t be a difference. However, the Rules DSL will sometimes use the first argument to determine the type of the second argument. This usually causes problems with String Items and Number Items but I’ve never seen it with enum type states (ON/OFF, UP/DOWN, etc).

The usual convention is to put item.state first and the value compared against second but I don’t think there is a technical reason for that.

Why the lock? Locks are dangerous to use in Rules DSL, though using synchronized like that is probably safer than using a ReentrantLock. I’d have to research that to be sure though. Of particular concern is making sure the lock gets unlocked when there is an error. When using ReentrantLocks this is a problem because certain types of errors just kill the Rule dead and the finally never runs to unlock the lock.

It used to be the case where you had to use a ; on return statements. I don’t remember the reason why, but that may be something to try.

Like yves suggests, you definitely need to add some logging to this Rule to narrow down where the error is occurring. You probably should be adding logWarn or logError statements every time you have a return since based on a quick scan they all appear to be error cases.

2 Likes

I used the lock because I have a shared structure, a HashMap, that is used by couple of rules within the same file. I’ve finally seen one null instance after I put in the log, and it has something to do with group members. It is this line:

val switchItem = gWallSwitch.members.findFirst[ t | t.name == switchName ]

I think the problem is within OpenHab itself. My rules have the synchronized block, so it can’t be a threading issue there. And consider that the null error only shows up very rarely, it must be a racing condition somewhere else, likely in OpenHab.

Rules DSL is not Java and in many respects it is unlike other programming languages. When you try to structure your coffee in ways you after used to work functions and data structures cobbled together from lists and maps and especially if you try to implement synchronization yourself they are coffee smells. Rules DSL doesn’t like coding structures like these and your coffee will be unnecessarily long, hard to maintain, and brittle.

So, if you are already a programmer I suggest looking into JSR223 Rules. It lets you build rules using Jython, JavaScript, or Groovy and it supports all of these sorts of coming common programming approaches.

If you are willing to step back and change how you approach coding your rules you can side step a lot of problems like theses and end up with short, easy to understand, and maintainable rules that don’t have these sorts of problems. But it is quite a challenge for a lot of people who already know how to program and it isn’t worth the effort for a lot of them.

If you must have a shared structure like this, then use a thread safe one like https://docs.oracle.com/javase/6/docs/api/java/util/concurrent/ConcurrentHashMap.html. You really need to go out of your way in Rules DSL to:

  • make your Rules run as fast as possible
  • not block other rules from running

See Why have my Rules stopped running? Why Thread::sleep is a bad idea for details.

Actually, it can because locking doesn’t seem to work very well with Rules DSL in general over and above the problems with locking up long running rules when the locks to work.

But assuming that your locks are working doesn’t mean that all of OH’s locks are working. If I had to guess, I’d first look to see if gWallSwitch.members is using a thread safe List. I suspect that the list is being monkeyed with while your findFirst is running.

I’ve only seen one other person report this as a problem and I’ve never seen it myself so I can’t say why you might be seeing it or if that is even relevant.

Thanks Rich. Yes my plan is move to JSR223 as soon as 2.4 is released. Last I looked the Jython wrapper requires OH 2.4. Do you know when 2.4 is released?

You can start with the 2.4 M5 milestone. I don’t think there have been any major bugs found in it that release.

The devs are pushing for a six month release schedule so I would expect 2.4 to be released sometime in December or January.

You can also run on the snapshots for a bit. I’m currently running on the snapshots (from a few days ago) to get some bug fixes I need to document the NGRE.

Rich, I just install 2.4 M5. Pretty painless except for the re-adding of the ZWave things. I also got the first hello_world jython script working. This is great as it opens up the ability to modulize code as well as unit testing the core logic.

Thanks for the suggestion.

3 Likes

even if I don’t work with zwave, I do prefer the installation with scripts. Much easier to be sure to have everything installed the same.