How do I handle GenericBody

Ikea remotes send Scenes command like

<ZigbeeDevice: 0ef91548-6212-4ce1-913d-6398a4ea72dd [0x7BF4] (Styrbar)> received Zigbee message: < ZigbeeMessageRx || type: 0x00, < AddressHeader || src_addr: 0x7BF4, src_endpoint: 0x01, dest_addr: 0x0000, dest_endpoint: 0x01, profile: 0x0104, cluster: Scenes >, lqi: 0x90, rssi: -64, body_length: 0x0009, < ZCLMessageBody || < ZCLHeader || frame_ctrl: 0x05, mfg_code: 0x117C, seqno: 0x11, ZCLCommandId: 0x07 >, GenericBody: 01 01 0D 00 > >

is it possible to handle clusters with GenericBody or have a generic handler? @erickv

That command ID corresponds to the reporting configuration response of this cluster.
https://developer-preview.smartthings.com/edge-device-drivers/zigbee/zcl/zcl_commands.html#config_rep_response.st.zigbee.zcl.ConfigureReportingResponse

Have you added the Zigbee handlers for this cluster to see if you receive the scene commands from the device?
Eg. This is the handler defined for the IASZone cluster > ZoneStatusChangeNotification command

--This handler was defined to receive this Zigbee message in the driver first and define the functionality  based on the value of a preference

local function ias_zone_status_change_handler(driver, device, zb_rx)
    if (device.preferences.garageSensor ~= "Yes") then
        contact_sensor_defaults.ias_zone_status_change_handler(driver, device, zb_rx)
    end
end

local zigbee_multipurpose_driver_template = {
    supported_capabilities = {
        --...
    },
    zigbee_handlers = {
        global = {},
        cluster = {
            [zcl_clusters.IASZone.ID] = {
                [zcl_clusters.IASZone.client.commands.ZoneStatusChangeNotification.ID] = ias_zone_status_change_handler
            }
        },
        attr = {...},
        zdo = {}
    },
    ...
}

Recently I also wrote the driver for some Ikea remote controllers. You are probably referring to the 5 buttons version. Ikea uses commands in the cluster Scenes that I cannot find in the Zigbee Cluster Library documentation (0x07 to 0x09) so this is probably the reason why Edge API also does not support it. But there is no problem to handle such messages. You just need to use numbers instead of generated constants. Here is my zigbee_handlers part:

zigbee_handlers = {
  ...
  cluster = {
    ...
    [Scenes.ID] = {
      [0x07] = left_right_pushed_handler,
      [0x08] = left_right_held_handler,
      [0x09] = not_held_handler  -- does nothing actually :)
    }
  }
}

and my handler implementation:

local function left_right_pushed_handler(driver, device, zb_rx)
  log.debug("Handling left/right button pushed " .. zb_rx.body.zcl_body.body_bytes:byte(1))
  local button_number = zb_rx.body.zcl_body.body_bytes:byte(1) == 0 and 4 or 3
  device:emit_event_for_endpoint(button_number, capabilities.button.button.pushed({ state_change = true }))
  device:emit_event(capabilities.button.button.pushed({ state_change = true }))
end

Hi @hmorsti

Are your drivers on GitHub? I’m trying to write a driver for the new IKEA 4 button remote (Styrbar) and it would be a handy reference. I can see from my own investigations already it has an OnOff Cluster and a Scene Cluster

Thanks

Lrmulli

Hi @lmullineux
Unfortunately it’s not available on GitHub but I can share the code here. This is src/init.lua:

local capabilities = require "st.capabilities"
local ZigbeeDriver = require "st.zigbee"
local defaults = require "st.zigbee.defaults"
local log = require "log"
local utils = require "st.utils"

log.info("Here we are")

local zigbee_button_driver_template = {
  supported_capabilities = {
    capabilities.battery,
    capabilities.button,
  },
  sub_drivers = {
    require("tradfri-on-off-button"),
    require("tradfri-remote-control")
  },
}

defaults.register_for_default_handlers(zigbee_button_driver_template, zigbee_button_driver_template.supported_capabilities)

local driver = ZigbeeDriver("zigbee-button", zigbee_button_driver_template)
driver:run()

and this is src/tradfri-remote-control/init.lua:

local log = require "log"
local zcl_clusters = require "st.zigbee.zcl.clusters"
local OnOff = zcl_clusters.OnOff
local Level = zcl_clusters.Level
local Scenes = zcl_clusters.Scenes
local PowerConfiguration = zcl_clusters.PowerConfiguration
local capabilities = require "st.capabilities"
local constants = require "st.zigbee.constants"
local messages = require "st.zigbee.messages"
local mgmt_bind_req = require "st.zigbee.zdo.mgmt_bind_request"
local mgmt_bind_resp = require "st.zigbee.zdo.mgmt_bind_response"
local zdo_messages = require "st.zigbee.zdo"
local utils = require "st.utils"
local device_management = require "st.zigbee.device_management"
local battery_defaults = require "st.zigbee.defaults.battery_defaults"


local is_tradfri_remote_control = function(opts, driver, device)
  local is_tradfri_on_off = device:get_manufacturer() == "IKEA of Sweden" and device:get_model() == "TRADFRI remote control"
  log.debug("Is Tradfri Remote Control: " .. tostring(is_tradfri_on_off))
  return is_tradfri_on_off
end

function toggle_handler(driver, device, value, zb_rx)
  log.debug("Handling Tradfri TOGGLE")
  device:emit_event_for_endpoint(5, capabilities.button.button.pushed({ state_change = true }))
  device:emit_event(capabilities.button.button.pushed({ state_change = true }))
end

function pushed_up_handler(driver, device, value, zb_rx)
  log.debug("Handling Tradfri pushed UP")
  device:emit_event_for_endpoint(1, capabilities.button.button.pushed({ state_change = true }))
  device:emit_event(capabilities.button.button.pushed({ state_change = true }))
end

function held_up_handler(driver, device, value, zb_rx)
  log.debug("Handling Tradfri held UP")
  device:emit_event_for_endpoint(1, capabilities.button.button.held({ state_change = true }))
  device:emit_event(capabilities.button.button.held({ state_change = true }))
end

function pushed_down_handler(driver, device, value, zb_rx)
  log.debug("Handling Tradfri pushed DOWN")
  device:emit_event_for_endpoint(2, capabilities.button.button.pushed({ state_change = true }))
  device:emit_event(capabilities.button.button.pushed({ state_change = true }))
end

function held_down_handler(driver, device, value, zb_rx)
  log.debug("Handling Tradfri held DOWN")
  device:emit_event_for_endpoint(2, capabilities.button.button.held({ state_change = true }))
  device:emit_event(capabilities.button.button.held({ state_change = true }))
end

local function left_right_pushed_handler(driver, device, zb_rx)
  log.debug("Handling Tradfri left/right button PUSHED, value: " .. zb_rx.body.zcl_body.body_bytes:byte(1))
  local button_number = zb_rx.body.zcl_body.body_bytes:byte(1) == 0 and 4 or 3
  device:emit_event_for_endpoint(button_number, capabilities.button.button.pushed({ state_change = true }))
  device:emit_event(capabilities.button.button.pushed({ state_change = true }))
end

local function left_right_held_handler(driver, device, zb_rx)
  log.debug("Handling Tradfri left/right button HELD, value: " .. zb_rx.body.zcl_body.body_bytes:byte(1))
  local button_number = zb_rx.body.zcl_body.body_bytes:byte(1) == 1 and 3 or 4
  device:emit_event_for_endpoint(button_number, capabilities.button.button.held({ state_change = true }))
  device:emit_event(capabilities.button.button.held({ state_change = true }))
end

function not_held_handler(driver, device, value, zb_rx)
  log.debug("Handling Tradfri not held. Nothing to do.")
end

local function component_to_endpoint(device, component_id)
  local ep_num = component_id:match("button(%d)")
  return { ep_num and tonumber(ep_num) } or {}
end

local function endpoint_to_component(device, ep)
  local button_comp = string.format("button%d", ep)
  if device.profile.components[button_comp] ~= nil then
    return button_comp
  else
    return "main"
  end
end

local function device_configure(driver, device, event, args)
  log.debug("Configuring device")
  local addr_header = messages.AddressHeader(
          constants.HUB.ADDR,
          constants.HUB.ENDPOINT,
          device:get_short_address(),
          device.fingerprinted_endpoint_id,
          constants.ZDO_PROFILE_ID,
          mgmt_bind_req.BINDING_TABLE_REQUEST_CLUSTER_ID
  )
  local binding_table_req = mgmt_bind_req.MgmtBindRequest(0)
  local message_body = zdo_messages.ZdoMessageBody({
    zdo_body = binding_table_req
  })
  local binding_table_cmd = messages.ZigbeeMessageTx({
    address_header = addr_header,
    body = message_body
  })
  device:send(binding_table_cmd)
  device_management.configure(driver, device)
end

local function device_added(driver, device)
  log.info("Device added handler")
  device:refresh()  -- for battery state. Needed?
end

local function device_init(driver, device, event, args)
  device:set_component_to_endpoint_fn(component_to_endpoint)
  device:set_endpoint_to_component_fn(endpoint_to_component)

  device:emit_event(capabilities.button.numberOfButtons(5))
  device:emit_event(capabilities.button.supportedButtonValues({'pushed', 'held'}))
  device:emit_event(capabilities.button.button.pushed())
  for i = 1, 5 do
    device:emit_event_for_endpoint(i, capabilities.button.numberOfButtons(1))
    device:emit_event_for_endpoint(i, capabilities.button.supportedButtonValues(i ~= 5 and {'pushed', 'held'} or {'pushed'}))
    device:emit_event_for_endpoint(i, capabilities.button.button.pushed())
  end
end

local function battery_perc_attr_handler(driver, device, value, zb_rx)
  local perc = value.value  -- Some Tradfri devices present value in percentage without dividing by 2, is it correct for Remote Control?
  device:emit_event(capabilities.battery.battery(math.min(perc, 100)))
end

local function zdo_binding_table_handler(driver, device, zb_rx)
  log.debug("Received ZDO binding table message")
  for _, binding_table in pairs(zb_rx.body.zdo_body.binding_table_entries) do
    if binding_table.dest_addr_mode.value == binding_table.DEST_ADDR_MODE_SHORT then
      local group = binding_table.dest_addr.value
      log.debug("Adding hub to group: " .. tostring(group))
      driver:add_hub_to_zigbee_group(group)
    end
  end
end

local tradfri_remote_control = {
  NAME = "IKEA Tradfri Remote Control",
  lifecycle_handlers = {
    init = device_init,
    added = device_added,
    doConfigure = device_configure
  },
  zigbee_handlers = {
    attr = {
      [PowerConfiguration.ID] = {
        [PowerConfiguration.attributes.BatteryPercentageRemaining.ID] = battery_perc_attr_handler
      }
    },
    cluster = {
      [OnOff.ID] = {
        [OnOff.commands.Toggle.ID] = toggle_handler
      },
      [Level.ID] = {
        [Level.commands.StepWithOnOff.ID] = pushed_up_handler,
        [Level.commands.MoveWithOnOff.ID] = held_up_handler,
        [Level.commands.StopWithOnOff.ID] = not_held_handler,

        [Level.commands.Step.ID] = pushed_down_handler,
        [Level.commands.Move.ID] = held_down_handler,
        [Level.commands.Stop.ID] = not_held_handler
      },
      [Scenes.ID] = {
        [0x07] = left_right_pushed_handler,
        [0x08] = left_right_held_handler,
        [0x09] = not_held_handler
      }
    },
    zdo = {
      [mgmt_bind_resp.MGMT_BIND_RESPONSE] = zdo_binding_table_handler
    }
  },
  can_handle = is_tradfri_remote_control
}

return tradfri_remote_control

Sorry for the code quality and many logs but they were very helpful during development.

Thanks so much for this, it’s helping me figure all this out and i feel like i’m getting somewhere now.

Is there a reason why each button press is followed by a subsequent button press?

  device:emit_event_for_endpoint(button_number, capabilities.button.button.pushed({ state_change = true }))
  device:emit_event(capabilities.button.button.pushed({ state_change = true }))

Thanks

The device presentation for this device has multiple components - a separate component for each individual button and a main component for the overall device.

The first event goes to the specified component, which is whichever component is mapped to button_number:

device:emit_event_for_endpoint(button_number, capabilities.button.button.pushed({ state_change = true }))

The second event has no component specified so it goes to the main component:

device:emit_event(capabilities.button.button.pushed({ state_change = true }))

As a result, you get a button push event on each button component when that button is pushed, and a button push event on the main component when any button is pushed.

ah - that makes sense then

I took them all out, I’ll go and put them back in.

Presumably it is there to facilitate automating something on any button pressed?

Yes. If you remove them then you should also remove the button capability from the main component, and just use the main component for the device attributes like battery that are common across all the other components.

Thanks for the help @hmorsti & @philh30 I have a working driver - I’ve published the details here Ikea Styrbar Remote

@lmullineux good to hear that 5 button code can be easily ported to 4 button :slight_smile:

Yes @hmorsti, it didn’t need many changes. Thanks a lot