I’ve recently added the MOES Zigbee Smart Knob to my setup and wanted a smooth, click‑and‑rotate control for my kitchen light. The MOES knob is a compact Zigbee rotary controller with push, double‑tap, and rotation gestures. It pairs through your Zigbee gateway (e.g., Z2M/ZHA) and exposes actions over MQTT, making it perfect for toggle, brightness step, and color temperature adjustments. Product page: AliExpress • MOES Zigbee Smart Knob.
In this post, I’ll show the full Home Assistant automation that:
- 🟢 Toggles the kitchen light with a single press
- 🌗 Steps brightness up/down in precise percentages on rotate
- 🌡️ Steps color temperature (mireds) warmer/cooler
- 🧠 Handles edge cases (off → on, min/max boundaries) without flicker
The Automation
alias: Kitchen light setting
description: ""
triggers:
- id: light_toggle
domain: mqtt
device_id: cbf7dfabcf8e9f15a987456920c2a4d
type: action
subtype: toggle
trigger: device
- id: step_down
domain: mqtt
device_id: cbf7dfabcf8e9f15a987456920c2a4d
type: action
subtype: brightness_step_down
trigger: device
- id: step_up
domain: mqtt
device_id: cbf7dfabcf8e9f15a987456920c2a4d
type: action
subtype: brightness_step_up
trigger: device
- id: temp_step_down
domain: mqtt
device_id: cbf7dfabcf8e9f15a987456920c2a4d
type: action
subtype: color_temperature_step_down
trigger: device
- id: temp_step_up
domain: mqtt
device_id: cbf7dfabcf8e9f15a987456920c2a4d
type: action
subtype: color_temperature_step_up
trigger: device
conditions: []
actions:
- choose:
- conditions:
- condition: template
value_template: "{{ trigger.id == 'light_toggle' }}"
sequence:
- target:
entity_id: "{{ target_light }}"
action: light.toggle
- conditions:
- condition: template
value_template: "{{ trigger.id == 'step_down' }}"
sequence:
- target:
entity_id: "{{ target_light }}"
data:
brightness: >
{% set step = (255 * (bright_step_pct|int) / 100) | round(0) | int %}
{% set curr = state_attr(target_light, 'brightness') | int(0) %}
{% set is_on = states(target_light) == 'on' %}
{% set base = curr if (is_on and curr>0) else 0 %}
{{ [base - step, 1] | max }}
action: light.turn_on
- conditions:
- condition: template
value_template: "{{ trigger.id == 'step_up' }}"
sequence:
- target:
entity_id: "{{ target_light }}"
data:
brightness: >
{% set step = (255 * (bright_step_pct|int) / 100) | round(0) | int %}
{% set curr = state_attr(target_light, 'brightness') | int(0) %}
{% set is_on = states(target_light) == 'on' %}
{% set base = curr if (is_on and curr>0) else 0 %}
{{ [base + step, 255] | min }}
action: light.turn_on
- conditions:
- condition: template
value_template: "{{ trigger.id == 'temp_step_down' }}"
sequence:
- target:
entity_id: "{{ target_light }}"
data:
color_temp: >
{% set current = state_attr(target_light, 'color_temp')
or (1000000 / (state_attr(target_light, 'color_temp_kelvin')|float(4000))) | round(0) | int %}
{% set min_m = (state_attr(target_light, 'min_mireds') | int(153)) %}
{% set max_m = (state_attr(target_light, 'max_mireds') | int(500)) %}
{% set step = mired_step | int(25) %}
{{ [current + step, max_m] | min }}
action: light.turn_on
- conditions:
- condition: template
value_template: "{{ trigger.id == 'temp_step_up' }}"
sequence:
- target:
entity_id: "{{ target_light }}"
data:
color_temp: >
{% set current = state_attr(target_light, 'color_temp')
or (1000000 / (state_attr(target_light, 'color_temp_kelvin')|float(4000))) | round(0) | int %}
{% set min_m = (state_attr(target_light, 'min_mireds') | int(153)) %}
{% set max_m = (state_attr(target_light, 'max_mireds') | int(500)) %}
{% set step = mired_step | int(25) %}
{{ [current - step, min_m] | max }}
action: light.turn_on
variables:
target_light: light.matter_kitchen_light
mired_step: 15
bright_step_pct: 10
mode: queued
max: 20Why this design works
- MQTT device triggers map the knob’s gestures to five clear IDs:
light_toggle,step_up,step_down,temp_step_up,temp_step_down. This keeps the action routing simple and readable. 🧭 - Single automation handles all gestures with a
chooseblock. This avoids duplicated state checks and keeps logic centralized. 🧩 - Stateless brightness math: brightness uses an absolute 0–255 scale, so stepping in percent (e.g., 10%) translates into predictable increments across bulbs. 📈
- Mireds for CT: color temperature uses mireds (not Kelvin) because HA’s
light.turn_onacceptscolor_temp. The template safely derives the current mireds even if the entity only reports Kelvin. 🌞➡️🌙 - Boundaries protected: both brightness and CT clamp to [min,max] so the light never errors at extremes. 🛡️
Prerequisites
Important for Zigbee2MQTT: set your dimmer’s Operation mode toevent(notcommand). In Zigbee2MQTT → your device → Exposes → Operation mode, choose event so the knob sends click/rotate events. Incommandmode it tries to control groups directly and Home Assistant won’t receive the MQTT device triggers used by this automation.
- A Zigbee gateway (Zigbee2MQTT or ZHA) that exposes knob actions as MQTT device triggers in Home Assistant.
- A dimmable, CT‑capable light (Matter/ Zigbee/ Wi‑Fi) registered in HA, here:
light.matter_kitchen_light. - Your knob’s
device_id(replacecbf7dfabcf8e9f15a987456920c2a4dwith yours). 🔧
Trigger mapping (MOES knob → HA actions)
- Single press →
toggle→light_toggle(on/off) - Rotate right →
brightness_step_up→step_up(brighter) - Rotate left →
brightness_step_down→step_down(dimmer) - Double‑tap / long‑press variants on some firmwares can be mapped similarly if exposed; this example focuses on the common five. 🎛️
Variables you can tune
variables:
target_light: light.matter_kitchen_light # Your kitchen light entity
mired_step: 15 # CT step size (smaller = finer)
bright_step_pct: 10 # Brightness step as percent of 255bright_step_pct:10→ 25–26 brightness points per step. For finer control set5.mired_step:15is a comfortable CT shift. For smoother transitions try10.
Brightness math, explained
{% set step = (255 * (bright_step_pct|int) / 100) | round(0) | int %}
{% set curr = state_attr(target_light, 'brightness') | int(0) %}
{% set is_on = states(target_light) == 'on' %}
{% set base = curr if (is_on and curr>0) else 0 %}- Convert a percentage into the 0–255 scale.
- Read the current brightness; if the light is off or missing a value, base is 0.
- Decrease branch clamps with
max([... , 1])to avoid0(some bulbs treat 0 as off). - Increase branch clamps with
min([... , 255])to prevent overflow.
Color temperature in mireds
{% set current = state_attr(target_light, 'color_temp')
or (1000000 / (state_attr(target_light, 'color_temp_kelvin')|float(4000))) | round(0) | int %}
{% set min_m = (state_attr(target_light, 'min_mireds') | int(153)) %}
{% set max_m = (state_attr(target_light, 'max_mireds') | int(500)) %}
{% set step = mired_step | int(25) %}- If
color_temp(mireds) isn’t present, convert Kelvin → mireds with1,000,000 / K. - Use entity‑reported
min_mireds/max_miredswhen available; otherwise fall back to 153–500 (approx 6500K → 2000K). temp_step_upmakes the light cooler (lower mireds),temp_step_downmakes it warmer (higher mireds). 🌤️
Mode & concurrency
mode: queued
max: 20queuedensures quick successive rotations don’t cancel each other. The queue caps at 20 to avoid backlog when you spin the knob like a DJ. 🎚️
Troubleshooting 🛠️
- Nothing happens: confirm the knob’s
device_idand that actions appear in Developer Tools → Events or Z2M logs. - Jittery brightness: reduce
bright_step_pctto 5 or 3. - CT doesn’t change: verify your light supports
color_tempand checkmin_mireds/max_miredsin the entity’s attributes. - Latency: ensure MQTT broker and Zigbee coordinator are on stable power and not CPU‑throttled.
Extend it ✨
- Map double‑tap to “max brightness” or to a scene.
- Add a hold gesture to toggle between warm/cool presets.
- Include a helper (
input_number) to adjustbright_step_pctat runtime from the dashboard.
Credits
- Hardware: MOES Zigbee Smart Knob (AliExpress link above)
- Platform: Home Assistant + MQTT + Matter light
Happy automating! 🏡💡