Using an IKEA Symfonisk rotary knob as a light dimmer

This button is quite inexpensive (AU$20)

I am using it as a smart light dimmer. It has a magnet so it sticks to the fridge, or to the corner of the wall.

Using the following code, implemented in JRuby, I can use this button as a light dimmer.

  • Rotate left: reduce brightness to a minimum of 1%, it won’t turn it off
  • Rotate right: increase brightness until 100%
  • The faster you turn, the quicker the rate of change
  • When it reached the max/min, it will blink the light to tell the user they can’t increase/decrease any further
  • Push once to toggle the light on/off - brightness set to the last value
  • double click to turn on and set to 2%
  • triple click to turn on and set to 100%

Note that I connect this button to openhab via Zigbee2mqtt.
The symfonisk dimmer “action” channel is linked to MasterBedRoom_Dimmer_Action item. The light bulb’s dimmer item is MasterBedRoom_Light_Dimmer

The code:

# frozen_string_literal: true

require 'openhab'

## Configuration area
DIMMERS = {
  MasterBedRoom_Dimmer_Action => MasterBedRoom_Light_Dimmer
}.freeze
## End config

module KnobAction
  RIGHT = 'rotate_right'
  LEFT = 'rotate_left'
  TOGGLE = 'play_pause'
  DOUBLE_CLICK = 'skip_forward'
  TRIPLE_CLICK = 'skip_backward'
end

Delta = Struct.new(:time_threshold, :value)

# Use a bigger delta value if the knob is rotated faster (less interval between updates)
# So the change is faster
DELTA_TABLE = [
  Delta.new(0.25, 10),
  Delta.new(0.7, 5),
  Delta.new(1, 2)
]

@last_update = Hash.new(Time.at(0))
@last_dimmer_value = Hash.new(100)
@dimmer_blinking = {}

def on_state(dimmer)
  @last_dimmer_value[dimmer]
end

def toggle(dimmer)
  dimmer << (dimmer.on? ? 0 : on_state(dimmer))
end

@last_blink_time = Hash.new(Time.at(0))

def debounce(dimmer, min_sec = 5)
  return true if Time.now - @last_blink_time[dimmer] < min_sec

  @last_blink_time[dimmer] = Time.now
  false
end

def clear_debounce(dimmer)
  @last_blink_time[dimmer] = Time.at(0)
end

def blink(dimmer)
  return if debounce(dimmer)

  @dimmer_blinking[dimmer] = true
  oldval = dimmer.to_f
  dimmer << 0
ensure
  after(150.ms) do
    dimmer << oldval
    @dimmer_blinking[dimmer] = false
  end
end

def update_dimmer(dimmer, value:, edge:)
  return blink(dimmer) if edge

  value = value.clamp(1, 100)
  # logger.error("update dimmer to #{value}")
  dimmer << value
end

def elapsed_time(key)
  now = Time.now
  delta = now - @last_update[key]
  @last_update[key] = now
  delta
end

def delta_value(key)
  delta_time = elapsed_time(key)
  DELTA_TABLE.select { |delta| delta_time < delta.time_threshold }.first&.value || 1
end

def process_rotation(knob, dimmer)
  delta = delta_value(knob)

  case knob
  when KnobAction::LEFT
    clear_debounce(dimmer) if dimmer == 100
    update_dimmer(dimmer, value: dimmer.to_f - delta, edge: dimmer <= 1)
  when KnobAction::RIGHT
    clear_debounce(dimmer) if dimmer <= 1
    update_dimmer(dimmer, value: dimmer.to_f + delta, edge: dimmer == 100)
  end
end

rule 'Rotary Knob Handler' do
  updated DIMMERS.keys
  not_if { |event| event.state.to_s.empty? }
  triggered do |knob|
    dimmer = DIMMERS[knob]
    # logger.warn("Rotary Knob received update: #{knob.name}: #{knob} => #{dimmer&.name}")
    next unless dimmer
    next if @dimmer_blinking[dimmer]

    case knob
    when KnobAction::LEFT, KnobAction::RIGHT then process_rotation(knob, dimmer)
    when KnobAction::TOGGLE then toggle(dimmer)
    when KnobAction::DOUBLE_CLICK then dimmer << 2
    when KnobAction::TRIPLE_CLICK then dimmer << 100
    end

    knob << ''
  end
end

rule 'Save dimmer state prior to turning off' do
  changed DIMMERS.values, to: 0
  only_if { |event| event.was&.on? }
  run do |event|
    @last_dimmer_value[event.item] = event.was
    logger.debug("Saving #{event.item.name} state: #{event.was}")
  end
end
4 Likes

Thanks for sharing, very nice implementation!
Sadly the device is not yet available over here (SEA), but will use your approach when it is.

Great, thanx. Just what I was looking for :slight_smile:

I also use Xiaomi aqara buttons and Tasmota buttons (as a wall switch) to control the dimmer. It’s handled differently:

  • Single click toggle
  • double click cycle the dimmer 1%, 25%, 50%, 75%, 100%
  • triple click go straight to 100%
  • Quad click toggle warm/cool
1 Like

Thanks! Might have a look, also got a few IKEA remotes flying around (un-used).
Main goal is anyways having full automation, hence dimming is controlled by my scenes but some flexibility of course is nice and a knob to turn is even nicer than a button alone :slight_smile:

“scene” is something I have never looked into. Can you give me an overview / high level description of how you implemented your scenes? It might give me some ideas :slight_smile:

The concept basically is that you have different “scenes” that will set items to different states, lights here is the easiest example:

Evening mode: turns on all lights and puts the dimmer to x%
Media mode: puts the dimmer to a low % when the TV is turned on
etc.

I got a scene item for each room so that I can control scenes differently (i.e. bedroom could be in sleep mode, while the livingroom is in media or guest mode).
At the moment I do control all of this manual with rules (i.e. when OH sees that my TV turns on in the evening, it will automatically dim all my lights to 20% and turn other lights off, and when the TV turns off it brings all lights back to normal, this works in combination with other things like brightness/lux of the overall room and “home” presence).

An interesting widget/rule combination/tool is this one here. Did not have time to implement it yet though.

Finally I got zigbee2mqtt running and showing “action” in my item. All good.

But after created your script rule nothing happens. What can I have missed?
JRuby is installed as addon and config items in script are changed.

When I run the rule with “Run Now” button I get following error.

2022-01-17 18:39:20.886 [ERROR] [internal.handler.ScriptActionHandler] - Script execution of rule with UID 'f88d4c6aba' failed: Error during evaluation of Ruby in org/jruby/RubyKernel.java at line 1017: (LoadError) no such file to load -- openhab```

@djoope, make sure to tell the addon to install the gem for you, as outlined here:

Also my rule is meant to be created as a text / file rule, not added through the GUI. You’d create a file e.g. rotary_dimmer.rb inside conf/automation/jsr223/ruby/personal/

Then you might want to uncomment the “logger” line, it should then show you the log in the karaf console.

Ok, I will try. Thanx.