[JSR223][Jython]What is the correct way to avoid a race condition getting an item value inside a /received update/ triggered rule?

In your example you need it because you have multiple items able to trigger the rule and you are using the received update trigger, that is far less common. I have some rules that I want to fire every time the device provides a status update, so that I can make sure it is set to the value the automation wants it to be at. I don’t think your rule needs to use received update, I think changed would be enough. changed fires after the item state has been updated, so you don’t have a race between your rule and the event update code in openHAB.

That is not what this is, the received update and changed triggers work in different ways, as intended. The received update trigger fires when an item state update event is put on the event bus. The changed trigger fires after the aforementioned update event is processed and the update changed the state. Often changed is the more appropriate trigger to use. This is not a case of “by design” it works this way so that’s it, this is “by design” both triggers are available because they are different and each have their use case.

In hopes of helping you along your openHAB/Python learning curve, I took your script in the first post and rewrote it. I have switch over to the easier way to do logging, sending commands, and getting item states. I have also demonstrated a more readable way to get the most relevant item state. And I have also changed some of your conditionals to be more Pythonic. Contrary to my above suggestion that changed might be a more appropriate trigger, I have left it as received update because I don’t know what your exact use case is. Also to demonstrate in case you ever need to use received update, because there may be cases where you need the rule to run even if the item state did not changed, at which time it’s up to you to get the most relevant item state as required by your code.

from core.rules import rule
from core.triggers import when

"""
from org.slf4j import LoggerFactory
def logprint(text):
    pass
    LoggerFactory.getLogger("org.eclipse.smarthome.automation.examples").info(text)
"""
from core.log import logging, LOG_PREFIX
log = logging.getLogger("{}.nightlights".format(LOG_PREFIX))

"""
@rule("Sunset Rule", description="", tags=["schedule"])
@when("Channel \"astro:sun:local:civilDusk#event\" triggered START")
def Sunset_Decorators(event):
    item=ir.getItem("DarkOutside")
    item.send(ON)
Use events.sendCommmand("string", "string")
"""
@rule("Sunset Rule", tags=["schedule"])
@when("Channel astro:sun:local:civilDusk#event triggered START")
def Sunset_Decorators(event):
    events.sendCommand("DarkOutside", "ON")

@rule("Sunrise Rule", description="", tags=["schedule"])
@when("Channel \"astro:sun:local:civilDawn#event\" triggered START")
def Sunrise_Decorators(event):
    events.sendCommand("DarkOutside", "OFF")

@rule("Night Lights", tags=["schedule"])
@when("Item DarkOutside received update")
@when("Item SleepMode received update")
def NightLights_Decorators(event):
    """
    isDarkOutside=1 if itemDarkOutside.getState() == ON else 0
    isSleepMode=1 if itemSleepMode.getState() == ON else 0
    Don't cache the item, use True/False
    """
    isDarkOutside = True if ir.getItem("DarkOutside").getState() == ON else False
    isSleepMode = True if ir.getItem("SleepMode").getState() == ON else False

    if event.itemName == "DarkOutside":
        isDarkOutside = True if event.itemState == ON else False
    else:
        isSleepMode = True if event.itemState == ON else False

    """
    logprint("dark: " + str(isDarkOutside) + "   sleep: " + str(isSleepMode))
    string.format is your friend, you can even use names for the fields to replace
    """
    log.info("dark: {dark}   sleep: {sleep}".format(
        dark=isDarkOutside,
        sleep=isSleepMode
    ))

    # in Python you can 'if var' to check 'if var == True'
    events.sendCommand("NightLights", "ON" if isDarkOutside else "OFF")
    events.sendCommand("MainGateLights_On", "ON" if isDarkOutside and not isSleepMode else "OFF")
    itemMainGateLights=ir.getItem("MainGateLights_On")
    itemMainGateLights.send(ON if isDarkOutside!=0 and isSleepMode==0 else OFF)
4 Likes

Oh wow. Thank you! I should have learned this years ago. I can’t believe I used to do everything in C++. When all you have is a hammer, everything looks like a nail! I’m liking Python more by the minute.

:orange_heart: :orange_heart: :orange_heart:… I can’t say anything else!

Hey, in the rule, instead of first getting the state from the item registry and then overriding (which violates DRY by repeating each item name), what do you think about this:

def GetUpdatedItemState(itemName, event)
	return ir.getItem(itemName).getState() if itemName!=event.itemName else event.itemState

...
    isDarkOutside = True if GetUpdatedItemState("DarkOutside",event) == ON else False
    isSleepMode = True if GetUpdatedItemState("SleepMode",event) == ON else False

Regarding update versus command, it’s exactly because I want to rule to run also when the state is reiterated that I chose “received update”. Z-wave isn’t perfect, and power outages are not unheard of here, so it’s good if things eventually get to the correct state automatically even if the system was off during a trigger. Although I haven’t done it yet, I plan to have the DarkOutside state refreshed every 5 minutes or so… so that it will re-trigger as “Off” throughout the day, and re-trigger as “On” throughout the night. That way any inconsistency will eventually correct itself, no matter where it came from.

I’m planning to do this by having a rule on a 5-minute cron-triggered rule check whether we’re between civilDawn and civilDusk or not, and update DarkOutside accordingly.
How would you recommend doing that in an elegant manner in Python?

Here’s how I had it in lua in Fibaro HC2. Absolutely fugly but worked. Note that there is no way for fibaro lua scripts not to be fugly (no way not to hardcode item IDs for one) so you get desensitized after a while.

--[[ 
%% properties 
%% autostart 
%% globals 
--]] 


--night light maintenance
--kitchen ceiling fan relay on
--(to prevent ceiling fans being off after power outage)


local idKitchenFans = 133;
local idNightLights = 137;


-- Initial parameters ------------------------------------------------------- 
 
if (fibaro:countScenes() > 1) then 
	-- fibaro:debug("SCENE ABORT --------------------------") 
	fibaro:abort() 
end
 

 
-- Functions ----------------------------------------------------------------- 
 
-- function is changing time in text format  "HH:MM" or os.date("*t") 
-- to number of minutes from midnight 
 
function toMinutes(czasHHMM) 
	local a 
	if type(czasHHMM) == "string" then 
		a = tonumber(string.sub(czasHHMM, 1, 2)) * 60 + tonumber(string.sub(czasHHMM, 4, 5)) 
	else    
		a = tonumber(czasHHMM.hour) * 60 + tonumber(czasHHMM.min) 
	end 
	return a 
end 
 
-- changing minutes to "HH:MM" 
 
function toHHMM(minutes) 
	local b = string.format("%02d",((minutes/60*100) - ((minutes/60*100) % 100))/100) 
	local c = string.format("%02d",minutes - (tonumber(b)*60)) 
	local d = b..":"..c 
	return d 
end 
 
-- end of Functions----------------------------------------------------------- 
 
 
function fnNightLights()
  
-- counting how many minutes is from midnight to sunset and sunrise
 
	local sunriseMinutes = toMinutes(fibaro:getValue(1, 'sunriseHour')) 
	local sunsetMinutes = toMinutes(fibaro:getValue(1, 'sunsetHour')) 
 
	-- counting how many minutes is from midnight to now 
	 
	local nowMinutes = toMinutes(os.date("*t")) 
  
	if nowMinutes > sunriseMinutes+5 and nowMinutes < sunsetMinutes-5 then
    
	  fibaro:call( idNightLights, 'turnOff' );
    
    else

      fibaro:call( idNightLights, 'turnOn' );
    
    end

  
	fibaro:call (idKitchenFans, 'turnOn' );
   	

  setTimeout(fnNightLights,5*60*1000);

  
end -- end while


fnNightLights();

```lua


Sorry for subjecting your eyes to it. That's what I had. I'll bet you could do it on a single line in python. Will you accept the challenge? :)

Since you’re only using the logger in the rule function, drop the import and use the log attribute added by the rule decorator…

NightLights_Decorators.log.info("dark: {dark}   sleep: {sleep}".format(dark=isDarkOutside, sleep=isSleepMode))

For any rules DSL users seeing this and freaking out about having to use the getState() method, Items also have a state attribute. But instead of getting the Item to get the state, it’s much easier to just use the items object provided in the default script scope (it’s at the bottom of this list). This provides the states of all Items as a dict…

isSleepMode = True if items["SleepMode"] == ON else False

I’ve only looked briefly at your thread and the rule @CrazyIvan359 put together, but I think you could implement this by just adding a cron trigger to the rule. Also, your rule shrinks quite a bit if you put the logic into a group, and could lose another line if you put the lights in a group too…

Switch DarkOutside "Dark" <moon> (Bench, gLights_Out)
Switch SleepMode "Sleep" <bedroom_blue> (Bench, gLights_Out)
Group:Switch:AND(ON,OFF) gLights_Out "Time for lights out [%s]"

… and then the last rule can be…

@rule("Night Lights", tags=["schedule"])
@when("Item gLights_Out changed")
@when("Time cron 0 0/5 * * * ?")# event will be None
def NightLights_Decorators(event):
    events.sendCommand("NightLights", str(items["gLights_Out"]) if event is None  else event.itemState))
    events.sendCommand("MainGateLights_On", str(items["gLights_Out"] if event is None event.itemState))

The is not much documentation on profiles and I haven’t read throuh the code yet, but if you had the lights in a group with a profile to follow the gLights_Out group, you wouldn’t need a rule at all… but the 5 minute cron trigger throws a wrench into that one!

1 Like

No problem, glad to help.

I always forget about these tricks. I need to spend some time updating some of rules to use them. I was not aware of either when I first started writing my JSR223 rules.

What is DRY? I would suggest this modification to prevent errors if event is None:

def GetUpdatedItemState(itemName, event):
    if event is None:
        return items[itemName]
    else
        return items[itemName] if itemName != event.itemName else event.itemState

@5iver 's suggestions for putting your 2 condition items in a group is definitely the way to go. You would be able to use the changed trigger and not have to worry about timing. As for setting DarkOutside, I am doing something similar that boils down to this:

DateTime AstroCivilDawn "Dawn Start [%1$tl:%1$tM %1$tp]" { channel="astro:sun:home:civilDawn#start" }
DateTime AstroCivilDusk "Dusk End [%1$tl:%1$tM %1$tp]" { channel="astro:sun:home:civilDusk#end" }
from core.date import to_java_zoneddatetime, ZonedDateTime

@rule("Update DarkOutside")
@when("System started")
@when("Time cron 0 */5 * * * ?")
def update_darkoutside():
    now = ZonedDateTime.now()
    command = "OFF"
    if now.isBefore(to_java_zoneddatetime(items[AstroCivilDawn])) or now.isAfter(to_java_zoneddatetime(items[AstroCivilDusk])):
        command = "ON"
    events.postUpdate("DarkOutside", command)
1 Like

Thank you, Scott! I will try this. If this takes care of the race condition, then I take everything back. :slight_smile:

Don’t Repeat Yourself

Thank you, this is exactly what I was looking for.

The example to set DarkOutside is actually the same principle as the Time of Day example script you can find in the Helper Libraries docs, we just don’t have it working with channels yet.

I don’t use jython so it may be different so bear that in mind with the following.

I mostly trigger rules on received command and use the variable called recievedcommand to check what the command was. This way I can move controls without triggering the rule when I wish by using the update method. When I want to trigger the rule I use the command method. I use a lot of Stand alone devices and see openhab as a slave so often I want to update controls because the work has already been done by stand alone devices.

An example is a doorbell where someone presses the push button multiple times. If the button sends an on command, then I will trigger When the item receives the on command so it picks up the multiple pushes with the least delay. Received update I find is the least useful, either use changed or command with the latter working if it is already on and it receives another on command.

I find it useful to watch the event.log file this way you get to see the triggers occurring and in what order they occur.

Interesting. I didn’t know about the events log, that could be useful!

I also completely missed the “received command” trigger, hadn’t heard of that one before. So I figured that one could be useful for the Aeotec Wallmote (Z-wave wall controller) since it has a problem with repeated commands. “changed” doesn’t work because it doesn’t change until you press a different button, and “received update” sometimes repeats, sometimes multiple times, sometimes a whole second late… This thing is really not worth the price – a whopping $80. Anyway, unfortunately “received command” doesn’t trigger at all for this device. I’m sure I’ll find a use for it elsewhere, though.

I’m aware this thread is dated but I ran into this issue this week while migrating my .rules to .py
I was about to post my own tests about that possible bug…
Thanks to @leif for document ing the issue and your tests, thanks to @rlkoshak, @mjcumming, @5iver and everyone contributing here for your very helpful explanations and coding advice.
I assume many have run into this or will do so when migrating - is there a link to here in ‘But How do i…’ which I failed to notice? Or , if not, maybe it should…

1 Like

What issue are you referring to? There was no issue in the OP, other than not knowing how to properly get the state of an Item, which the helper library documentation definitely covers.

I think that is a matter of interpretation – I absolutely still consider it an issue. In my scripts, in the rule handlers I now use event.itemState when reading the triggering item, and ir.getItem to read the state of all other items. And yes, that does work, but as a programmer and designer of other systems I do consider this a workaround. As a script writer it would have been so much easier for me if this had been done in the back-end code, essentially intercepting the return value from getState() with the most current value, if called from within a rule handler with an applicable item.

If the back-end had done that, this whole thread would not exist.

I’m not saying that is the absolutely correct way architecturally… I am however saying that this would remove a scripting pitfall I obviously wasn’t the only one to fall into, hence the resurrection of this thread. :slight_smile: