JRuby OpenHAB Rules System

Ah, there you are! That makes sense. With this I can split the rule files again and it works nicely.

Another noobie-question:
Is timers.cancel(someItem) if timers.include?(someItem) necessary or can it be reduced to timers.cancel(someItem) in a typical use case? Typical means: Usually I am not interested in whether the timer could be canceled or not. I assume it will get canceled if there exists one for the item.

Correct - you donā€™t have to check if it exists before canceling. And it will return true if it canceled; false otherwise. https://openhab.github.io/openhab-jruby/main/OpenHAB/DSL/TimerManager.html#cancel-instance_method

Alright then. I think Iā€˜m done with the refactoring from RuleDSL to JRuby. :star_struck:

I will make some cosmetic changes here and there and will post examples afterwards in the examples section of this forum to show how I solved certain use cases to ultimately inspire more people to use this wonderful scripting language.

Thank you all for the great support!

3 Likes

I am trying to open/close my blinds based on the astro binding but want to consider differences in weekends or holidays.

The trigger condition would be:
Mo-Fr 10 minutes after sunrise but never before 06:30 and not when a switch item ā€žholidayā€œ is ON
Sa-Su 10 minutes after sunrise but never before 07:00

The ā€ž10 minutes afterā€œ and ā€žnever beforeā€œ can be handled by the astro binding.
But how can I combine both triggers in one rule? My current approach would be one rule for each trigger condition but can this also be put into one rule?

your code goes here
rule "Open blinds in the morning" do
  channel "astro:sun:home:rise#event"
  not_if { Time.now.holiday? } # or check your holiday item here instead
  run do
    earliest = Time.now.weekend? ? "07:00" : "06:30"
    earliest = Time.parse(earliest)

    after([earliest, Time.now].max) do
      # Open blinds
    end
  end
end

Reason this works:
after can accept not just duration (from now) but also an absolute time, thus saving you from having to do any calculations / conversions.

This will not open the blinds on holiday. Do you still want to open it on holiday but never before 7am like itā€™s the weekend?

If you still want to open the blinds during holidays, but after 7am, it would look like this:

rule "Open blinds in the morning" do
  channel "astro:sun:home:rise#event"
  run do
    now = Time.now
    earliest = (now.weekend? || now.holiday?) ? "07:00" : "06:30"
    earliest = Time.parse(earliest)

    after([earliest, now].max) do
      # Open blinds
    end
  end
end

1 Like

Well, this feature makes things a lot easier!

Yes, the blinds should open on holidays as well (or when the ā€œholidayā€ switch is set to ON, as ephemeris does not know when I took days off ^^) but at 07:00 then as your second rule suggests.

Thank you!

Hi, I just found something very odd which I currently can only nail down to a race condition issue and hope you can guide me to the right direction.

I have a rule which reacts on opening/closing a window and changes the thermostat temperature:

rule "Window opened/closed" do
    changed Fensterkontakte.members, to:[OPEN, CLOSED], for: 5.seconds
    run do |event|        
        window = event.item        
        temperatureSetpoint = window.location.equipments(Semantics::HVAC).members.points(Semantics::Setpoint, Semantics::Temperature).first     
        currentTemperature = window.location.equipments(Semantics::HVAC).members.points(Semantics::Measurement, Semantics::Temperature).first    
        thermostate = window.location.equipments(Semantics::HVAC).members.points(Semantics::Switch).first 

        if window.open?
            logger.info("#{window} was opened. Outside temperature: #{Aussentemperatur.state}")            
            setMetadata(temperatureSetpoint, "temperatureSetpoint_before_opening", temperatureSetpoint.state.to_f) unless timers.include?(temperatureSetpoint)
            timers.cancel(temperatureSetpoint) # In case the window was opened again while a timer was running
		# some more code ...
		else
			temperatureSetpoint_value = temperatureSetpoint.metadata.dig("dynamicMetadata", "temperatureSetpoint_before_opening").to_f
            logger.info("#{fenster} was closed. In 15 minutes #{temperatureSetpoint} will be set to #{temperatureSetpoint_value} Ā°C.")                                                
            after (15.minutes), id: temperatureSetpoint do
                temperatureSetpoint.ensure << temperatureSetpoint_value if temperatureSetpoint.state.to_f == 16.0 # In case the user changed the temperature within these 15 minutes, the system does not change it back
            end
        end   
	end
end

Usually this works as intended but I found an edge case where problems occur. When closing and openenig a window very quickly within 1-2 seconds the line

setMetadata(temperatureSetpoint, "temperatureSetpoint_before_opening", temperatureSetpoint.state.to_f) unless timers.include?(temperatureSetpoint)

gets executed. So the runtime system assumes that there is no active timer but there is definitely one as I closed and opened the window. It looks like the system did not have the time yet to create the timer after having closed the window.
I increased the for: guard to higher values like 10 seconds but this does not have any effect.
What do you suggest? Actively waiting a few seconds before executing this posted line above?

I am running OH 3.4.5 on a Raspberry Pi 4. So hardware is quite limited for sure but I hope there are ways around here.

If you closed the window then reopened it again within 1-2 seconds, you would not receive the CLOSED event because it wouldnā€™t have been closed for 5 seconds. Youā€™ll only receive the OPEN event at the end after 5 seconds.

Also, change this:
after (15.minutes), id: temperatureSetpoint do to:
after 15.minutes, id: temperatureSetpoint do
Or alternatively `after(15.minutes, id: temperatureSetpoint) do

The original version, while it works, seems a bit odd to me.

1 Like

That is correct. In my case the system behaves as if the window was opened and then it was opened again. The closing was not registered. That means that I need to execute the if window open? only when the state was closed before. So:

if window.open? && event.was.closed?

Does this make sense?

How about simplifying it like this, so you donā€™t actually need to create another timer:

rule "Window opened/closed" do
  changed Fensterkontakte.members, to: CLOSED, for: 20.seconds
  changed Fensterkontakte.members, to: OPEN, for: 5.seconds
  run do |event|
    window = event.item
    hvac = window.location.equipments(Semantics::HVAC).members
    setpoint_temp = hvac.points(Semantics::Setpoint, Semantics::Temperature).first
    current_temp = hvac.points(Semantics::Measurement, Semantics::Temperature).first
    thermostate = hvac.points(Semantics::Switch).first

    setpoint_before_opening = setpoint_temp.metadata.dig("dynamicMetadata", "temperatureSetpoint_before_opening")&.to_f

    if window.open?
      logger.info("#{window} was opened. Outside temperature: #{Aussentemperatur.state}")

      unless setpoint_before_opening
        setMetadata(setpoint_temp, "temperatureSetpoint_before_opening", setpoint_temp.state.to_f)
      end
      # some more code ...
    else
      # In case the user changed the temperature within these 15 minutes, the system does not change it back
      setpoint_temp.ensure << setpoint_before_opening if setpoint_before_opening && setpoint_temp.state.to_f == 16.0
      setpoint_temp.metadata["dynamicMetadata"].delete("temperatureSetpoint_before_opening")
    end
  end
end

Iā€™ve also changed your variables into snake_case. Itā€™s the Ruby convention and my syntax checker in vscode keeps warning me about it. In case youā€™re using vscode, Iā€™d recommend installing ruby-lsp extension + rubocop gem to do your auto formatting.

Good hint! I do use VSC, so I will install those extensions.

OK, your adjustments do make sense. My idea for this rule was to change the set point temperature to 16 Ā°C when the window was opened and once the window was closed, OH waits another 15 minutes and then sets it back to its original value.
This assures that the heating does not get swtiched on due to (by Germans beloved) ā€œStoƟlueftenā€ (rapid air exchange). The room has 15 minutes time to adjust itself. It mostly goes back to its original temperature which is why heating is not necessary. Only if the temperature is still below the original threshold before opening the window then of course the heat should get switched on.

In the ā€œsome more codeā€ section I also initiated a timer which sends a WhatsApp message to our phones if the room temperature fell below a certain value. It gets rescheduled as long as the window is opened and canceled once closed.

I think with your modifications (I guess the 20.seconds should be changed to 15.minutes) this should still be possible.

But you are right, your simplification are much more convenient as there is 1 timer less I have to worry about. :slight_smile:

Hey guys, having a general question regarding OH3.4.5. Are there any known issues regarding memory leaks with JRuby?

I sometimes use the ā€œScriptsā€ section on the OH homepage to test simple JRuby scripts and to learn/understand the syntax. However, when executing these simple 4-liners the CPU usage of the OH Docker container increases and increases. At some point it takes ages until OH executes the rule and from that point on I need to restart the container.

In case you need more details, let me know but for now I just wanted to understand whether there are generally known issues.

There isnā€™t a known issue with the jrubys add-on/library on 3.x afaik. @Andrew_Rowe experienced something similar which ended up to do with an openhab core issue but I donā€™t remember whether it was on 4.x or 3.x

While writing rules and sending commands/updates to items I always wondered: Why not using ensure everywhere? The method makes sure that an item only gets updated if its state is different than the command about to be sent. In which scenario would this not make sense?
I mean, there can be edge cases where one reacts to received command instead of changed. But besides that I actually always would use ensure as it does no harm but reduces communication load on the bus.

Iā€™d be interested in hearing what others think about this.

For me, it depends on the specific circumstances and items. I use received_command a lot. Often on virtual items where I donā€™t even bother setting it to off afterwards, so it could stay on forever, or off, I pay no attention to its state. received_command ON just means ā€œtrigger this routineā€. In that situation, I canā€™t use ensure, unless I also made sure it switches back to OFF afterwards.

In other cases when sending a command would cause things to actually publish an mqtt message out or perform an http post request, then I would use ensure.

There was an idea in the past to ā€œsetā€ ensure per rule, but that can be done using an ensure block.

1 Like

does it really? Iā€™ve always wondered. Does it actually check the state and not send a command if the Item is already in that state?

Yes it does, if you used ensure. If it doesnā€™t, that would be considered a bug.

1 Like

:heart: ensure. Itā€™s one of my favorite features of JRuby rules. But it canā€™t always be used. Besides some virtual or proxy items that you may not care to keep state up to date, some devices canā€™t be trusted to accurately portray state (or simply cannot - for example I have a motorized drapery that I can send open, close, or stop, but have no feedback on the current state), so itā€™s important that the command is always sent regardless.

That said, as long as I can have a decent way to force a command to be sent, Iā€™d be very much in favor of an ensure! top level method to perma-enable it. Iā€™m thinking on! vs on, command! vs command, etc. Thereā€™s not an easy way to do that with the << convenience alias for command, but I think Iā€™d rather not have to type .ensure all the time.

1 Like

I like this a lot! Letā€™s make it so! If youā€™re still busy, I can give it a go. Iā€™m happy either way

Yeah, that makes sense to me. So far I have not experienced any device in my house sending wrong states.