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