Motion sensor and lighting with JRuby in a generic rule

Hey @JimT ,

I just need your help once again.

Relating to your code example to the motion sensor lighting handling.

Sometimes it seems the rule is executed twice and I dont know why. Is there an error in the jruby layer or is the timer instantiated twice?
For your information. I have two motion sensors in this room…

2023-03-10 08:24:32.563 [INFO ] [rs.lighting_-_motion_sensor_lighting] - LIGHTS-MOTION-CONTROL: Dimm light in BZ, no presence anymore.
2023-03-10 08:24:32.633 [INFO ] [rs.lighting_-_motion_sensor_lighting] - LIGHTS-MOTION-CONTROL: Switched light in BZ to OFF, no presence anymore.

But it’s also happening in rooms with only one motion sensor

2023-03-10 07:41:50.260 [INFO ] [rs.lighting_-_motion_sensor_lighting] - LIGHTS-MOTION-CONTROL: Dimm light in F2, no presence anymore.
2023-03-10 07:41:50.264 [INFO ] [rs.lighting_-_motion_sensor_lighting] - LIGHTS-MOTION-CONTROL: Switched light in F2 to OFF, no presence anymore.

Maybe you have any idea…

Here is the code of the rule:

################################
 # Lighting with motion sensors
rule "Lighting - Motion Sensor Lighting" do
  changed groupMotionsensorPresences.members
  run do |event|
    room_code = homezone.getRoomCode(event.item.name)
    room_code_item_number = homezone.getRoomCodeAndItemNumber(event.item.name)
    # Escape if room is not allowed to be switched
    #next unless @allowed_rooms.include? room_code

    # Use "Motionsensors_" for the inside (BZ) and "Motionsensor_" for the outside (OS_1) sensors
    enabled = items["Motionsensor_#{room_code_item_number}_enable"] ? items["Motionsensor_#{room_code_item_number}_enable"] :items["Motionsensors_#{room_code}_enable"]
    dimmer = items["groupLight_#{room_code_item_number}_dimmers"] ? items["groupLight_#{room_code_item_number}_dimmers"] : items["groupLight_#{room_code}_dimmers"]
    light = items["groupLight_#{room_code_item_number}_switches"] ? items["groupLight_#{room_code_item_number}_switches"] : items["groupLight_#{room_code}_switches"]
    light_current_value = items["Motionsensor_#{room_code_item_number}_light"]&.state ? items["Motionsensor_#{room_code_item_number}_light"]&.state : items["groupMotionsensors_#{room_code}_light"]&.state
    light_on_value = items["Motionsensors_#{room_code}_lightOnValue"]&.state
    timer_duration = items["Motionsensors_#{room_code}_timerDuration"]&.state&.to_i&.seconds || 3.minutes 

    next if enabled.off? || light_current_value > light_on_value
    
    # light.off ? (logger.info "LIGHTS-MOTION-CONTROL: Switched light in #{room_code} to ON (dimmer=#{dimmer}), presence is detected.") : nil
    if dimmer != nil # For lights with dimmers (mainly used)
      dimmer.state != getDimmerValue(room_code) ? dimmer.command(getDimmerValue(room_code)) : nil
    else # For lights without dimmers
      light.on
    end

    if event.state.on? || timers[room_code] != nil
      timers[room_code]&.cancel # for library version 4.x
      # for library version 5.x:
      # timers.cancel(room_code)
      next
    end

    after(timer_duration, id: room_code) do |timer|
      presence = items["groupMotionsensors_#{room_code}_presence"] ? items["groupMotionsensors_#{room_code}_presence"]: items["Motionsensor_#{room_code_item_number}_presence"]
      next if light.off? || enabled.off? || presence.on?
      
      unless isRoomConditionFulfilled(room_code)
        logger.info "LIGHTS-MOTION-CONTROL: Roomcondition NOT fulfilled."
        next
      end

      if dimmer != nil # For lights with dimmers (mainly used)
        if dimmer.state == getReducedDimmerValue(room_code)
          light.off
          logger.info "LIGHTS-MOTION-CONTROL: Switched light in #{room_code} to OFF, no presence anymore."
        else 
          dimmer.command(getReducedDimmerValue(room_code))
          logger.info "LIGHTS-MOTION-CONTROL: Dimm light in #{room_code}, no presence anymore."
          timer.reschedule 30.seconds
        end
      else # For lights without dimmers
        light.off
        logger.info "LIGHTS-MOTION-CONTROL: Switched light in #{room_code} to OFF, no presence anymore."
      end
    end
  end
end

def getDimmerValue(room_symbols)
  if NightLightMode.on? && items["Motionsensors_#{room_symbols}_useNightlight"].on?
    return DefaultDimmerValue_NightLight.state.to_i
  else
    case items["Motionsensors_#{room_symbols}_lightScene"]
    when "relaxe"
      return DefaultDimmerValue_Relaxe.state.to_i
    when "normal"
      return DefaultDimmerValue_Normal.state.to_i 
    when "bright"
      return DefaultDimmerValue_Bright.state.to_i 
    end
  end
end

def getReducedDimmerValue(room_symbols)
  if NightLightMode.on? && items["Motionsensors_#{room_symbols}_useNightlight"].on?
    return DefaultDimmerValue_NightLight.state.to_i
  else
  case items["Motionsensors_#{room_symbols}_lightScene"]
    when "relaxe"
      return (DefaultDimmerValue_Relaxe.state/2).to_i
    when "normal"
      return (DefaultDimmerValue_Normal.state/2).to_i 
    when "bright"
      return (DefaultDimmerValue_Bright.state/2).to_i 
    end
  end
end

def isRoomConditionFulfilled(room_symbols) 
  case room_symbols
  when "F1"
    (groupLight_WZ_switches.state == OFF) ? true : false # Sync to living room lights, switch floor lights only OFF, if they are also OFF
  when "AZ"
    ((LaptopWork_NI_1_online.off? && Smartplug_Shelly_2_watt.state <= 55) || Motionsensor_AZ_1_light.state > 85) ? true : false
  else
    return true
  end
end

I extended the rule to also use the motion sensors from the outside (another name pattern) and Items of type dimmer and switch. Some of my lights dont have dimmer items.

and the contentof the required lib “homezone”

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

  def getRoomCodeAndItemNumber(item_name) 
    return item_name.split("_")[1].to_s + "_" + item_name.split("_")[2].to_s
  end
end

I still use 4.0 library

org.openhab.automation.jrubyscripting:gems=openhab-scripting=~>4.0

Kind regards!

I’m not quite sure why it didn’t work. I can only spot a syntax error - you need to separate : and the next operand of the trinary operator with a space. In Ruby :xxxx is a symbol.

This isn’t a “good” syntax:

def a
  def b
    # foo
  end
end

Unlike in Python, you shouldn’t nest methods in Ruby. Use a module for the outer part instead. Module names need to start with a capital letter.

I’ve rewritten your script below - see if you could do a diff to see all the changes

module Homezone
  # Example of presence items: Motionsensor_BZ_1_presence or Motionsensor_OS_1_presence
  def room_code(item_name)
    item_name.split("_")[1]
  end

  def room_code_item_number(item_name)
    item_name.split("_")
             .shift # remove the first element
             .first(2) # get just the first 2 elements
             .join("_") # rejoin them
  end
end

################################
# Lighting with motion sensors
rule "Lighting - Motion Sensor Lighting" do
  changed groupMotionsensorPresences.members
  run do |event|
    room_code = Homezone.room_code(event.item.name)
    room_code_item_number = Homezone.room_code_item_number(event.item.name)
    # Escape if room is not allowed to be switched
    # next unless @allowed_rooms.include? room_code

    # In Ruby, the `||` operator isn't a simple "boolean logic" operator that returns true or false
    # like in other languages. It would return the actual value of the winning operand. This
    # makes it very handy for situations like below:
    # - If the first item is nil (doesn't exist), return the second item

    # Use "Motionsensors_" for the inside (BZ) and "Motionsensor_" for the outside (OS_1) sensors
    enabled = items["Motionsensor_#{room_code_item_number}_enable"] || items["Motionsensors_#{room_code}_enable"]
    dimmer = items["groupLight_#{room_code_item_number}_dimmers"] || items["groupLight_#{room_code}_dimmers"]
    light = items["groupLight_#{room_code_item_number}_switches"] || items["groupLight_#{room_code}_switches"]
    light_current_value = items["Motionsensor_#{room_code_item_number}_light"]&.state || items["groupMotionsensors_#{room_code}_light"]&.state
    light_on_value = items["Motionsensors_#{room_code}_lightOnValue"]&.state
    timer_duration = items["Motionsensors_#{room_code}_timerDuration"]&.state&.to_i&.seconds || 3.minutes

    # Need to consider whether light_on_value and light_current_value could potentially be nil here?

    next if enabled.off? || light_current_value > light_on_value

    # light.off ? (logger.info "LIGHTS-MOTION-CONTROL: Switched light in #{room_code} to ON (dimmer=#{dimmer}), presence is detected.") : nil

    if dimmer ## `!= nil ` is redundant.
      # For lights with dimmers (mainly used)
      value = dimmer_value(room_code)
      dimmer.ensure << value
      logger.info("Setting #{dimmer.name} to #{value}")
    else # For lights without dimmers
      light.on
    end

    # If the dimmers also have `lights` (switches) associated with the same lights, and
    # when you set the dimmers, the switch items also turn on,
    # you could simplify the above code to two lines like this:
    ##########
    # dimmer.ensure << dimmer_value(room_code) if dimmer
    # light.ensure.on # `ensure` wouldn't execute the command if it's already on (from setting the dimmer above)
    ##########

    if event.state.on? # || --> timers[room_code] <-- it seems wrong here. You'd be cancelling your timer the second time you received an OFF state
      logger.info("cancelling timer for #{room_code}")
      timers[room_code]&.cancel # for library version 4.x
      # for library version 5.x:
      # timers.cancel(room_code)
      next
    end

    logger.info("(re)setting timer for #{room_code} for #{timer_duration}")

    after(timer_duration, id: room_code) do |timer|
      presence = items["groupMotionsensors_#{room_code}_presence"] || items["Motionsensor_#{room_code_item_number}_presence"]
      next if light.off? || enabled.off? || presence.on?

      unless room_condition_fulfilled?(room_code)
        logger.info "LIGHTS-MOTION-CONTROL: Roomcondition NOT fulfilled."
        next
      end

      if dimmer.nil? # For lights without dimmers
        light.off
        logger.info "LIGHTS-MOTION-CONTROL: Switched light in #{room_code} to OFF, no presence anymore."
        next
      end

      reduced_value = reduced_dimmer_value(room_code)

      if dimmer.state == reduced_value # For lights with dimmers (mainly used)
        light.off
        logger.info "LIGHTS-MOTION-CONTROL: Switched #{dimmer.name} in #{room_code} to OFF, no presence anymore."
      else
        dimmer.command(reduced_value)
        logger.info "LIGHTS-MOTION-CONTROL: Dimm #{dimmer.name} in #{room_code} to #{reduced_value}, no presence anymore."
        timer.reschedule 30.seconds
      end
    end
  end
end

# This method can simply return the "state" (which is of PercentType), no need to convert to integer
def dimmer_value(room_symbols)
  if NightLightMode.on? && items["Motionsensors_#{room_symbols}_useNightlight"].on?
    return DefaultDimmerValue_NightLight.state # the `return` here is necessary, so that we exit from the current method
  end

  # No `return`s are needed below. Ruby uses the "last statement" as the return value.
  case items["Motionsensors_#{room_symbols}_lightScene"].state # Remember to use .state - otherwise your code will break in 5.0
  when "relaxe"
    DefaultDimmerValue_Relaxe.state
  when "normal"
    DefaultDimmerValue_Normal.state
  when "bright"
    DefaultDimmerValue_Bright.state
  end
end

# DRY - Don't repeat yourself
def reduced_dimmer_value(room_symbols)
  value = dimmer_value(room_symbols)
  return value if value == DefaultDimmerValue_NightLight.state

  value / 2
end

def room_condition_fulfilled?(room_symbols)
  case room_symbols
  when "F1"
    groupLight_WZ_switches.off? # Sync to living room lights, switch floor lights only OFF, if they are also OFF
  when "AZ"
    (LaptopWork_NI_1_online.off? && Smartplug_Shelly_2_watt.state <= 55) || Motionsensor_AZ_1_light.state > 85
  else
    true
  end
end

I’ve added some extra logging - hopefully it’ll help us figure out what’s going on.

Oh, and please upgrade to version 5.0. It would make it easier for me to test things out for you. I’ll be happy to help with any upgrade issues. I’m guessing you haven’t got hundreds of rules to upgrade.

Impressiv! Thanks. Then there were some mistakes.

So many changes I made. I updated to lib 5.0. Here is how my config file looks like:

org.openhab.automation.jrubyscripting:openhab-jrubyscripting=>=5.0.0
# optional: uncomment the following line if you prefer not having to 
# insert require 'openhab' at the top of your scripts.
org.openhab.automation.jrubyscripting:require=openhab

I think I dont have tor write "require ‘openhab/dsl’ " at to top of my rule files?
If I do I receive an error

12:02:50.466 [ERROR] [ript.internal.ScriptEngineManagerImpl] - Error during evaluation of script 'file:/etc/openhab/automation/jsr223/ruby/lighting/motionSonsors.rb': Error during evaluation of Ruby in org/jruby/RubyKernel.java at line 1017: (LoadError) no such file to load -- openhab/dsl

I leave that out.

But I have a major problem with your code.

I placed

module Homezone
  # Example of presence items: Motionsensor_BZ_1_presence or Motionsensor_OS_1_presence
  def room_code(item_name)
    item_name.split("_")[1]
  end

  def room_code_item_number(item_name)
    item_name.split("_")
             .shift # remove the first element
             .first(2) # get just the first 2 elements
             .join("_") # rejoin them
  end
end

in the file …/automation/ruby/lib/Homezone.rb

and require it with:

require "Homezone"

But I receive

12:07:44.109 [ERROR] [yobj.OpenHAB.DSL.Rules.AutomationRule] - undefined method `room_code' for Homezone:Module (NoMethodError)
In rule: Lighting - Motion Sensor Lighting
/etc/openhab/automation/jsr223/ruby/lighting/motionSonsors.rb:9:in `block in <main>'

The config should be:

org.openhab.automation.jrubyscripting:gems=openhab-scripting=~>5.0.0
org.openhab.automation.jrubyscripting:require=openhab/dsl

Note the change from openhab-jrubyscripting to openhab-scripting

No it’s not necessary (although it’s ok if you did) to write require "openhab/dsl" at the top of your script.

Oops sorry. It should be:

module Homezone
  class << self # This makes the methods inside become "class methods" so you can call them using Homezone.methodname
    # Example of presence items: Motionsensor_BZ_1_presence or Motionsensor_OS_1_presence
    def room_code(item_name)
      item_name.split("_")[1]
    end

    def room_code_item_number(item_name)
      item_name.split("_")
               .shift # remove the first element
               .first(2) # get just the first 2 elements
               .join("_") # rejoin them
    end
  end
end

Also please use lower case for the file name, so homezone.rb - it’s just the convention that we usually stick to. Then change the require to require "homezone"

Okay. Now it works.

Next error I receive is from the Homezone module.

Looks like he won’t find “first” method. Is there anything to require?

12:24:29.560 [ERROR] [rubyscripting.rule.motionSonsors.rb:6] - undefined method `first' for "Motionsensor":String (NoMethodError)
/etc/openhab/automation/ruby/lib/homezone.rb:11:in `room_code_item_number'
/etc/openhab/automation/jsr223/ruby/lighting/motionSonsors.rb:10:in `block in <main>'

This was also my bad. Array#shift would return the shifted (removed) first element, which is a String, which doesn’t have #first.

Here’s a fixed version

module Homezone
  class << self
    # Example of presence items: Motionsensor_BZ_1_presence or Motionsensor_OS_1_presence
    def room_code(item_name)
      item_name.split("_")[1]
    end

    def room_code_item_number(item_name)
      item_name.split("_")
               .slice(1, 2) # get elements 1 and 2 only
               .join("_") # rejoin them
    end
  end
end

To check if you’re using the helper library version 5, create a new file and paste this in it while watching the log

logger.warn "JRuby Scripting Library version #{OpenHAB::DSL::VERSION}"
12:55:41.545 [WARN ] [omation.jrubyscripting.rule.test.rb:3] - JRuby Scripting Library version 5.0.0

Thank you for your detailed explanations. :+1:

Untill now it works and the log looks fine. But I will give it a try in the dark this evening.

Hopefully the bathroom lights won’t turn off again like yesterday evening, during my wife takes a shower…
Very hard to find an explanation. :laughing:

That happened to me in the early days too. So for my bathroom lights, I set the timeout to 10-15 minutes, and if the exhaust fan is running, don’t turn it off. Keep rescheduling the timer. My exhaust fan depends on the humidity sensor.

You can simulate the events - just send the (motion sensor) item ON/OFF via karaf console

Once you’re happy with your rules, you should look into using semantic model for your items. You then won’t need to use that item name splitting / parsing with underscores anymore.

Yes I tested it this way. But in my experience, sometimes when you actually use it, it behaves differently than expected/tested.

Yes, you are right, I should take a look at the semantic model. But for now I’d like to play with jruby to take a look what can be done with it. :slight_smile:

@JimT

Thanks for the solution. After the weekend I can tell you a succes. It’s working fine.

But now I’m using lib 5.0 I faced some trouble using ColorItems for my hue bulbs.

Until now I used the following command to change the light color in lib 4.x

Light_SZ_1_color << {hue: 37, saturation: 100, brightness: 20}

This now results in a warning:

08:45:19.008 [WARN ] [ernal.defaultscope.ScriptBusEventImpl] - Command '{:h=>37, :s=>100, :b=>20}' cannot be parsed for item 'Light_SZ_1_color (Type=ColorItem, State=37,85,0, Label=Light Color, Category=colorlight, Groups=[groupLight_SZ_1, groupLight_SZ_colors, groupLight_All_colors])'.

I red the change log and it seems there are some changes for the HSBType and QuantitiyType…

Try

Light_SZ_1_color << "37,100,20"

Or

Light_SZ_1_color << HSBType.from_hsb(37, 100, 20)

Changelog:

HSBType is no longer convertible and comparable against Strings, Hashes, and Arrays. Just construct an HSBType. Sending a HTML hex color as a string as a command is still supported.

:smiley:
It gets easier, I could have expected that…

Thanks!

Maybe I’m wrong, but compared to RulesDSL, JRuby works faster with the HUE bulbs. The bulbs reacts much faster, if the rule is fired with JRuby.