Driver:try_create_device returns number not an object

expected behavior

device = driver:try_create_device(metadata)
device:set_field(“token”, token)

actual behaviour

device_id = driver:try_create_device(metadata) – 1
device_list = driver:get_devices() – [ ]
device = device_list[device_list] – nil

oops, it is not possible to provide extra data from the discovery function

@veonua, Since this API relies on low-level resources, for consistency, you should be handling this process across device lifecycles.

In addition, I recommend you to try invoking resources through pcall / assert functions. In most cases, you’ll get a better reference of the error raised than directly invoking the function, e.g.:

local success, msg = pcall(driver.try_create_device, driver, device_table)

sorry, I do not understand what you are trying to tell

I have to save the device-specific piece of data in the permanent storage. for this, we have to call

UDP discovery + HTTP get for token => new device with token

device:set_field(“token”, token)

but it seems like try_create_device does not provide the device (and its storage).

local success, msg = pcall(driver.try_create_device, driver, device_table)

what will be in the msg? is the pcall imported by default? could you give a full example?

What I suggest is to handle this allocation procedure at the added lifecycle, since it will be called as a confirmation that the device was created successfully.


Yes, it is part of the global scope and it is used to catch errors in Lua applications (Lua5.3 Reference / Error Handling), e.g.:

local function sum(x, y)
  assert(x, 'missing param: x')
  assert(y, 'missing param: y')
  return x + y
end

local success, res = pcall(sum, 10)
-- This will trigger second assert
-- at sum and will return:
--     success: false
--     res: 'missing param: y'

To have a nice reference of how much of the Lua standard libraries are supported, I’d recommend you to check the Global Environment section of the Edge Reference.

there is the issue that discovery does not pass any object to the added lifecycle.

so I had to disonnect 2nd device to avoid allocation of 2nd device token to the 1st device.

also I didn’t find a better solution than passing the token via global variable, so the token will stay in memory much longer than needed

This seems unusual. Can you share with me a scheme of the discovery flow you’re trying to achieve, so I can replicate the issue is a similar environment?

it’s a classical race condition. The discovery flow is sending a broadcast UDP package and then HTTP GET to all the devices that responded.

driver: who is online (UDP)
device1: me
device2: me
driver to device1: what is your token (HTTP)
driver to device2: what is your token (HTTP)
device1: token1
driver: ok try_create_device (device1)
device2: token2
driver: ok try_create_device (device2)
driver: added_handler(device1), save a token to the persistent storage !!! oops the last token is token2
driver: added_handler(device2), save a token... ok

here is my code SmartThingsEdge-Xiaomi/nanoleaf at main · veonua/SmartThingsEdge-Xiaomi · GitHub

1 Like

@veonua I noticed that your code is influenced by the SSDP implementation in the lightbulb-lan-esp8266 sample, which is meant to discover only one device per scanning, or is that you forgot to commit the changes to actually follow the sequence you shared above?

Also, regarding the sequence you shared, I’d recommend you rely on the added_handler to request and allocate the token in the persistent storage, for example:

-- discovery flow for device1
driver: who is online (UDP)
device1: me

-- create it if metadata is
-- supported by driver
driver: ok try_create_device (device1)

-- added lifecycle handler invoked
driver: added_handler(device1), what is your token (HTTP to DNI)
device1: token1
driver: device:set_field(device.id) = token1
-- added lifecycle ends

which is meant to discover only one device per scanning, or is that you forgot to commit the changes to actually follow the sequence you shared above?

exactly, I just removed the “return” from the while loop, so it kept scanning. Got the total mess when the driver newer stopped to scan.

it’s naive to think that driver developers are doing much better. so

-- discovery flow for device1
driver: who is online (UDP)
device1: me

-- create it if metadata is
-- supported by driver
driver: ok try_create_device (device1)

-- added lifecycle handler invoked
driver: added_handler(device1), what is your token (HTTP to DNI)
device1: {failed/not ready to response}

so we ended up with a device in the ST UI without token and all future interactions with the device will fail. So it makes sense to add the device only when it is ready.

You know… this made me realize that the lightbulb-lan-esp8266 isn’t that different from what you’re trying to achieve, so, I believe that the inconsistency you facing is because the key you’re using to store the token isn’t consistent enough, so:

  • Are you using the UUID from the USN header to store it?
  • It is for sure unique across the devices (even if they’re the same model)

Nevertheless, I’ll test this out right away, because maybe there’s an issue with the persistent storage that we’re not catching.

I am not sure what is the UUID from ST header.
you can see the UDP response SmartThingsEdge-Xiaomi/discovery.lua at main · veonua/SmartThingsEdge-Xiaomi · GitHub

my problem is that the try_create function does not return any UUID, but returns 1

Oops!.. I meant USN header, here.

USN is unique, but I do not use it

local metadata = {
    type = config.DEVICE_TYPE,
    device_network_id = device['location'],
    label = device['nl-devicename'],
    profile = config.DEVICE_PROFILE,
    manufacturer = manufacturer,
    model = model,
    vendor_provided_label = device['nl-deviceid'],
  }

@veonua I’ve been tweaking a little bit the lightbulb-lan-esp8266 Sample to actually simulate the token request inside the fetch_device_info function, so, the token was included in the XML response (only for testing purposes) and the allocation proceeded there as well.

To allocate the token properly, I used the driver.datastorage as following:

driver: who is online        (UDP - SSDP MSEARCH)
device1: me                  (SSDP RESPONSE)

driver: what is your token   (HTTP - invoke fetch_device_info)
device1: XML response        (include token)
driver: allocate token:
  local key = device.location -- or any device-specific and unique value
  driver.datastorage[key] = token
  driver.datastorage:save()
driver: ok try_create_device (device1)
device2: me
... and so on

And, at the end the tokens were allocated this way:

{
  ... -- device cache and stuff
  device-0="dummy-token-abc-0",
  device-1="dummy-token-abc-1",
  device-2="dummy-token-abc-2",
  device-3="dummy-token-abc-3",
  device-4="dummy-token-abc-4",
  device-5="dummy-token-abc-5",
  device-6="dummy-token-abc-6",
  device-7="dummy-token-abc-7"
}

Check if the driver.datastorage API is a good path for your implementation

thank you for this, driver.datastorage looks like a proper place.

or any device-specific and unique value

I don’t know other values besides the location.

driver.datastorage:save()

does it require load after?

Nope, it is loaded as the driver initializes.

1 Like

This make sense for something like the token, but what about the IP address. We know the IP address at the time the device is discovered. How can it be persisted for later use by the device?

@blueyetisoftware

Set the IP Address as the device_network_id param when calling device:try_create_device(...).

This will allow you to access its IP anytime you get access to the device object.

This is what I have done for now since these devices have static IPs. However, I suspect the instance name may be better if IPs can change. If the IP gets reused for other devices, they will look like duplicates during the scan. I guess you would then advise an IP lookup in an init/refresh handler?