Issue Creating Device Subscriptions

I have an app that stores the installedAppId, and gets the list of all devices for my location. i authorize my app to read and excecute those devices, and the api returns the list of deviceObjects including their deviceIds, and sends me back to my app. my app, then, presents a window with all of the devices stylized and selectable. as the user selects them, the app builds the array of devices, and sends them to my smartapp web hook server /subscriptions endpoint. but I have not been able to successfully create the subscription at this point, since my app is not the smartthings app it won’t automatically initiate an update lifecycle and I dont know how to manually initiate it. but I want o be able to use the context.api.subscriptions.subscribeToDevices() call and subscribe to all capabilities for those deviceId’s. how do I accomplish this? erver.post(‘/setup-subcriptions’, async (req, res) => {
try {
const deviceIdsString = req.body.deviceIDs;
const installedAppId = req.header(‘installed-App-Id’);

    if (!deviceIdsString || !installedAppId) {
      print('Invalid request: Missing deviceIDs or installedAppId: ${deviceIds}')
        return res.status(400).send('Invalid request: Missing deviceIDs or installedAppId: ${deviceIds}');
    }
    
    const deviceIds = deviceIdsString.split(',').map(id => id.trim()); // Split and trim device IDs
    
    if (!Array.isArray(deviceIds) || deviceIds.length === 0) {
      console.log("DeviceIds: ${deviceIds}")
        return res.status(400).send('Invalid device IDs provided');
      
    }

    // Store the deviceIds for the installedAppId
    deviceIdCache[installedAppId] = deviceIds;
    
    // Create subscriptions for each device ID
    await Promise.all(deviceIds.map(deviceId => createSubscription(deviceId, installedAppId)));
    
    res.sendStatus(200); // Send success response
} catch (error) {
    console.error('Error setting up subscriptions:', error);
    res.sendStatus(500); // Send error response
}

});

I think I understand your issue but I believe @nayelyz will be able to do a better job of helping you as I don’t use the JavaScript/TypeScript SDKs, or indeed JavaScript/TypeScript, for apps.

The basic concept is that you keep your app ID, client ID and client secret in your context store, and then when the lifecycle events deliver you an authToken and refreshToken you add those too (I do it in the update lifecycle). That gives you everything you need to maintain your own authentication token independently of the lifecycle events. So you would make the subscription API calls in your subscription handler. I understand the SDKs take care of all of that for you but I don’t know exactly how they plumb together.

1 Like

Hi, @delray2!

As I understand correctly, you’re using a Webhook SmartApp that can be installed through the SmartThings App to send the info back and forth to SmartThings and your app, right?

Is this going to be a commercial product? Because SmartApps are not available for certification so they cannot be available in the SmartThings catalog.
The best option would be an OAuth integration but it has a default limit of 500 installs and each authorization to a location counts as one install.

Perhaps you’re missing the part of storing the context as @orangebucket mentioned because that allows you to access that object from every section of your code, not only in the lifecycles.
Then, you call withContext() to be able to make API calls like in this sample:

With the OAuth integration, you can request authorization tokens once the user accepts access to your app and don’t need to wait for any SmartApp lifecycle, you can create the subscriptions using the context you store, like in the sample mentioned above but in another section.:

Please, let us know if this info was helpful.

1 Like

I’m simply building off of the glitch subscriptions api example, Glitch :・゚✧, but I’ve added some routes and changed some to suit my needs. In the original, the user signs into smartthings and authorizes one of locations in their account, in turn authorizing access to all of the devices in that location.

server.get('/oauth/callback', async (req, res, next) => {
	try {
    console.log('OAUTH CALLBACK', req.query)
		// Store the SmartApp context including access and refresh tokens. Returns a context object for use in making
		// API calls to SmartThings
		const ctx = await apiApp.handleOAuthCallback(req)

		// Get the location name (for display on the web page)
		const location = await ctx.api.locations.get(ctx.locationId)

		// Set the cookie with the context, including the location ID and name
		req.session.smartThings = {
			locationId: ctx.locationId,
			locationName: location.name,
			installedAppId: ctx.installedAppId
		}

		**// Remove any existing subscriptions and unsubscribe to device switch events**
**		await ctx.api.subscriptions.delete()
**		await ctx.api.subscriptions.subscribeToCapability('switch', 'switch', 'switchHandler');**

		// Redirect back to the main page
		res.redirect('/')
	} catch (error) {
		next(error)
	}
})`

I’m not only interested in subscribing only to events of the ‘switch’ capability, I am interested in subscribing to all and any events that occur to specific devices,
the application generally flows like this: user opens app. selects “add devices” button which brings them to an onboarding page. from there they will click on a button for the platform from which they will add their devices to my app. this button is linked to send the user through the oauth process. I’ve created the smartapp, and its callback is my glitch app, which redirects to another route within my app “callbackToApp”:

server.get('/calbackToApp', async function(req, res) {
  const data = req.session.smartThings
  const installedAppId = data.installedAppId
  res.send(`<html>
      <head><title>Authentication Success</title></head>
      <body>
        <script>
          window.installedAppId = ${JSON.stringify(installedAppId)};
          window.postMessage('authSuccess');
        </script>
        Authentication Successful. Please return to the app.
      </body>
      </html>`)
    });

I know this is an uncommon way of doing things but it was the only way I could think of to get those credentials back to my application.( I know that the installedappid should be encrypted before sending and it is, I just simplified it for the purposes of getting to the point. )
from there, the user sends the installedAppId as an encrypted header which my server decrypts when it is received.
when my app gets the installed App Id, it calls to the “viewData” endpoint, which I have also modified to categorize devices in to different “types”, as was necessary for my application, and returns that devices array to my app, and my app presents a window to my user to select which devices they would like to choose. there is a done button. when they click it, the app calls to “setup-subscriptions”:

server.post('/setup-subscriptions', async (req, res) => {
    const installedAppId = req.header('installed-App-Id'); 
// const encryptedAppId = req.header('installed-App-Id'); 
const installedAppId =  decryptInstalledAppId(encryptedAppId)
    const deviceIds = req.body.deviceIDs; //array of deviceIds from app

    if (!installedAppId) {
        return res.status(400).send({ error: 'Missing installedAppId in headers' });
    }
    if (!deviceIds || deviceIds.length === 0) {
        return res.status(400).send({ error: 'No devices provided or invalid device format' });
    }

    try {
        const ctx = await apiApp.withContext(installedAppId);
        if (!ctx) {
            throw new Error('Failed to establish context with SmartThings API');
        }

        // Construct device configurations from device IDs
        const devicesConfig = deviceIds.map(deviceId => ({
            deviceId: deviceId,
            componentId: "main"
        }));

        // Options for subscription
        const options = {
            stateChangeOnly: true
        };

        // Subscribe to all capabilities and attributes for each device
        const subscriptionPromises = devicesConfig.map(deviceConfig =>
            ctx.api.subscriptions.subscribeToDevices(
                [deviceConfig],
                '*', // All capabilities
                '*', // All attributes
                'switchHandler',
                options
            ).catch(error => {
                console.error(`Failed to subscribe device ${deviceConfig.deviceId}: ${error}`);
                return { deviceId: deviceConfig.deviceId, error: error.message };
            })
        );

        const subscriptionResults = await Promise.all(subscriptionPromises);

        res.send({ success: true, results: subscriptionResults });
    } catch (error) {
        console.error(`Error subscribing to devices: ${error}`);
        res.status(500).send({ error: error.message });
    }
});

and I’m getting a success response. but when I change the state of subscribed to devices, im not gettin anything in any console, not in the ice where I build the app, and not in the glitch logs.

You should verify if the subscription was created using the endpoint https://api.smartthings.com/v1/installedapps/installedAppId/subscriptions. You can authenticate with the app’s Access Token or your Personal Access Token.
I created a subscription similar to yours and I get the events correctly. This is the command:

await context.api.subscriptions.subscribeToDevices(
      context.config.tvSwitch,
      "*",
      "*",
      "tvSwitchOnHandler",
      {stateChangeOnly: false}
    );

And, this is the response from that endpoint:

{
    "items": [
        {
            "id": "9b1de362-6bbc-...",
            "installedAppId": "cf5bd567-32c3-4b6e-...",
            "sourceType": "DEVICE",
            "device": {
                "deviceId": "3c0db414-5786-4088-...",
                "componentId": "main",
                "capability": "*",
                "attribute": "*",
                "value": "*",
                "stateChangeOnly": false,
                "subscriptionName": "tvSwitchOnHandler_0",
                "modes": []
            }
        }
    ],
    "_links": {}
}
1 Like