JRuby OpenHAB Rules System

@NikM you should take advantage of the UoM / Dimensioned items. I rewrote your script below. Note we prefer snake_case in Ruby.

basic_power_consumption , forecast_1h, calculation, etc, are QuantityType, and technically you shouldn’t round / truncate the values. You only want to do that during the display and that’s where the format() comes in.

# Ensure that all relevant items below are declared as
# Number:Power for power
# Number:Energy for energy
rule "eMobility - Stop" do
  received_command TestSwitch
  run do
    logger.info "Start"

    basic_power_consumption = 300 | "kWh"
    forecast_1h = WeatherLocalHours01SolarHarvest.state - basic_power_consumption
    logger.info "Value #{forecast_1h}"

    time_since = Time.now.min.minutes
    average_solar_power = RCT_Power_Solar.average_since(time_since)

    calculation = average_solar_power / 60 * minute
    logger.info "Calculation #{calculation.format("%.2f %unit%")}"

    forecast_1h = (5 | "kWh") - calculation

    logger.info "Result #{forecast_1h.format("%.2f %unit%")}"
  end
end

Thanks a lot @JimT.

All my items are without UoM. I like my values without dimensions. They only get units while displaying. So its blank and easy to handle.
Actually I started openhab with UoM, but it was always more complicated with so without.

Thats will be a hard job for me to use snake_case, I used CamelCase since years… But I will try for the variables in Ruby. Thanks for the hint.

So all in all you think the subtraction is not working, because I dont use UoM?

One further question: What exact is the technical reason I should not round a value, where such a hugh accuracy is not needed?
For me it seems easier to use .round one time, than twice .format(“%.2f %unit%”) to display the value in a more readable…

You can use camelCase if you prefer, Ruby doesn’t care. The snake_case is just by convention.

You can also work without UoM if you prefer, although there should be no reason to avoid UoM because in Ruby it’s just as easy either way.

edit: I’m actually not sure why you got the error NullPointerException here. It seems to be a Java issue. Your initial suspicion about BigDecimals might be true. Can you create a reproduceable test for me to try?

Also, the convention with Ruby is to use 2 spaces for indentation. You could set up vscode to auto format your code using rubocop on file save. I use rubocop-daemon to make it fast (otherwise it takes several seconds every time you save).

It’s more of a philosophical preference, I guess. However, it is much preferable working with UoM vs plain number for the flexibility in dealing with different units, especially kW vs W, kWh vs Wh, etc.

As I started with openhab years ago, I had often problems in the RulesDSL with UoM. So I decided to implement with blank values.
But maybe it was my unexpirience with this.
I’m coming from C and C#…
I think I will give it a try once again…

Does it automatically handle for example kW and W in a calculation? No need to divide the kW value by 1000?

I will take a look for a test case next week, I was able to reproduce with the value from the influx db.

And I will also take a look for automatic formatting.

Thankful for your advises.

The whole idea of QuantityType is that each variable contains its unit, any operation involving QuantityType takes the units into consideration and performs automatic unit conversion as necessary for you.

This means you can have variable_a of 10 W + variable_b of 1 kW and when you add them together you’ll correctly get 1.01 kW or 1010 W, and not 11. Equally, when working with Temperatures for example, you can add and compare Celsius and Fahrenheit without having to first manually perform the unit conversion yourself. It is really very convenient to use.

Take a look at this documentation for the upcoming version 5.x (not yet released as of March 4th 2023) of the jruby library:
https://openhab.github.io/openhab-jruby/main/OpenHAB/Core/Types/QuantityType.html

It may seem complicated, but in general, it will do the right thing for you.

Feel free to post any questions if you have any. Happy to try to help if I can.

What am I doing wrong?

when OPEN   then light.on for: 10.minutes

Ends in an Error:

11:30:06.981 [ERROR] [yobj.OpenHAB.DSL.Rules.AutomationRule] - wrong number of arguments (given 1, expected 0) (ArgumentError)        
In rule: Lighting generic
uri:classloader:/META-INF/jruby.home/lib/ruby/stdlib/delegate.rb:83:in `method_missing'
/etc/openhab/automation/jsr223/ruby/lights/motionSonsors.rb:12:in `block in <main>'

Here is my whole rule. If there is motion detected a member of groupMotionsensorPresences will switch to ON and turn the depending lights in the room ON

rule "Lighting generic" do
  changed groupMotionsensorPresences.members
  run do |event|
    next if GetRoomSymbols(event.item.name) != "AZ"
    light = items["groupLight_#{GetRoomSymbols(event.item.name)}_switches"]
    logger.info "Switched light in #{GetRoomSymbols(event.item.name)} to #{event.state}"
    case event.state
    when ON then light.on 
    when OFF then light.off for: 1.minutes
    end
  end
  not_if { |event| items["groupLight_#{GetRoomSymbols(event.item.name)}_switches"].nil? }
  #otherwise { |event| logger.error "No matching light found for closet door #{event.item.name}"  } 
end

# Example of presence items: Motionsensor_BZ_1_presence 
def GetRoomSymbols(itemName) 
  return itemName.gsub("Motionsensor_", "").gsub("_presence", "").gsub(/\d/, "").gsub("_", "")
end

@JimT

Sorry, I’m not able to reproduce the scenario with constant values of BigDecimals. It’s only getting the error above, if I calculate with the persistant value from the database.

What’s on line 12?

btw you could DRY your code a little and I’m not sure if your logic is correct in regards to the duration… I’d think you would want to have the timer on the ON command, not the off command? unless you wanted to turn off the light 1 minute AFTER the motion sensor changed to off? If that’s the case, you’d also want to cancel that timer when you receive another ON motion state before that timer expired, otherwise your light will “randomly” turn off.

If you could explain exactly what you wanted, I could post a sample code

The pattern is at line 12.

Its just copied from the example above.
goto

The only thing I really want to know, is: Why is the
“when XY then DO for: 10.minutes”-pattern not working as expected?
But, yes. I want the lights to turn OFF after 1 minute, if there is no presence anymore.

Thanks for the offer. But you don’t have to write an example especially for me.

Your code above isn’t the same though.

Could you copy paste your line 12?

Okay, made it simple:

rule 'Closet Door Lights' do
  changed TestSwitch
  run do |event|
     case event.state
     when ON   then groupLight_AZ_switches.on for: 1.minutes
     when OFF then groupLight_AZ_switches.off
     end
  end
end

Results in the following error, if TestSwitch changed to ON:

06:51:41.389 [ERROR] [yobj.OpenHAB.DSL.Rules.AutomationRule] - wrong number of arguments (given 1, expected 0) (ArgumentError)        
In rule: Closet Door Lights
uri:classloader:/META-INF/jruby.home/lib/ruby/stdlib/delegate.rb:83:in `method_missing'
/etc/openhab/automation/jsr223/ruby/test.rb:5:in `block in <main>'

Your syntax (GroupItem.on for: 1.minute) should work on the latest version of the library (5.0 - currently on RC version). It has been a while since I used version 4.x, so I’m not sure if that should work there.

However, this should work on 4.x:

groupLight_AZ_switches.all_members.each { |member| member.on for: 1.minute }

This is assuming that groupLight_AZ_switches is a GroupItem?

Thanks @JimT

This is working. The error occurs only if I use GroupItems with the mentioned timer-pattern. Switching GroupItems to ON/OFF is no problem.

Another question to timers. What could be the id of a timer?
Looks like there are only symbols allowed? I’m facing some troubles using a string as a timer id.
Maybe thats because a string-instance is not unique?

I could imagine to have 2 timers in a rule. Therefore I like to use something like:

timer_one = "#{event.item}_1" # trying hard to use snake_case ;-)
timer_two = "#{event.item}_2"
timers[timer_one] = after 30.seconds do
  # dimm lights
  timers[timer_two] = after 30.seconds do 
    # turn lights off
  end
end

I know, that I could reschedule a timer to only use one timer, but I have no better example…

timer ids can be anything: strings, symbols, array, object, hash, anything that exists in Ruby world can be used. Even a class can be used!

But the way you’re using timers[] is incorrect. You don’t assign to timers[] like that. It is a special object that acts like a hash so you can access timer ids.

You create a timer with an id using after like this:

after(30.seconds, id: "yourid") do 
  # do stuff here
end

timers["yourid"].cancel 

also timer_one = "#{event.item}_1" is wrong here. It should be timer_one = "#{event.item.name}_1"

There has been a big change in 5.0 in the interpretation of Item objects to avoid ambiguities like this.

Finally, here’s how I would rewrite your rule:

rule "Lighting generic" do
  changed groupMotionsensorPresences.members, to: ON
  run do |event|
    room_code = getRoomSymbols(event.item.name)
    next if room_code != "AZ"

    lights = items["groupLight_#{room_code}_switches"]
    next unless lights

    logger.info "Switched light in #{room_code} to #{event.state}"

    lights.all_members.ensure.on
    after(1.minute, id: room_code) { lights.all_members.ensure.off }
  end
end

# Example of presence items: Motionsensor_BZ_1_presence
def getRoomSymbols(itemName)
  itemName.gsub("Motionsensor_", "").gsub("_presence", "").gsub(/\d+/, "").gsub("_", "") # no need for "return" here
  # Alternative implementation (if the pattern allows)
  # itemName.split("_")[1]
end

Your lights will turn on and stay on as long as movements were detected, and only turn off once no movements were detected for 1 minute.

Please test it, I’m not 100% sure if the ensure syntax above would work as I don’t have this version installed. Let me know if you encountered any errors.

Could you explain the bigger picture here, so I can make a better suggestion? How does it fit in the overall rule, what triggers it etc. Do you want to make the lights brighter under a certain conditions, and dim them otherwise?

The reason I need to know is that you might want to cancel some of those timers when the other conditions were met, so your lights don’t dim or turn off by mistake because you hadn’t cancelled those timers.

Okay here is the big rule:

@allowed_rooms = ["AZ", "BZ", "F2"]

################################
 # Lighting with motions sensors
rule "Lighting generic" do
  changed groupMotionsensorPresences.members
  run do |event|
    
    room_code = getRoomCode(event.item.name)
    # Excape if room is not allowed to be switched
    next unless @allowed_rooms.include? room_code

    enabled = items["Motionsensors_#{room_code}_enable"] #A switch to turn off motion detection
    dimmer = items["groupLight_#{room_code}_dimmers"]
    light = items["groupLight_#{room_code}_switches"]
    light_current_value = items["groupMotionsensors_#{room_code}_light"] # including brightness values, no light is needed if its bright enough
    light_on_value = items["Motionsensors_#{room_code}_lightOnValue"]
    timer_duration = items["Motionsensors_#{room_code}_timerDuration"].to_i.seconds # NumberItem to set the individual timer duration
    
    case event.state
    when ON  
      if timers[room_code]
        timers[room_code]&.cancel
        dimmer.command(100)
        logger.info "LIGHTS-MOTION-CONTROL: Canceled timer after presence is detected"
      end
      if light.off? && enabled.on? && light_on_value >= light_current_value
        dimmer.command(100)
        logger.info "LIGHTS-MOTION-CONTROL: Switched light in #{room_code} to ON (dimmer=#{dimmer}), presence is detected."
      end
      logger.info "LIGHTS-MOTION-CONTROL: timer #{timer_duration}s, enabled #{enabled}, light_on_value #{light_on_value}, light_current_value #{light_current_value}"
    when OFF  
      if light.on? && enabled.on? 
        after(timer_duration, id: room_code) do # Use the triggering item as the timer ID
          if light.on? && enabled.on? && items["groupMotionsensors_#{room_code}_presence"].off? 
            if dimmer != 50
              dimmer.command(50)
              logger.info "LIGHTS-MOTION-CONTROL: Dimm light in #{room_code}, no presence anymore."
              timers[room_code]&.reschedule 10.seconds
            else
              light.off
              logger.info "LIGHTS-MOTION-CONTROL: Switched light in #{room_code} to OFF, no presence anymore."
              timers[room_code]&.cancel
            end
          end
        end
      end
    end
  end
end

 # Example of presence items: Motionsensor_BZ_1_presence 
def getRoomCode(item_name) 
  return item_name.split("_")[1]
end

The idea is to use the motion sensors for presence detection. If there is presence detected turn on the light in the depending room.
If the motion sensor updates his state for presence to OFF a timer should start to dimm the lights to a lower value. After this a second or a rescheduled timer should turn the lights completely OFF.
If In the time the timers are active a presence is redetected by the motion sensor the timer should be canceled.

If have up and running this with RulesDSL and for each single motion sensor/room in hunderds of code lines. So if I’m going to refactor, I will make it more generic.

Seems like it not going into this code, if presence is redetected:

when ON  
      if timers[room_code] #light.on? && enabled.on?
        timers[room_code]&.cancel
        dimmer.command(getDimmerValue(room_code))
        logger.info "LIGHTS-MOTION-CONTROL: Canceled timer after presence is detected"
      end

Thanks for the good explanation to timers in your last post. I have updated this rule with your good advices.

if I use:

timers[room_code] = after timer_duration do ...

instead of

after(timer_duration , id: room_code) do ...

The code to dimm up the lights after presence redetection is executed…

It’s confusing me…

Try this:

ALLOWED_ROOMS = %w[AZ BZ F2]

################################
# Lighting with motions sensors
rule "Lighting generic" do
  changed groupMotionsensorPresences.members, to: ON
  run do |event|
    room_code = get_room_code(event.item.name)
    # Escape if room is not allowed to be switched
    next unless ALLOWED_ROOMS.include? room_code

    enabled = items["Motionsensors_#{room_code}_enable"] # A switch to turn off motion detection
    next unless enabled.on?

    dimmer = items["groupLight_#{room_code}_dimmers"]
    light = items["groupLight_#{room_code}_switches"]
    light_current_value = items["groupMotionsensors_#{room_code}_light"]&.state # including brightness values, no light is needed if its bright enough
    light_on_value = items["Motionsensors_#{room_code}_lightOnValue"]&.state
    timer_duration = items["Motionsensors_#{room_code}_timerDuration"]&.state&.to_i&.seconds || 3.minutes # NumberItem to set the individual timer duration

    next unless light_on_value >= light_current_value

    dimmer.command(100)
    logger.info "LIGHTS-MOTION-CONTROL: Canceled timer after presence is detected"

    after(timer_duration, id: room_code) do |timer|
      next unless light.on?
      next unless enabled.on?

      if dimmer.state == 50
        light.off
        logger.info "LIGHTS-MOTION-CONTROL: Switched light in #{room_code} to OFF, no presence anymore."
      else
        dimmer.command(50)
        logger.info "LIGHTS-MOTION-CONTROL: Dimm light in #{room_code}, no presence anymore."
        timer.reschedule 10.seconds
      end
    end
  end
end

# Example of presence items: Motionsensor_BZ_1_presence
def get_room_code(item_name)
  item_name.split("_")[1]
end

Works and I recognize that I still have something to learn.

But it’s behavior is not like a charme.

the motion sensors sometimes updates the presence status very slow. In some cases it could be, that there will be no update for 60-90s.
If the rule now dimms down the light, because the timer ran out, there is no chance to brighten the lights up again. Thats the reason for me to start the timer with the presence status turning to OFF.
In this case the light dimms down and if you are in the room, u still have to move and the light will be dimm up again.

Does it make sense to install “JRuby Scripting with JRuby 9.4” provided by you? To have the newer library (5.0)? I still use the native JRuby-Addon.