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