SmartThings SDK Subscribing to Devices Dynamically

Hi all,

I am looking at the subscription endpoint for the SmartThings SDK.

On line 299 the subscribeToDevices method is declared which takes 5 inputs, the comments above the method describe it and the inputs (Have copied them below just for ease of reference)

/**
	 * Creates device event subscriptions for one or more devices specified in a SmartApp device configuration setting.
	 * This method is intended for use from SmartApps or API Access apps and must be called from a client configured
	 * with an installedAppId. Use the create() method if the client is not
	 * so configured.
	 * @param devices a SmartApp device configuration setting configured with one or more devices
	 * @param capability alphanumeric ID of the capability to subscribe to or '*' to subscribed to all capabilities of
	 * the devices
	 * @param attribute string defining what attribute(s) and attribute value(s) to subscribe to. Specifying an attribute
	 * name such as 'switch' subscribed to all values of the switch attribute. Specifying a name.value string such as
	 * 'switch.on' subscribed to only the on values of the switch. Specifying the wildcard '*' subscribes to all
	 * values of all attributes of the capability.
	 * @param subscriptionName the alphanumeric subscription name
	 * @param options map of options, stateChange only a modes. If not stateChangeOnly is not specified the default
	 * is true. If modes is not specified then events are sent for all modes.
	 */

My question is on the first input parameter devices which is described as:

@param devices a SmartApp device configuration setting configured with one or more devices

What would this input if I wanted to subscribe to a device but have not set it up in the SmartApp device configuration

I can get the devices listed out using the devices but now I want to be able to be able to subscribe to particular devices.

router.get('/callback', async function (req, res, next) {
    try{
        // Will verify the refresh and retrieve resfresh and 
        const ctx = await smartapp.handleOAuthCallback(req)
        
        // Remove any existing subscriptions and unsubscribe to device switch events
        // To see the endpoints look here https://github.com/SmartThingsCommunity/smartthings-core-sdk/tree/master/src/endpoint
        await ctx.api.subscriptions.unsubscribeAll()
   
        let devices = await (ctx.api.devices.list())
        console.log(devices)
}catch(error){
        console.error(error)
}

The output from console.log(devices) is an array of my devices. Taking just one of the elements from this array shown below, what information from it should I pass (if any) as the
parameter devices for the subscribeToDevices function?

{
    deviceId: 'f3e720c5-665f-4328-8e1f-618a771863d1',
    name: 'Qubino Energy Monitor',
    label: 'Meter1',
    manufacturerName: 'SmartThingsCommunity',
    presentationId: 'SmartThings-smartthings-Test_Meter',
    deviceManufacturerCode: '0159-0007-0052',
    locationId: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
    roomId: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
    deviceTypeId: '179c4492-1189-4629-8d88-c6577b0c221b',
    deviceTypeName: 'Test Meter',
    deviceNetworkType: 'ZWAVE',
    components: [ [Object] ],
    dth: {
      deviceTypeId: '179c4492-1189-4629-8d88-c6577b0c221b',
      deviceTypeName: 'Test Meter',
      deviceNetworkType: 'ZWAVE',
      completedSetup: true,
      networkSecurityLevel: 'ZWAVE_S2_AUTHENTICATED',
      hubId: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
      executingLocally: false
    },
    type: 'DTH',
    restrictionTier: 0
  }

EDIT
Just to clarify a bit more what I am trying to do. I can currently subscribe to an individual capability of the device. For example the code below subscribes to the voltageMeasurement of the device listed above.

await ctx.api.subscriptions.subscribeToCapability('voltageMeasurement', '*', 'voltageHandler');

Instead of subscribing to each capability individually it would be nice to have one subscription which subscribes to the entire device as it updates all its sensory capabilities at the same time.

The devices parameter for subscribeToDevices should have the same structure as the context.config.settingName property when using section.deviceSetting in the SmartApp page. If you want to subscribe to all the device capabilities, it is better to use a Component Subscription.
Hereā€™s an example:

let deviceInfo=[{
    "valueType":"DEVICE",
    "deviceConfig":{
        "deviceId":"xxxx-xxxx-xxxx",
        "componentId":"main"
    }
}]

//Create Component Subscription
ctx.api.subscriptions.subscribeToDevices(deviceInfo, '*', '*', 'deviceSubscription'),

Hi @nayelyz,

Thanks for the reply I will give it a go!

Hi @nayelyz,

Used that format and it works well, I did end up breaking it up by capability so there are individual callbacks for each capability to make it easier to handle.

Just another quick question, for getting the ctx object back at a later point in the server I see that you can use the withContext method if you are persisting the data in DynamoDB shown in the snippet below using the session storage. Snippet is taken from line 132 of this sample.

server.get('/viewData', async (req, res) => {
	const data = req.session.smartThings

	// Read the context from DynamoDB so that API calls can be made
	const ctx = await apiApp.withContext(data.installedAppId)

If you arenā€™t using Dynamo, what values do I need to initialize a context instance without using the withContext helper method?

Glad it works! Please, give me some time, Iā€™ll check more details to answer your question.

DynamoDB is used only for the contextStore.
You can use any kind of persistent storage you want and save the object with the needed information to pass it to the withContext function of the SDK.
If you had the values in an object called localStore, then you would have to send this:

const ctx = await apiApp.withContext({
    installedAppId: localStore.installedAppId,
    locationId: localStore.locationId,
    authToken: localStore.authToken,
    refreshToken: localStore.refreshToken})

Or, as in this example, you could save those values in a server session:

2 Likes

Hi @nayelyz,

Thanks for that!

Just another slight hitch in subscribing to devices. I have two electrical meters in a setup but it appears I cannot subscribe to there capabilities separately.

I have a function that gets the the meters information in an array meters I then iterate through this array calling the function createMeterSub shown below passing each meters information through the variable meter

async function createMeterSub(meter,ctx){ 
    let info = [{
                "valueType":"DEVICE",
                "deviceConfig":{
                    "deviceId":meter.deviceId,
                    "componentId":"main"
                }
                }]
    try{
        // Subscription to Voltage 
        await ctx.api.subscriptions.subscribeToDevices(info, 'voltageMeasurement', 'voltage', 'voltageHandler')

        // Subscription to Energy
        await ctx.api.subscriptions.subscribeToDevices(info, 'energyMeter', 'energy', 'energyHandler')

        // Subscription to Power
        await ctx.api.subscriptions.subscribeToDevices(info, 'powerMeter', 'power', 'powerHandler')
    }catch(error){
        console.error(error);
    }
}

The first run through of the function works fine and subscriptions are created however on the second run through I get an error returned from the api.

Error: Request failed with status code 409: {"requestId":"715AD2F8-D549-4EFB-8656-8E7A1736364A","error":{"code":"ConflictError","message":"conflicting-subscription-name","details":[]}}

I can see that the deviceID for the two requests are different however it doesnā€™t seem to be creating subscriptions at device level instead at capability level. So when I try to subscribe to the voltageMeasurement capability of the second meter it is saying that the subscription already exists.

I do delete all my subscriptions first before creating any new subscriptions using

 await ctx.api.subscriptions.delete()

An example is I have two devices.

  1. DeviceID_One - 2c13206c-db5c-4b70-b7be-4a2c60139ef4
  2. DeviceID_Two - 221eca3e-8d7b-4de5-a6fa-c3063bdae33b

The first loop goes through fine and creates subscriptions for voltageMeasurement, energyMeter and powerMeter.
On the second loop when sending data at:

config: {
    url: 'https://api.smartthings.com/installedapps/XXXXXXX/subscriptions',
    method: 'post',
    data: '{"sourceType":"DEVICE","device":{"deviceId":"221eca3e-8d7b-4de5-a6fa-c3063bdae33b","componentId":"main","capability":"voltageMeasurement","attribute":"voltage","stateChangeOnly":true,"subscriptionName":"voltageHandler_0","value":"*"}}',

I get the error:

Error: Request failed with status code 409: {"requestId":"715AD2F8-D549-4EFB-8656-8E7A1736364A","error":{"code":"ConflictError","message":"conflicting-subscription-name","details":[]}}

But printing out my subsriptions using

subs =  await ctx.api.subscriptions.list()
console.log(subs)

gives

    [
      {
        id: '34c2ab09-c269-4e6e-a745-b3beb887aabc',
        installedAppId: 'XXXXXXXXXXXXX',
        sourceType: 'DEVICE',
        device: {
          deviceId: '2c13206c-db5c-4b70-b7be-4a2c60139ef4',
          componentId: 'main',
          capability: 'energyMeter',
          attribute: 'energy',
          value: '*',
          stateChangeOnly: true,
          subscriptionName: 'energyHandler_0',
          modes: []
        }
      },
      {
        id: 'a26f710e-f72b-4326-8ca4-d9fa430926bb',
        installedAppId: 'XXXXXXXX',
        sourceType: 'DEVICE',
        device: {
          deviceId: '2c13206c-db5c-4b70-b7be-4a2c60139ef4',
          componentId: 'main',
          capability: 'powerMeter',
          attribute: 'power',
          value: '*',
          stateChangeOnly: true,
          subscriptionName: 'powerHandler_0',
          modes: []
        }
      },
      {
        id: '6a87d4c8-c192-4626-9b1f-ce8d716f949a',
        installedAppId: 'XXXXXXXXXXX',
        sourceType: 'DEVICE',
        device: {
          deviceId: '2c13206c-db5c-4b70-b7be-4a2c60139ef4',
          componentId: 'main',
          capability: 'voltageMeasurement',
          attribute: 'voltage',
          value: '*',
          stateChangeOnly: true,
          subscriptionName: 'voltageHandler_0',
          modes: []
        }
      }
    ]

All registered to the first device, no subscriptions to the second device.

Should I be changing the name of the subscription somewhere myself?

EDIT
I am reusing handlers too but I didint think that would be a problem. So I have 3 handlers associated with my SmartApp

      // Power event 
    .subscribedEventHandler('powerHandler', subscriptionService.powerHandler)

    // Voltage event 
    .subscribedEventHandler('voltageHandler', subscriptionService.voltageHandler)

    // Energy Event 
    .subscribedEventHandler('energyHandler', subscriptionService.energyHandler)

I am trying to reuse each of these for each meters voltageMeasurement, energyMeter and powerMeter, do I need to create a new handler for each subscription?

Hereā€™s the constraint of the Subscription name field. It states that it "Must be unique per installed app."
As there is already a ā€˜voltageHandlerā€™ subscription related to this installed App Id, then you should change the name, you could add a different index at the end for each device.

So would I need to do somethings like this

    // Power event 
    .subscribedEventHandler('powerHandler'+DeviceID, subscriptionService.powerHandler)

    // Voltage event 
    .subscribedEventHandler('voltageHandler'+DeviceID, subscriptionService.voltageHandler)

    // Energy Event 
    .subscribedEventHandler('energyHandler'+DeviceID, subscriptionService.energyHandler)

Creating a new handler for every subscription or is there a way to just get at the subscription name and reuse handlers?

EDIT
Trying this but seems it wants the name in certain format.

async function createMeterSub(meter,ctx){ 
    let info = [{
                "valueType":"DEVICE",
                "deviceConfig":{
                    "deviceId":meter.deviceId,
                    "componentId":"main"
                }
                }]
    try{
        let voltage_sub = `voltageHandler${meter.deviceId}`
        let energy_sub  = `energyHandler${meter.deviceId}`
        let power_sub   = `powerHandler${meter.deviceId}`
        // Subscription to Voltage 
        smartapp.subscribedEventHandler(voltage_sub, voltageHandler)
        await ctx.api.subscriptions.subscribeToDevices(info, 'voltageMeasurement', 'voltage',voltage_sub)

        // Subscription to Energy
        smartapp.subscribedEventHandler(energy_sub, energyHandler)
        await ctx.api.subscriptions.subscribeToDevices(info, 'energyMeter', 'energy', energy_sub)

        // Subscription to Power
        smartapp.subscribedEventHandler(power_sub, powerHandler)
        await ctx.api.subscriptions.subscribeToDevices(info, 'powerMeter', 'power' ,power_sub)
    }catch(error){
        console.error(error);
    }
}

The error

Error: Request failed with status code 422: {"requestId":"E78FFFF7-A6EF-4BA4-91FA-DDB22A3B0FDE","error":{"code":"ConstraintViolationError","message":"The request is malformed.","details":[{"code":"PatternError","target":"subscriptionName","message":"subscriptionName does not match the pattern ^[-_!.~'()*0-9a-zA-Z]{1,36}$.","details":[]}]}}

The request

data: '{"sourceType":"DEVICE","device":{"deviceId":"221eca3e-8d7b-4de5-a6fa-c3063bdae33b","componentId":"main","capability":"voltageMeasurement","attribute":"voltage","stateChangeOnly":true,"subscriptionName":"voltageHandler221eca3e-8d7b-4de5-a6fa-c3063bdae33b_0","value":"*"}}',

Fixed by using label, I think name was too long

Great! Thanks for the update.

Hi @nayelyz,

I am just trying to subscribe to the hub Healths using the subscribeToHubHealth method in the subscription endpoint. (Defined on line 460 here)

The response is returning is a http code 403 telling me I am not permitted to setup that subscription for the user. The permissions my app has are below, I taught it would have been covered in the read all and specific devices but obviously not.

  1. r:devices:$
  2. r:devices:*
  3. r:locations:*
  4. x:devices:*

What other permissions would I need to subscribe to the hub status?

EDIT
I have turned on all permissions but it is not updating in the application, still only seeing devices and locations. Do I have to do anything additionally when changing the permissions, I selected save in the Developer Workspace and got a confirmation Connector Saved!

Hey @Warren1

Youā€™re missing the r:hubs:* scope, and before including it at your SmartApp and run it, you might need to add it at your SmartAppā€™s configuration as well (Apps API).

Hi @erickv,

I donā€™t seem to have r:hubs:* as an option when selecting scopes from my SmartApp. When you say add it to my configuration what do you mean by this?

Below are all the scopes I can add to my SmartApp

He meant you to add it manually through the SmartThings API. What we do, is:

  1. Get the current scopes of the SmartApp.

  1. Copy the result, add the r:hubs:* scope, and save the new configuration.

You should see this permission on the Authorization page.

Note: Every time you make a change to the project in the Developer Workspace, URL, display name, etc. youā€™ll need to repeat those steps.

2 Likes

Hi Nayelyz,

Iā€™ma also trying to add the ā€˜r:hubs:*ā€™ and I have 2 questions.

  1. Iā€™m using the url Developer Workspace which is my organization workspace (not private). I donā€™t see any access to the URL that you are using for development work. All I see is create project/live logging and the setup with the deploy option. Which is all working fine. How do I get the URL for the developers that you are using in the example?

  2. In what phase can I get the oauth information and then add the 'r:hubs:* option as you do in the example above? is that in the CONFIG or INSTALL phase?

Thank you for any help,

Fred

Sorry never tagged @nayelyz

Hi! You need to use the Apps endpoint of the SmartThings API, instead of the Developer Workspace URL:

The app ID is under ā€œApp Credentialsā€ at the Developer Workspace

This must be done only once through the API not the SmartApp itself, after that, every instance will have the scope.

Hi @nayelyz,

Getting close now to using the HUB_HEALTH sourceType for setting the subscription. I have the r:hub:* coming back properly for oauth scope (thank you) and now getting a 403 (no permissions) on attempting the subscription. Iā€™m pretty sure its in the CONFIGURATION setup. Everything subscribes properly except the HUB HEALTH. Is their an example of permissioning the hub. Iā€™m not seeing it in any API documentation.

Thank you,

Fred

Glad to hear that. Yes, you might be missing the permissions([<list of scopes>]) property in the SmartApp Definition, otherwise, the r:hubs:* scope is whitelisted but not in use at the SmartApp.
Note: Remember to include appId() as well.

Hereā€™s an example:

@nayelyz - Apologize for not seeing this earlier. Hub is testing out well for ā€˜ONLINEā€™ and ā€˜OFFLINEā€™ status now. The HTTP request status for the EVENT lifecycle for hub is 400 while the normal contact and motion actuation sensor status is 200.

Do you think the 400 is normal hub health status or just a code overlook? Iā€™ll test the contact and motion sensor device ā€˜OFFLINEā€™ and ā€˜ONLINEā€™ and see if the 400 status comes with that. That should indicate it was planned. Iā€™ll let you know (using ecolink sensors and it can take up to 8 hours for offline to show up).