Skip to content

Add custom command support and Zigbee channel retention#80

Open
mwitkow wants to merge 3 commits intoluar123:masterfrom
mwitkow:master
Open

Add custom command support and Zigbee channel retention#80
mwitkow wants to merge 3 commits intoluar123:masterfrom
mwitkow:master

Conversation

@mwitkow
Copy link

@mwitkow mwitkow commented Jan 25, 2026

I'm building an esph-ome philips-multicolor light (addressable strip).

  • Add support for explicitly setting a Zigbee channel (for faster joining)
  • Add support for basic Zigbee light effects used for detection.

@luar123
Copy link
Owner

luar123 commented Jan 25, 2026

Thanks, looks good and clean.

Not sure if we should add a data type parameter. And I am not a big fan of the void pointer parameter but not sure if there is a better alternative.

Do you also use #26 ?

At the moment I am reworking the action callbacks, because they are not threadsave. I would like to merge that first and then look into this.

@luar123
Copy link
Owner

luar123 commented Jan 25, 2026

Could you provide an example yaml?

@mwitkow
Copy link
Author

mwitkow commented Jan 25, 2026

Wow, thanks for the pointers. Here's the example hue light i'm vibe coding:

substitutions:
  name: "hue-style-light"
  pin: "10"
  num_leds: "6"
  chipset: "ws2812"
  rgb_order: "GRB"
  is_rgbw: "false"

esphome:
  includes:
    - reset_utils.h

external_components:
  - source:
      type: local
      path: ../zigbee_esphome/components
    components: [zigbee]

esp32:
  board: esp32-h2-devkitm-1
  partitions: ../zigbee_esphome/partitions_zb.csv
  framework:
    type: esp-idf

# Enable logging
logger:

globals:
  - id: color_x
    type: float
    restore_value: yes
    initial_value: '0'
  - id: color_y
    type: float
    restore_value: yes
    initial_value: '0'

zigbee:
  id: "zb"
  router: true
  stack_size: 8192
  on_identify_effect:
    then:
      - script.execute:
          id: identify_effect_script
          target_light: !lambda "return id(light_1);"
          effect_id: !lambda "return effect_id;"
  on_custom_command:
    then:
      - lambda: |-
          // Cluster 0x0300 (Color Control), Command 0x44 (Color Loop Set)
          if (cluster_id == 0x0300 && command_id == 0x44) {
            // Very basic implementation: just toggle the loop effect
            // In a full implementation we would parse Update Flags, Action, Direction, Time, Start Hue
            id(light_1).make_call().set_effect("Oscillating Color Loop").perform();
          }
  endpoints:
    - num: 1
      device_type: COLOR_DIMMABLE_LIGHT
      clusters:
        - id: ON_OFF
          attributes:
            - attribute_id: 0
              type: bool
              on_value:
                then:
                  - light.control:
                      id: light_1
                      state: !lambda "return x;"
        - id: LEVEL_CONTROL
          attributes:
            - attribute_id: 0
              type: U8
              value: 255
              on_value:
                then:
                  - light.control:
                      id: light_1
                      brightness: !lambda "return ((float)x)/255;"
        - id: COLOR_CONTROL
          attributes:
            - attribute_id: 3 # CurrentX
              type: U16
              on_value:
                then:
                  - lambda: |-
                      id(color_x) = (float)x/65536.0f;
                      auto call = id(light_1).make_call();
                      call.set_red(zigbee::get_r_from_xy(id(color_x), id(color_y)));
                      call.set_green(zigbee::get_g_from_xy(id(color_x), id(color_y)));
                      call.set_blue(zigbee::get_b_from_xy(id(color_x), id(color_y)));
                      if (${is_rgbw}) {
                        call.set_white(0.0f);
                      }
                      call.perform();
            - attribute_id: 4 # CurrentY
              type: U16
              on_value:
                then:
                  - lambda: |-
                      id(color_y) = (float)x/65536.0f;
                      auto call = id(light_1).make_call();
                      call.set_red(zigbee::get_r_from_xy(id(color_x), id(color_y)));
                      call.set_green(zigbee::get_g_from_xy(id(color_x), id(color_y)));
                      call.set_blue(zigbee::get_b_from_xy(id(color_x), id(color_y)));
                      if (${is_rgbw}) {
                        call.set_white(0.0f);
                      }
                      call.perform();
            # Color Temperature (0x0007)
            - attribute_id: 7
              type: U16
              on_value:
                then:
                  - lambda: |-
                      auto call = id(light_1).make_call();
                      call.set_color_temperature((float)x);
                      call.perform();
            # Color Mode (0x0008) - 1 = XY, 2 = CT
            - attribute_id: 8
              type: 8BIT_ENUM
              value: 1
            # Color Capabilities (0x400A) - XY (8) + Color Temperature (16) = 24
            - attribute_id: 0x400A
              type: 16BITMAP
              value: 24

light:
  - platform: esp32_rmt_led_strip
    rgb_order: ${rgb_order}
    pin: ${pin}
    num_leds: ${num_leds}
    rmt_symbols: 48
    id: light_1
    chipset: ${chipset}
    is_rgbw: ${is_rgbw}
    restore_mode: RESTORE_DEFAULT_OFF
    effects:
      - pulse:
          name: "Blink"
          transition_length: 300ms
          update_interval: 300ms
      - pulse:
          name: "Breathe"
          transition_length: 1s
          update_interval: 1s
      - strobe:
          name: "Okay"
          colors:
            - state: true
              brightness: 100%
              red: 0%
              green: 100%
              blue: 0%
              duration: 500ms
            - state: false
              duration: 500ms
      - strobe:
          name: "Channel Change"
          colors:
            - state: true
              brightness: 100%
              red: 100%
              green: 50%
              blue: 0%
              duration: 150ms
            - state: false
              duration: 150ms
      - addressable_lambda:
          name: "Oscillating Color Loop"
          update_interval: 50ms
          lambda: |-
            static float hue = 0.0;
            static bool direction = true;
            float step = 0.01;
            if (direction) {
              hue += step;
              if (hue >= 1.0) {
                hue = 1.0;
                direction = false;
              }
            } else {
              hue -= step;
              if (hue <= 0.0) {
                hue = 0.0;
                direction = true;
              }
            }
            // Map 0..1 to 0..360 for hue
            float h = hue * 360.0;
            float r, g, b;
            hsv_to_rgb(int(h), 1.0, 1.0, r, g, b);
            auto color = Color(uint8_t(r * 255), uint8_t(g * 255), uint8_t(b * 255));
            for (int i = 0; i < it.size(); i++) {
              it[i] = color;
            }

  # Onboard LED (RGB) kept separate and OFF
  - platform: esp32_rmt_led_strip
    rgb_order: GRB
    pin: 8
    num_leds: 1
    rmt_symbols: 48
    id: onboard_led
    chipset: ws2812
    internal: true
    restore_mode: ALWAYS_OFF

# Contains 'script' (orchestration) of effects to standard zigbee commands.
script:
  - id: identify_effect_script
    mode: restart
    parameters:
      target_light: light::LightState*
      effect_id: int
    then:
      - if:
          condition:
            lambda: 'return effect_id == 0;'
          then: # Blink: single fast cycle
            - lambda: 'target_light->make_call().set_effect("Blink").perform();'
            - delay: 600ms
            - lambda: 'target_light->make_call().set_effect("none").perform();'
      - if:
          condition:
            lambda: 'return effect_id == 1;'
          then: # Breathe: smooth fading, 15s
            - lambda: 'target_light->make_call().set_effect("Breathe").perform();'
            - delay: 15s
            - lambda: 'target_light->make_call().set_effect("none").perform();'
      - if:
          condition:
            lambda: 'return effect_id == 2;'
          then: # Okay: Green flash
            - lambda: 'target_light->make_call().set_effect("Okay").perform();'
            - delay: 1s
            - lambda: 'target_light->make_call().set_effect("none").perform();'
      - if:
          condition:
            lambda: 'return effect_id == 0x0b;'
          then: # Channel Change: Rapid Orange flash
            - lambda: 'target_light->make_call().set_effect("Channel Change").perform();'
            - delay: 3s
            - lambda: 'target_light->make_call().set_effect("none").perform();'
      - if:
          condition:
            lambda: 'return effect_id == 0xff || effect_id == 0xfe;'
          then: # Stop / Finish
            - lambda: 'target_light->make_call().set_effect("none").perform();'

binary_sensor:
  - platform: gpio
    pin:
      number: 9
      mode:
        input: true
        pullup: true
      inverted: true
    id: button_1
    on_press:
      then:
        - zigbee.report: zb
    on_click:
      - min_length: 5s
        max_length: 10s
        then:
          - logger.log: "Resetting Zigbee stack..."
          - zigbee.reset: zb
      - min_length: 10s
        max_length: 1h
        then:
          - logger.log: "Performing Full Factory Reset (Erasing NVS)..."
          - lambda: |-
              full_factory_reset();

status_led:
  pin: 13

@mwitkow mwitkow changed the title Add identify and custom command support Add custom command support and Zigbee channel retention Jan 25, 2026
@luar123
Copy link
Owner

luar123 commented Feb 9, 2026

I have pushed my changes. Could you please rebase your work and use the new zigbee_event for handling the commands?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants