Creating unit tests for zigbee driver (device_lifecycle "init" or "added")

I would like to test this code:

In other words, I would like to make sure these commands (lines 74 and 76) were called at “init” device lifecycle.

So I created this test: (currently, commented out)

Without the comments, I receive this error:

TRACE || Zigbee Device: 00000000-1111-2222-3333-000000000001
Manufacturer: single_switch Model: single_switch
	[1]: Basic, OnOff
DEBUG || driver device thread event handled
TRACE || Received event with handler _resync
TRACE || Found DeviceLifecycleDispatcher handler in default-clusters
INFO  || <ZigbeeDevice: 00000000-1111-2222-3333-000000000001 [0x0001]> sending Zigbee message: < ZigbeeMessageTx || Uint16: 0x0000, < AddressHeader || src_addr: 0x0000, src_endpoint: 0x01, dest_addr: 0x0001, dest_endpoint: 0x01, profile: 0x0104, cluster: Basic >, < ZCLMessageBody || < ZCLHeader || frame_ctrl: 0x00, seqno: 0x00, ZCLCommandId: 0x00 >, < ReadAttribute || AttributeId: 0x0004, AttributeId: 0x0000, AttributeId: 0x0001, AttributeId: 0x0005, AttributeId: 0x0007, AttributeId: 0xFFFE > > >
Failed with message:
Zigbee message channel send was given unexpected message:
ZigbeeMessageTx:
        Uint16: 0x0000
        AddressHeader:
            src_addr: 0x0000
            src_endpoint: 0x01
            dest_addr: 0x0001
            dest_endpoint: 0x01
            profile: 0x0104
            cluster: Basic
        ZCLMessageBody:
            ZCLHeader:
                frame_ctrl: 0x00
                seqno: 0x00
                ZCLCommandId: 0x00
            ReadAttribute:
                AttributeId: 0x0004
                AttributeId: 0x0000
                AttributeId: 0x0001
                AttributeId: 0x0005
                AttributeId: 0x0007
                AttributeId: 0xFFFE

FAILED

I understood the error happened because utils.spell_magic_trick(device) was caught before “init” lifecycle ended.
The current implementation of test.register_message_test requires I inform a “receive” message first before 0 or more “send” messages.

So my question: How can I create a test to make sure these commands are being called at “init” lifecycle ?

Can you provide more info about this, please? what do you need to inform that was received?
Would it be for the first time the device is discovered? Because the init lifecycle is also called when we update the driver or it was restarted or some reason (power outage, Hub reboot, etc.)
I see that the function spell_magic_trick(device) sends a read attribute command for a few attributes.

I need to certify these messages were sent in the “init” device lifecycle:

{ -- device:send(...)
	channel = "zigbee",
	direction = "send",
	message = { mock_parent_device.id, zigbee_test_utils.build_attribute_read(mock_parent_device, zcl_clusters.Basic.ID, { 0x0004, 0x0000, 0x0001, 0x0005, 0x0007, 0xFFFE }):to_endpoint(0x01) },
}, -- message above is sent by utils.spell_magic_trick(device)
{ -- device:emit_event(...)
	channel = "capability",
	direction = "send",
	message = mock_parent_device:generate_test_message("main", capabilities["valleyboard16460.info"].value("...")),
}

But the current implementation of test.register_message_test requires that I inform a message received before the messages sent.

So I thought I would need to add this as the first message:

{
	channel = "device_lifecycle",
	direction = "receive",
	message = { mock_parent_device.id, "init" },
}

But when I do this, I receive an error message saying zigbee channel sent an unexpected message.
The error makes sense to me because “init” hasn’t finished yet and a zigbee message was sent.
So I thought I should add the message received mentioned above as the last message, but then I would need another message received before the “init” device lifecycle because of test.register_message_test restriction.

These messages should be sent in any of these situations you mentioned.

Currently, I had to use a delayed call:

Does it mean I shouldn’t call device:send(...) or device:emit_event(...) during “init” device lifecycle ?

Or calling them with driver:call_with_delay(...) are ok and should I use another way to register the test without using test.register_message_test ? For example, using test.register_coroutine_test.

Hi, @w35l3y

Sorry for the delay.
I asked the team and they mentioned it would be better if you changed the logic in your code.
The init lifecycle can be executed before important parts of the device instance are initialized, also, in this lifecycle is highly possible that the Zigbee EUID isn’t set yet.
Have you tried if it works best from another lifecycle like “added”?

The spell works in the added lifecycle, but I still wasn’t able to create a test case using test.register_message_test when an event is always sent.

Do you have any example of test using test.register_message_test with event being sent at init or added lifecycles ?

I am using init lifecycle to make sure it will be executed in case device switches the driver from another to mine.
I haven’t tested if added lifecycle is executed in that situation. I know for sure init is.

In the mean time, I am using driver:call_with_delay(...) to delay the event from being sent.

Have you tried using the driverSwitched lifecycle instead? I’ve seen others use it to re-configure the devices when they start using their driver.

https://developer.smartthings.com/docs/edge-device-drivers/driver.html#lifecycle-event-handlers

Sorry, I don’t have samples about that, but I think checking the other lifecycle would be best as it is set specifically to check if the device changed to this driver.
I made a test and when we change a device from another driver to the current one, first, the lifecycle of “added” is executed and then “driverSwitched”.

The init lifecycle is executed when there’s a version update or the Hub was restarted so, it can cause more confusion.

1 Like

Thanks! That is what I have so far with my tests:

Situations init added doConfigure infoChanged driverSwitched removed
Paired for the first time 1 2 3
Switched from another driver 1 2 4 (*) 3
Switched to another driver 1
Updated driver code 1
Modified settings 1
Removed device 1

(*) Only when the new driver is not the one that device were first paired ?
https://developer.smartthings.com/docs/edge-device-drivers/driver.html?highlight=driverswitched#lifecycle-event-handlers

I will update with the solution once I find a way to test init or added lifecycles without using driver:call_with_delay(...)

Solved!

I was able to remove the driver:call_with_delay(...) by calling the expected messages twice.

  1. In the test_init
  2. In the list of messages (test.register_message_test)

I don’t think there is a way to use register_message_test when one have timer.
I personally think tests using register_message_test are easier to implement/understand.

Original

    init = function (driver, device, ...)
      if device.network_type == device_lib.NETWORK_TYPE_ZIGBEE then
        device:set_find_child(utils.find_child_fn)

        driver:call_with_delay(0, function () -- I wanted to remove `driver:call_with_delay`
          utils.spell_magic_trick(device) -- keep it
          device:emit_event(capabilities["valleyboard16460.info"].value(tostring(utils.info(device)))) -- keep it
        end)
      end
    end,

Common (test)

local function magic_spell (device) -- example of zigbee message
  return { device.id, zigbee_test_utils.build_attribute_read(device, zcl_clusters.Basic.ID, { 0x0004, 0x0000, 0x0001, 0x0005, 0x0007, 0xFFFE }):to_endpoint(0x01) }
end

local function custom_info (device, value) -- example of capability message
  return device:generate_test_message("main", capabilities["valleyboard16460.info"].value(value)
end

Before (test)

test.register_coroutine_test("device_lifecycle added", function ()
  test.timer.__create_and_queue_test_time_advance_timer(0, "oneshot") -- attempt to remove timer
  test.socket.zigbee:__expect_send(magic_spell(mock_parent_device)) -- spell
  test.socket.capability:__expect_send(custom_info(mock_parent_device, "..."))) -- info

  test.socket.device_lifecycle:__queue_receive({ mock_parent_device.id, "init" })
end, {
  test_init = function()
    test.mock_device.add_test_device(mock_parent_device)
  end
})

After (test)

test.register_message_test("device_lifecycle added", {
  {
    channel = "device_lifecycle",
    direction = "receive", -- currently, must start with received message
    message = { mock_parent_device.id, "added" },
  },
  {
    channel = "zigbee",
    direction = "send",
    message = magic_spell(mock_parent_device), -- spell (1st call)
  },
  {
    channel = "capability",
    direction = "send",
    message = custom_info(mock_parent_device, "...")), -- info (1st call)
  },
  {
    channel = "device_lifecycle",
    direction = "receive",
    message = { mock_parent_device.id, "init" },
  },
}, {
  test_init = function ()
    -- it was the ace in the hole I was looking for
    test.socket.zigbee:__expect_send(magic_spell(mock_parent_device)) -- spell (2nd call)
    test.socket.capability:__expect_send(custom_info(mock_parent_device, "..."))) -- info (2nd call)

    test.mock_device.add_test_device(mock_parent_device)
  end
})

So, if someone need to send messages in the init or added lifecycle without using timer, one will be able to test the code by informing them in the test_init.