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: 20

Why 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 choose block. 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_on accepts color_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 to event (not command). In Zigbee2MQTT → your device → ExposesOperation mode, choose event so the knob sends click/rotate events. In command mode 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 (replace cbf7dfabcf8e9f15a987456920c2a4d with yours). 🔧

Trigger mapping (MOES knob → HA actions)

  • Single presstogglelight_toggle (on/off)
  • Rotate rightbrightness_step_upstep_up (brighter)
  • Rotate leftbrightness_step_downstep_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 255
  • bright_step_pct: 10 → 25–26 brightness points per step. For finer control set 5.
  • mired_step: 15 is a comfortable CT shift. For smoother transitions try 10.

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 avoid 0 (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 with 1,000,000 / K.
  • Use entity‑reported min_mireds/max_mireds when available; otherwise fall back to 153–500 (approx 6500K → 2000K).
  • temp_step_up makes the light cooler (lower mireds), temp_step_down makes it warmer (higher mireds). 🌤️

Mode & concurrency

mode: queued
max: 20
  • queued ensures 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_id and that actions appear in Developer Tools → Events or Z2M logs.
  • Jittery brightness: reduce bright_step_pct to 5 or 3.
  • CT doesn’t change: verify your light supports color_temp and check min_mireds/max_mireds in 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 adjust bright_step_pct at runtime from the dashboard.

Credits

  • Hardware: MOES Zigbee Smart Knob (AliExpress link above)
  • Platform: Home Assistant + MQTT + Matter light

Happy automating! 🏡💡