Light control (on/off/brightness/color temp/color change) using Ikea Styrbar wireless remote

Okay, so now i get:

2024-03-07 15:24:48.714 [ERROR] [rubyscripting.rule.rodret_handler:34] - undefined method `ensure' for nil:NilClass (NoMethodError)
/openhab/conf/automation/ruby/rodret_handler.rb:45:in `block in <main>'
2024-03-07 15:24:50.354 [ERROR] [rubyscripting.rule.rodret_handler:34] - undefined method `ensure' for nil:NilClass (NoMethodError)
/openhab/conf/automation/ruby/rodret_handler.rb:44:in `block in <main>'
2024-03-07 15:24:51.530 [ERROR] [rubyscripting.rule.rodret_handler:34] - undefined method `on' for nil:NilClass (NoMethodError)
/openhab/conf/automation/ruby/rodret_handler.rb:24:in `adjust'
/openhab/conf/automation/ruby/rodret_handler.rb:46:in `block in <main>'
2024-03-07 15:24:51.864 [ERROR] [rubyscripting.rule.rodret_handler:34] - undefined method `[]' for #<OpenHAB::DSL::TimerManager:0x46305f80> (NoMethodError)

Please post your complete rule

# frozen_string_literal: true

module ButtonAction
  ON = 'on'
  OFF = 'off'

  BRIGHTNESS_UP = 'brightness_move_up'
  BRIGHTNESS_DOWN = 'brightness_move_down'
  BRIGHTNESS_STOP = 'brightness_stop'
end

def blink(dimmer)
  power = dimmer.points(Semantics::Power).first
  power.off
ensure
  after(350.ms) do
    power.on
  end
end

def adjust(dimmer, delta)
  return timers[dimmer]&.cancel if delta.zero?

  dimmer.points(Semantics::Power).first&.ensure.on

  after(0.ms, id: dimmer) do |timer|
    next blink(dimmer) if (dimmer == '100 %' && delta.positive?) || (dimmer <= '1 %' && delta.negative?)

    dimmer << (dimmer.to_i + delta).clamp(1, 100)
    timer.reschedule 100.ms
  end
end

rule 'Rodret Dimmer Handler' do
  updated rodretDimmers.members
  triggered do |action|
    light = action.equipment
    next unless light

    dimmer = light.points(Semantics::Light).first
    power = light.points(Semantics::Power).first

    case action.state
    when ButtonAction::ON then power.ensure.on
    when ButtonAction::OFF then power.ensure.off
    when ButtonAction::BRIGHTNESS_UP then adjust(dimmer, 5)
    when ButtonAction::BRIGHTNESS_DOWN then adjust(dimmer, -5)
    when ButtonAction::BRIGHTNESS_STOP then adjust(dimmer, 0)
    end
  end
end

This part:

    light = action.equipment
    next unless light

    dimmer = light.points(Semantics::Light).first
    power = light.points(Semantics::Power).first

This is because dimmer and/or power is nil.
The way the script is designed, your dimmer needs to be a semantic Point (which I believe it is), but the script is looking for a sibling point with Power property, and because it didn’t find it, power is nil.

So you might need to adjust the script or your semantic model.

So i require at least a power and a dimmer semantic point?

Or adjust the code to suit your item structure.

OK, added the power item, now i get

2024-03-08 08:26:24.608 [WARN ] [ore.internal.scheduler.SchedulerImpl] - Scheduled job '/openhab/conf/automation/ruby/rodret_handler.rb:26' failed and stopped
        at RUBY.adjust(/openhab/conf/automation/ruby/rodret_handler.rb:27) ~[?:?]
2024-03-08 08:26:25.002 [ERROR] [rubyscripting.rule.rodret_handler:34] - undefined method `[]' for #<OpenHAB::DSL::TimerManager:0x46305f80> (NoMethodError)
/openhab/conf/automation/ruby/rodret_handler.rb:22:in `adjust'
/openhab/conf/automation/ruby/rodret_handler.rb:48:in `block in <main>'

when I hold down a button.

This was also from v4 of the helper library.

The syntax for v5 is:
timers.cancel(dimmer) instead of timers[dimmer]&.cancel

Alright, we’re getting there :slightly_smiling_face:

2024-03-08 09:00:26.596 [WARN ] [ore.internal.scheduler.SchedulerImpl] - Scheduled job '/openhab/conf/automation/ruby/rodret_handler.rb:26' failed and stopped
        at RUBY.adjust(/openhab/conf/automation/ruby/rodret_handler.rb:27) ~[?:?]

What’s on line 27?

I think I know… it’s yet another v4 to v5 breaking change.

This is your code (v4)

  after(0.ms, id: dimmer) do |timer|
    next blink(dimmer) if (dimmer == '100 %' && delta.positive?) || (dimmer <= '1 %' && delta.negative?)

    dimmer << (dimmer.to_i + delta).clamp(1, 100)
    timer.reschedule 100.ms
  end

This is the conversion to v5:

  after(0.ms, id: dimmer) do |timer|
    next blink(dimmer) if (dimmer.state.to_i == 100 && delta.positive?) || (dimmer.state.to_i <= 1 && delta.negative?)

    dimmer << (dimmer.state.to_i + delta).clamp(1, 100)
    timer.reschedule 100.ms
  end

Explanation: In v4, you can treat an item as if it’s the state, so you can write if DimmerItem1 == "100 %"
In v5, you can’t do that anymore because it caused ambiguities, so instead, you’d have to specify that you’re referring to the state i.e. if DimmerItem.state.to_i == 100

Another change was the comparison was made stricter. You can’t just compare it with string, so you’d either have to convert the state to an integer and compare it against a number or, compare the state to a PercentType, e.g. if DimmerItem1.state == PercentType.new(100). I chose to use .to_i usually unless I’m dealing with QuantityType, in which case I’d compare the state against a QuantityType.

I think I have a few things mixed up from the original post and the updated for v5… I need to get that sorted…

So, my updated code looks like:

# frozen_string_literal: true

BUTTON_ACTIONS = {
  'on' => :on,
  'off' => :off,
  'brightness_move_up' => :brightness_up,
  'brightness_move_down' => :brightness_down,
  'brightness_stop' => :brightness_stop
}.freeze

def blink(dimmer)
  power = dimmer.points(Semantics::Level)
  power.off
ensure
  after(350.ms) { power.on }
end

def adjust(dimmer, delta)
  return timers.cancel(dimmer) if delta.zero?

  dimmer.points(Semantics::Light).first&.ensure&.on

  after(0.ms, id: dimmer) do |timer|
    current_level = if dimmer.is_a?(GroupItem) && dimmer.state.nil?
                      dimmer.members.map(&:state).map(&:to_f).sum / dimmer.members.size
                    else
                      dimmer.state&.to_f || 0
                    end

    next blink(dimmer) if (current_level >= 100 && delta.positive?) || (current_level <= 1 && delta.negative?)

    new_level = (current_level.to_i + delta).clamp(1, 100)
    dimmer << new_level
    timer.reschedule 100.ms
  end
end

rule 'Rodret Dimmer Handler' do
  updated rodretDimmers.members
  run do |event|
    light = event.item.equipment
    next unless light

    dimmer = light.points(Semantics::Light).first
    power = light.points(Semantics::Power).first

    action_name = BUTTON_ACTIONS[event.state.to_i]
    logger.info("Ikea Dimmer Handler - button pressed: #{action_name}")

    case action_name
    when :on then switch.ensure.on
    when :off then switch.ensure.off
    when :brightness_up then adjust(dimmer, 5)
    when :brightness_down then adjust(dimmer, -5)
    when :brightness_stop then adjust(dimmer, 0)
    end
  end
end

I get the log lines, but the ā€œbutton pressedā€ is empty (I guess there is a problem with either getting the action name or the lookup…):


2024-03-08 09:27:09.409 [INFO ] [rubyscripting.rule.rodret_handler:38] - Rodret Dimmer Handler - button pressed:

Change line 47 and 48 to

    action_name = BUTTON_ACTIONS[event.state]
    logger.info("Ikea Dimmer Handler - button pressed: #{action_name} (#{event.state})")

You’ve mixed my original example with the code from @jabra_the_hut. His button is different and sends different codes to the IKEA styrbar / zigbee2mqtt. On his it’s a numeric code instead of string names.

Okay, I can see the keypresses now in the log.
Unfortunately I think there is still something messed up between the lookup and the case-statement, since now it does nothing but log the keys:

BUTTON_ACTIONS = {
  'on' => :on,
  'off' => :off,
  'brightness_move_up' => :brightness_up,
  'brightness_move_down' => :brightness_down,
  'brightness_stop' => :brightness_stop
}.freeze

does not seem to trigger any case of

rule 'Rodret Dimmer Handler' do
  updated rodretDimmers.members
  run do |event|
    light = event.item.equipment
    next unless light

    dimmer = light.points(Semantics::Light).first
    power = light.points(Semantics::Power).first

    action_name = BUTTON_ACTIONS[event.state]
    logger.info("Rodret Dimmer Handler - button pressed: #{action_name} (#{event.state})")

    case action_name
    when :on then power.ensure.on
    when :off then power.ensure.off
    when :brightness_up then adjust(dimmer, 5)
    when :brightness_down then adjust(dimmer, -5)
    when :brightness_stop then adjust(dimmer, 0)
    end
  end
end

Btw the log output looks like it ā€œskipsā€ the ā€œaction_nameā€ variable and just outputs ā€œevent.stateā€:

Rodret Dimmer Handler - button pressed:  (brightness_move_up)

Change this:

    action_name = BUTTON_ACTIONS[event.state]
    logger.info("Rodret Dimmer Handler - button pressed: #{action_name} (#{event.state})")

    case action_name
    when :on then power.ensure.on
    when :off then power.ensure.off
    when :brightness_up then adjust(dimmer, 5)
    when :brightness_down then adjust(dimmer, -5)
    when :brightness_stop then adjust(dimmer, 0)
    end

to this:

    action_name = event.state
    logger.info("Rodret Dimmer Handler - button pressed: #{action_name}")

    case action_name
    when 'on' then power.ensure.on
    when 'off' then power.ensure.off
    when 'brightness_move_up' then adjust(dimmer, 5)
    when 'brightness_move_down' then adjust(dimmer, -5)
    when 'brightness_stop' then adjust(dimmer, 0)
    end

The problem with the first code is: event.state is a StringType, not a plain string, so the lookup into BUTTON_ACTION didn’t result in a match. In this case BUTTON_ACTION is not really that useful so just remove it and use the direct value.

This time, comparison within case/when between StringType and String would work, and BUTTON_ACTION is not needed

The original idea was to use constants for these reasons:

  • Shorter
  • If there’s a typo, it would result in an error that I could detect earlier on

Yep, that seems to do it. Now I’ll try to get it to work with my lights! Thanks for now! I’m sure I’ll come back :slight_smile:

The original code was this (and it’s still my preference)

module ButtonAction
  ON = 'on'
  OFF = 'off'

  BRIGHTNESS_UP = 'brightness_move_up'
  BRIGHTNESS_DOWN = 'brightness_move_down'
  BRIGHTNESS_STOP = 'brightness_stop'

  LEFT = 'arrow_left_click'
  LEFT_HOLD = 'arrow_left_hold'
  LEFT_RELEASE = 'arrow_left_release'

  RIGHT = 'arrow_right_click'
  RIGHT_HOLD = 'arrow_right_hold'
  RIGHT_RELEASE = 'arrow_right_release'
end

rule 'Ikea Dimmer Handler' do
  updated gDimmers.members
  triggered do |action|
    ...
    case action.state
    when ButtonAction::ON then # do something
    when ButtonAction::OFF then # do something
    when ButtonAction::BRIGHTNESS_UP then # do something
    when ButtonAction::BRIGHTNESS_DOWN then # do something
    when ButtonAction::BRIGHTNESS_STOP then # do something
    ...
    end
  end
end