[SOLVED] Error with averageSince function in text-based rules (persistence - influxDb, mapdb)

Hi all,
I’ve been having difficulty accessing persistence data. My default persistence is mapdb (largely to be able to restore all the data/switches/etc without a huge data footprint), but most of my data I care about goes to influxdb (not sure if this default setting is where the problem is). I can come up with all sorts of beautiful graphs with this data - most of it for the past year - so the persistence is working well from that end.

I’ve been writing rules to trigger bathroom fans based on humidity, and seem to run into an error using
Ensuite_Humidity.averageSince(now.minusHours(4))

I am persisting all humidity items every 1hour and every change. I don’t believe I have any null values involved, as all the sensors have been working well recently. When a rule calls the averageSince function, I get an error in the logs.

My relevant rule:

rule "Ensuite Bathroom Humidity"
	when
        	Item Ensuite_Humidity changed
	then

		var Number calcDownstairsAverage=0
		logInfo("BathroomFans.rules", "About to call up historical value.")
		var Number prevAverage = Ensuite_Humidity.averageSince(now.minusHours(4))
		var Number TriggerHumidity1=DownstairsAverageHumidity.state +11
		var Number TriggerHumidity2=prevAverage+11
		var Number StopHumidity1=DownstairsAverageHumidity.state+9
		var Number StopHumidity2=prevAverage+9
		var Number MinHumidity = 47
		logInfo("BathroomFans.rules", "About to calculate average.")
		calcDownstairsAverage = (Ensuite_Humidity.state+Livingroom_Humidity.state+Mudroom_Humidity.state+GuestBath_Humidity.state+hallwayHumidity.state)/5
		logInfo("BathroomFans.rules", "Average calculated.")
		logInfo("BathroomFans.rules", calcDownstairsAverage)
		logInfo("BathroomFans.rules", "About to update item.")
		DownstairsAverageHumidity.sendCommand(calcDownstairsAverage)
		logInfo("BathroomFans.rules", "Updated item.")
		logInfo("BathroomFans.rules", DownstairsAverageHumidity.state)
		if((Ensuite_Humidity.state >= TriggerHumidity1)||(Ensuite_Humidity>=TriggerHumidity2)){
		  logInfo("BathroomFans.rules", "Ensuite Humidity Rule Triggered.")
		  val StringBuilder message = new StringBuilder("Ensuite Bathroom Humidity is ")
		  message.append(Ensuite_Humidity.state)
		  message.append(". ")
		  if (Ensuite_Humidity.state >= MinHumidity) {

			if(Ensuite_Humidity.state >= TriggerHumidity1) {
				message.append("This is higher than regional humidity threshold )")
				message.append(TriggerHumidity1)
				message.append(").  ")
			}
			if(Ensuite_Humidity.state >= TriggerHumidity2) {
				message.append("This is higher than previous average humidity threshold (")
				message.append(TriggerHumidity2)
				message.append(").  ")
			}
			
		    if (FF_EnsuiteShowerFan.state == OFF) {
				FF_EnsuiteShowerFan.sendCommand(ON)
				FF_EnsuiteToiletFan.sendCommand(ON)
				message.append("Turning on shower and toilet fans.")
		    }
		    else {
				message.append("Fan already on.")
				FF_EnsuiteToiletFan.sendCommand(ON)  // ensuring the toilet fan is on, and not needlessly complicating the code
		    }
		  }
		  else {
		    message.append("This still below the minimum humidity threshold (")
			message.append(MinHumidity)
			message.append(")")
		  }
		  logInfo("BathroomFans.rules", message)
		}
	
	else if ((((Ensuite_Humidity.state < StopHumidity1)&&(FF_EnsuiteShowerFan.state == ON))||((Ensuite_Humidity.state < StopHumidity2)&&(FF_EnsuiteShowerFan.state == ON)))&&(StillShowering==OFF)) {
		  val StringBuilder message = new StringBuilder("Ensuite Bathroom Humidity is ")
		  message.append(Ensuite_Humidity.state)
		  message.append(". ")

		  if (Ensuite_Humidity.state < StopHumidity1) {
		    message.append("This is now lower than the regional humidity threshold (")
			message.append(StopHumidity1)
			message.append(") Turning off shower fan.")
		  }
		  if (Ensuite_Humidity.state < StopHumidity2) {
		    message.append("This is now lower than the previous average humidity threshold (")
			message.append(StopHumidity2)
			message.append(") Turning off shower fan.")
		  }
		  logInfo("BathroomFans.rules", message)
		  FF_EnsuiteShowerFan.sendCommand(OFF)
		  FF_EnsuiteToiletFan.sendCommand(OFF)
		}
end

and my relevant log:

2019-11-06 11:52:37.397 [INFO ] [el.core.internal.ModelRepositoryImpl] - Loading model 'BathroomFans.rules'

2019-11-06 11:52:44.282 [INFO ] [el.core.internal.ModelRepositoryImpl] - Refreshing model 'BathroomFans.rules'

2019-11-06 11:52:57.449 [INFO ] [home.model.script.BathroomFans.rules] - About to call up historical value.

2019-11-06 11:52:57.459 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule 'Ensuite Bathroom Humidity': An error occurred during the script execution: Could not invoke method: org.eclipse.xtext.xbase.lib.ObjectExtensions.operator_plus(java.lang.Object,java.lang.String) on instance: null

and my persistence:

// influxdb persist file
Strategies {
everyMinute : "0 * * * * ?"
everyHour : "0 0 * * * ?"
everyDay : "0 0 0 * * ?"
every2Minutes : "0 */2 * ? * *"
every5Minutes : "0 */5 * ? * *"
every15Minutes : "0 */15 * ? * *"
default = everyChange
}

Items {
gTemperatureItems*: strategy = everyHour, everyChange
gHumidityItems*: strategy = everyHour, everyChange
gMotionItems*: strategy = every15Minutes, everyChange
gAnyRecentMotion*: strategy = every15Minutes, everyChange
gLightSensorItems*: strategy = everyHour, everyChange
gWeatherTemp*, gWeatherHumidity*: strategy = everyHour
Presence_Sensors*: strategy = every15Minutes, everyChange
gLock*: strategy = every15Minutes, everyChange
gFans*: strategy = everyChange, everyDay
gEspMotion*: strategy = everyChange
}

A snippet of my graphana data for today. (FYI, the downards spike in humidity on the ensuite fan was me toggling the data between values to artifically trigger my rule)


Any ideas on directions to go from here?
If I have default persistence being mapdb, does this affect the averageSince function? Is there anyway to specify which persistence I have averageSince use, rather than having to switch everything to influxdb?

Thanks
Ben

OH 2.4 on Raspberry Pi3B (Openhabian) with zwave snapshot 2.5 as of Nov 5th.

Not quite. You get an error from somewhere in that rule but it’s not caused by the line to call averageSince() as there’s no arithmetic operation in that line but that’s what the error says.

I believe yes. If you persist the item in both databases, how is OH supposed to know which one to retrieve the data from ? That’s when the default comes into play.
It is an error to make mapdb the default persistence. It just stores a single value and (likely) not the time so it is pointless to apply averageSince() do that likely returns null as @rossko57 correctly states. Change default persistence to influxdb.

prevAverage = Ensuite_Humidity.averageSince(now.minusHours(4))

Which persistence service will that access?
mapdb holds only one value and cannot provide a sensible answer here.
In rules DSL, a nonsense answer is null.

TriggerHumidity2=prevAverage+11
You cannot later add anything to null (it’s null, not zero).

Check in PaperUI
Configuration → System → Persistence

Thanks all,
I was quite aware my default persistence was mapdb (from Configuration->System->Persistence)
I suppose I was imagining there would be a way to specify which persistence to depend on to calculate the average.
If would have been nice to have an option like Ensuite_Humidity.Influxdb.averageSince(now.minusHours(4)) … etc.

In the long run, however, the light switches would contribute very little to my data footprint, and changing my default persistence to influxdb is probably the most straight-forward.

mstormi: I think the averageSince function does involve arithmetic - and the logging before (and not afterwards) seems to indicate this as well. As rossko57 points out, though, this likely null value from the averageSince function could throw an exception with the following equation as well.

I’ll move my mapdb persistence items over to influxdb and change my default persistence and then look into this again (that and the errors thrown 5iver’s schlage door lock rules that accessed persistence data as well).

Thanks!
Ben

There is: Persistence | openHAB.
In particular read the lines AFTER the code examples

2 Likes

You can persist and should all items in both DBs. But you must change the default persistence. MapDB does make no sense at all BTW.
Eventually explicitly select a DB when using DB functions, see Vincent’s hint. But once you changed default it should also work with your current code.

Thanks @mstormi and @vzorglub
I’ve updated the default persistence to InfluxDB now, to keep it simple. I realize I could specify the persistence service for the .persist command if wanting to use a non-default service, and assume there is something similar for the averageSince command.

Since I’ve updated the default persistence service. Through some further trial and error, I realized an item from my items file didn’t have a value yet, DownstairsAverageHumidity. I artificially addressed this (as my null-checking hadn’t seemed to be working correctly).
Even after explicitly setting this number item to 50, and still had an error thrown from the addition operation.

relevant number declaration from sensors.items

Number DownstairsAverageHumidity "Downstairs Average Humidity" (gHumidityItems)

Rules:

rule "Ensuite Bathroom Humidity"
	when
        	Item Ensuite_Humidity changed
	then

		var Number calcDownstairsAverage=0
		logInfo("BathroomFans.rules", "About to call up historical value.")
		var Number prevAverage = 0 // will be set/calculated later
		var Number TriggerHumidity1=0 // will be set/calculated later
		var Number TriggerHumidity2=0 // will be set/calculated later
		var Number StopHumidity1=0  // will be set/calculated later
		var Number StopHumidity2=0  // will be set/calculated later
		var Number MinHumidity = 47
		
		prevAverage = Ensuite_Humidity.averageSince(now.minusHours(4))
		logInfo("BathroomFans.rules", "Done Calling")
		if ((prevAverage == null)||(prevAverage == NULL)) {
		  logInfo("BathroomFans.rules", "Previous Average was null.  Setting to ecobee humidity")
		  prevAverage = hallwayHumidity.state
		}
		prevAverage = hallwayHumidity.state // will need to remove this line later
		logInfo("BathroomFans.rules", "About to check DownstairsAverageHumidity")
		
		if ((DownstairsAverageHumidity.state == null)||(DownstairsAverageHumidity.state == NULL)) {
		  logInfo("BathroomFans.rules", "PDownstairsAverageHumidity.state was null, likely not used yet.  Setting to ecobee humidity")
		  DownstairsAverageHumidity.sendCommand(hallwayHumidity.state) 
		}
		DownstairsAverageHumidity.sendCommand(50) // will need to remove this line later
		
		logInfo("BathroomFans.rules", "About to do math")
		TriggerHumidity1=DownstairsAverageHumidity.state +11
		logInfo("BathroomFans.rules", "math step 1")
		TriggerHumidity2=prevAverage+11
		logInfo("BathroomFans.rules", "math step 2")
		StopHumidity1=DownstairsAverageHumidity.state+9
		logInfo("BathroomFans.rules", "math step 3")
		StopHumidity2=prevAverage+9
		logInfo("BathroomFans.rules", "math step 4")
		
		logInfo("BathroomFans.rules", "About to calculate average.")
		calcDownstairsAverage = (Ensuite_Humidity.state+Livingroom_Humidity.state+Mudroom_Humidity.state+GuestBath_Humidity.state+hallwayHumidity.state)/5
		logInfo("BathroomFans.rules", "Average calculated.")
		logInfo("BathroomFans.rules", calcDownstairsAverage)
		logInfo("BathroomFans.rules", "About to update item.")
		DownstairsAverageHumidity.sendCommand(calcDownstairsAverage)
		logInfo("BathroomFans.rules", "Updated item.")
		logInfo("BathroomFans.rules", DownstairsAverageHumidity.state)
		if((Ensuite_Humidity.state >= TriggerHumidity1)||(Ensuite_Humidity>=TriggerHumidity2)){
		  logInfo("BathroomFans.rules", "Ensuite Humidity Rule Triggered.")
		  val StringBuilder message = new StringBuilder("Ensuite Bathroom Humidity is ")
		  message.append(Ensuite_Humidity.state)
		  message.append(". ")
		  if (Ensuite_Humidity.state >= MinHumidity) {

			if(Ensuite_Humidity.state >= TriggerHumidity1) {
				message.append("This is higher than regional humidity threshold )")
				message.append(TriggerHumidity1)
				message.append(").  ")
			}
			if(Ensuite_Humidity.state >= TriggerHumidity2) {
				message.append("This is higher than previous average humidity threshold (")
				message.append(TriggerHumidity2)
				message.append(").  ")
			}
			
		    if (FF_EnsuiteShowerFan.state == OFF) {
				FF_EnsuiteShowerFan.sendCommand(ON)
				FF_EnsuiteToiletFan.sendCommand(ON)
				message.append("Turning on shower and toilet fans.")
		    }
		    else {
				message.append("Fan already on.")
				FF_EnsuiteToiletFan.sendCommand(ON)  // ensuring the toilet fan is on, and not needlessly complicating the code
		    }
		  }
		  else {
		    message.append("This still below the minimum humidity threshold (")
			message.append(MinHumidity)
			message.append(")")
		  }
		  logInfo("BathroomFans.rules", message)
		}
	
	else if ((((Ensuite_Humidity.state < StopHumidity1)&&(FF_EnsuiteShowerFan.state == ON))||((Ensuite_Humidity.state < StopHumidity2)&&(FF_EnsuiteShowerFan.state == ON)))&&(StillShowering==OFF)) {
		  val StringBuilder message = new StringBuilder("Ensuite Bathroom Humidity is ")
		  message.append(Ensuite_Humidity.state)
		  message.append(". ")

		  if (Ensuite_Humidity.state < StopHumidity1) {
		    message.append("This is now lower than the regional humidity threshold (")
			message.append(StopHumidity1)
			message.append(") Turning off shower fan.")
		  }
		  if (Ensuite_Humidity.state < StopHumidity2) {
		    message.append("This is now lower than the previous average humidity threshold (")
			message.append(StopHumidity2)
			message.append(") Turning off shower fan.")
		  }
		  logInfo("BathroomFans.rules", message)
		  FF_EnsuiteShowerFan.sendCommand(OFF)
		  FF_EnsuiteToiletFan.sendCommand(OFF)
		}
end

Logs:

2019-11-13 09:54:38.563 [vent.ItemStateChangedEvent] - Ensuite_Humidity changed from 59.98 to 80

2019-11-13 09:54:42.833 [vent.ItemStateChangedEvent] - network_servicedevice_192_168_0_132_80_lastseen changed from 2019-11-13T09:53:42.757-0800 to 2019-11-13T09:54:42.785-0800

==> /var/log/openhab2/openhab.log <==

2019-11-13 09:54:42.833 [DEBUG] [.internal.InfluxDBPersistenceService] - got DateTimeType value 1573667682785

2019-11-13 09:54:43.740 [INFO ] [home.model.script.BathroomFans.rules] - About to call up historical value.

2019-11-13 09:54:43.748 [DEBUG] [.internal.InfluxDBPersistenceService] - got a query

2019-11-13 09:54:43.751 [DEBUG] [.internal.InfluxDBPersistenceService] - query string: select value from "autogen"."Ensuite_Humidity" where  time > 1573653283s  limit 2147483647

2019-11-13 09:54:43.779 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.782 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.784 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.787 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.790 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.792 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.795 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.797 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.800 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.803 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.806 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.808 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.811 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.813 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.816 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.819 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.822 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.825 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.828 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.830 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.833 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.836 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.839 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.842 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.845 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.848 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.851 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.853 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.856 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.859 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.862 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.865 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.868 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.870 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.874 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.876 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.880 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.883 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.886 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.889 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.892 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.894 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.898 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.900 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.904 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.907 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.911 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.914 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.918 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.921 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.925 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.928 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.931 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.934 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.937 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.940 [DEBUG] [.internal.InfluxDBPersistenceService] - objectToState found a NumberItem

2019-11-13 09:54:43.949 [INFO ] [home.model.script.BathroomFans.rules] - Done Calling

2019-11-13 09:54:43.957 [INFO ] [home.model.script.BathroomFans.rules] - About to check DownstairsAverageHumidity

==> /var/log/openhab2/events.log <==

2019-11-13 09:54:43.967 [ome.event.ItemCommandEvent] - Item 'DownstairsAverageHumidity' received command 50

==> /var/log/openhab2/openhab.log <==

2019-11-13 09:54:43.967 [INFO ] [home.model.script.BathroomFans.rules] - About to do math

2019-11-13 09:54:43.973 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule 'Ensuite Bathroom Humidity': An error occurred during the script execution: Could not invoke method: org.eclipse.xtext.xbase.lib.ObjectExtensions.operator_plus(java.lang.Object,java.lang.String) on instance: null

2019-11-13 09:54:43.987 [DEBUG] [.internal.InfluxDBPersistenceService] - got DecimalType value 50

2019-11-13 09:54:43.990 [DEBUG] [.internal.InfluxDBPersistenceService] - got DecimalType value 50

I have no doubt I have some other errors later in the script, but can’t seem to sort out why this addition is throwing an error. Any ideas?
TriggerHumidity1 is defined as a number
DownstairsAverageHumidity is defined in an items file as a number
Both should no longer be null at the time of the addition

Thanks
Ben

For your ‘null’ checks, don’t you need ‘===’? NULL can use ‘==’ but ‘null’ needs ‘===’. (From: After openhabe 2.3 update, homematic binding don't update item status) I don’t know if this is part of your issue or not, but maybe it’s worth a try.

WARNING sendCommand is asynchronous, it goes off onto openHABs event bus and actions it may trigger - like autoupdate changing the Item’s state - will happen shortly, but not immediately.
The rule doesn’t stop and wait for that to happen. So the Item state a few lines later may be either the new state or the old state.

Maybe that is what you intend, but it is not obvious.

It would probably be helpful to know what you were going to do math on

logInfo("BathroomFans.rules", "About to do math on " + DownstairsAverageHumidity.state.toString)

Also state objects are not actually Number types, rules can be picky in odd ways - try

TriggerHumidity1=(DownstairsAverageHumidity.state as Number) + 11
1 Like

Thanks @rossko57, The ‘as Number’ section was the key.
I also learned that I can’t log numbers directly:
logInfo(“BathroomFans.rules”, calcDownstairsAverage) - will throw an error
logInfo(“BathroomFans.rules”, "calculated as: " + calcDownstairsAverage) - will work (I assume this forces a string conversion for the number)

Thanks @jswim788 - I would have never thought to consider a triple = sign. I have put this into my rule largely for error checking, but can’t expect to likely require that bit of code anymore now that my influxdb history is full.

Question:
If sendCommand is asynchronous, is postUpdate a better option, or is there something else that is not asynchronous?

I’m not sure it’s going to be an actual issue for me, but it would be good to know.

For those attempting to create a similar humidity rule, here is my working rule:
(edited to add “.state” to the StillShowering item, to allow the fans to shut off correctly)
(edited to fix the == NULL from ===NULL – in contrast to the ===null)

rule "Ensuite Bathroom Humidity"
	when
        	Item Ensuite_Humidity changed
	then

		var Number calcDownstairsAverage=0
		var Number prevAverage = 0 // will be set/calculated later
		var Number TriggerHumidity1=0 // will be set/calculated later
		var Number TriggerHumidity2=0 // will be set/calculated later
		var Number StopHumidity1=0  // will be set/calculated later
		var Number StopHumidity2=0  // will be set/calculated later
		var Number MinHumidity = 47
		prevAverage = Ensuite_Humidity.averageSince(now.minusHours(4))

		if ((prevAverage === null)||(prevAverage == NULL)) {
		  logInfo("BathroomFans.rules", "prevAverage was null.  Setting to ecobee humidity")
		  prevAverage = hallwayHumidity.state
		}
		// prevAverage = hallwayHumidity.state // was for debuging
		
		if ((DownstairsAverageHumidity.state === null)||(DownstairsAverageHumidity.state == NULL)) {
		  logInfo("BathroomFans.rules", "DownstairsAverageHumidity.state was null, likely not used yet.  Setting to ecobee humidity")
		  DownstairsAverageHumidity.sendCommand(hallwayHumidity.state) 
		}
		// DownstairsAverageHumidity.sendCommand(50) // was for debuging
		
		TriggerHumidity1=(DownstairsAverageHumidity.state as Number) + 11
		TriggerHumidity2=prevAverage+11
		StopHumidity1=(DownstairsAverageHumidity.state as Number) +9
		StopHumidity2=prevAverage+9

		calcDownstairsAverage = ((Ensuite_Humidity.state as Number)+(Livingroom_Humidity.state as Number)+(Mudroom_Humidity.state as Number)+(GuestBath_Humidity.state as Number)+(hallwayHumidity.state as Number))/5
		logInfo("BathroomFans.rules", "Downstairs average humidity calculated as: " + calcDownstairsAverage)
		DownstairsAverageHumidity.sendCommand(calcDownstairsAverage)
		if(((Ensuite_Humidity.state as Number) >= TriggerHumidity1)||((Ensuite_Humidity.state as Number)>=TriggerHumidity2)){
		  logInfo("BathroomFans.rules", "Ensuite Humidity Rule Triggered.")
		  val StringBuilder message = new StringBuilder("Ensuite Bathroom Humidity is ")
		  message.append(Ensuite_Humidity.state)
		  message.append(". ")
		  if ((Ensuite_Humidity.state as Number) >= MinHumidity) {
			if((Ensuite_Humidity.state as Number) >= TriggerHumidity1) {
				message.append("This is higher than regional humidity threshold (")
				message.append(TriggerHumidity1)
				message.append(").  ")		
			}
			if((Ensuite_Humidity.state as Number) >= TriggerHumidity2) {
				message.append("This is higher than previous average humidity threshold (")
				message.append(TriggerHumidity2)
				message.append(").  ")
			}
			
		    if (FF_EnsuiteShowerFan.state == OFF) {
				FF_EnsuiteShowerFan.sendCommand(ON)
				FF_EnsuiteToiletFan.sendCommand(ON)
				message.append("Turning on shower and toilet fans.")
		    }
		    else {
				message.append("Fan already on.")
				FF_EnsuiteToiletFan.sendCommand(ON)  // ensuring the toilet fan is on, and not needlessly complicating the code
		    }
		  }
		  else {
		    message.append("This still below the minimum humidity threshold (")
			message.append(MinHumidity)
			message.append(")")
		  }
		  logInfo("BathroomFans.rules", "Message: " +message)
		}
	
	else if ((((Ensuite_Humidity.state < StopHumidity1)&&(FF_EnsuiteShowerFan.state == ON))||((Ensuite_Humidity.state < StopHumidity2)&&(FF_EnsuiteShowerFan.state == ON)))&&(StillShowering.state==OFF)) {
	logInfo("BathroomFans.rules", "ElseIf1")
		  val StringBuilder message = new StringBuilder("Ensuite Bathroom Humidity is ")
		  message.append(Ensuite_Humidity.state)
		  message.append(". ")

		  if ((Ensuite_Humidity.state as Number) < StopHumidity1) {
		  logInfo("BathroomFans.rules", "ElseIf2")
		    message.append("This is now lower than the regional humidity threshold (")
			message.append(StopHumidity1)
			message.append(") Turning off shower fan.")
		  }
		  if ((Ensuite_Humidity.state as Number) < StopHumidity2) {
		  logInfo("BathroomFans.rules", "ElseIf3")
		    message.append("This is now lower than the previous average humidity threshold (")
			message.append(StopHumidity2)
			message.append(") Turning off shower fan.")
		  }
		  logInfo("BathroomFans.rules", "Message: " +message)
		  FF_EnsuiteShowerFan.sendCommand(OFF)
		  FF_EnsuiteToiletFan.sendCommand(OFF)
		}
	logInfo("BathroomFans.rules", "Rule Completed.")
end

and on top of your own specifiic sensor/fan items, the items file needs:

Number DownstairsAverageHumidity "Downstairs Average Humidity" (gHumidityItems)
Switch StillShowering "Ensuite Showering" <motion> (gAnyRecentMotion,Presence_Sensors) {expire="15m,command=OFF"}

With gHumidityItems persisted to influxdb

My StillShowering switch uses the expire binding. When the shower light switch is turned on, StillShowering is set to ON, with a 15 minute expiry. This stops the humidity rules from turning off the shower fan when the humidity creeps up slowly with a newly started shower, but is still below the threshold.

(also, modify all your sensors data, averaging calculation, etc, as needed.
FYI, in my circumstance, I have a toilet fan very close to the shower fan, so if the humidity gets high, I turn them both on to help lower it faster.

postUpdate is asynchronous too. Both merely send the instruction to the event bus, for all of openHAB to see and act on. Some other part of OH actions the update - eventually.

Your rule knows exactly what is has just sent as update/command. You don’t need to fetch a new value back from the Item.

If you want the “old” value, from before the update/command, capture it in a variable before you issue the update/command, to remove doubt about timing.

If you really really need the newly updated value - let’s say you send command “INCREASE” to a dimmer that responds with “new brightness 80%”, then you are doing it wrong in one rule.
Have rule A send a command, and rule B respond to a state update. Make your rules asynchronous.

Use sendCommand to items linked to binding channels when you want something to happen
Use postUpdate with “Virtual” items or when you want to update a linked item
It makes no sense to use sendCommand to virtual items as there is nothing to send a command to.

Well, in general yes, there are odd exceptions.
I’ve got a few that exploit the separation of command and state (after you’ve disabled autoupdate) e.g. send command “start” to trigger some rule, have that update same Item to “Stated at 22:30” for display.

I had switched to often use sendCommand a little while ago as I was getting some odd missed rule triggering when I used postUpdate (and the rule triggered with item changed).
I agree, though, postUpdate makes more sense.

There’s nothing wrong with using sendCommand. Just appreciate how the events bus works and that state is never synchronized to commands.

Using postUpdate and tracking state changes is equally effective of course in normal use, generates less logging etc.

There is a proviso - when you deal with multiple events very close together in time. Less than 10mS.

A command is an event of itself, you can trigger rules from it and test its value in the rule (with receivedCommand implicit variable) - and everything works even under rapid fire conditions, because you are looking at a single event

Updates however, even though you can trigger rules from them, you cannot examine the value of the update event directly. There is no receivedUpdate implicit variable. You have to examine the Item state itself, and that exposes you to the risk that something else has interfered with it since the update event was fired; the current Item state may not match the event you are currently looking at.
You really don’t run into that unless you have rapid updates flying about, or your system is creaking under heavy load.

Note that changed rule triggers do what they say and are not intended to fire on every update.

1 Like

Note that you use ‘===’ to compare to ‘null’, but ‘==’ to compare to NULL, so you want:

if ((prevAverage === null)||(prevAverage == NULL)) {
2 Likes