'Authorizing the selected devices failed'


(Paul Chambers) #1

I’m working on a SmartApp that exposes a REST endpoint to interface with another web service I’m tinkering with. I used Danny Kleinman’s ‘Creating a Custom REST SmartApp Endpoint’ tutorial as a starting point, and have it behaving the way I want, with one exception.

For efficiency’s sake, I wanted to preprocess the list of devices I got from the user/oauth process into the state global. It gets used a lot, and I don’t want to rebuild it every time I receive a request.

Here’s the weird part. if I add something to ‘state’ during the ‘installed’ or ‘updated’ methods, the oauth process fails, every time. After selecting the devices to control (from a long list of capabilities) and hitting ‘Authorize’, I get a dialog that says “Authorizing the selected devices failed. Please try again later or contact support@smartthings.com” and the oauth process bails. If I comment out the one line that alters state, everything proceeds normally and it works as intended (though the rest of my code doesn’t do much, since it looks like no devices were selected by the user).

Here’s a snippet:

def allDevices() {
	def devices =    
	  [ accelerationSensor, actuator, alarm, battery, button, colorControl,
		carbonMonoxideDetector, configuration, contactSensor, energyMeter,
		illuminanceMeasurement, imageCapture, indicator, locationMode, lock,
		lockCodes, momentary, motionSensor, musicPlayer, polling, powerMeter,
		presenceSensor, refresh, relativeHumidity, sensor, signalStrength,
		smokeDetector, switches, switchLevel, temperatureMeasurement, tone,
		thermostat, threeAxis, valve, waterSensor
	  ].flatten()
	devices.retainAll( { it } )
    devices.unique( { it?.id } )
	devices
}

def installed() {
    // state.devices = allDevices()
    state.devices.each { log.debug "$it.id, $it.name, $it.label" } 
    log.trace "Installed"
}

def updated() {
    // state.devices = allDevices()
    state.devices.each { log.debug "$it.id, $it.name, $it.label" } 
	log.trace "Updated"
}

If I uncomment the two commented lines, the oauth process doesn’t complete - though all the log lines make it into the log console, so the method executes to completion, and the logged contents of state.devices is correct. Something goes awry after that point, after my methods have exited.

If I just invoke allDevices() and don’t assign the result to state.devices, the oauth process completes too. So it’s not a side-effect of that method running, only happens if the result is assigned to state

I think I’ve convinced myself at this point that the weird behavior I’m seeing is in the SmartThings platform, rather than my code. How do I get hold of a friendly ST developer? :slight_smile:

  • Paul

(Todd Wackford) #2

Hey Paul (@paulchambers),

<deleted original answer>

Never mind. I just read your code better, you’re already using flatten and unique, so it should work. Send your issue to support@smartthings.com too so they will for sure look at it soon. They’re in here pretty often, but not always.

Hope this helps,
Twack (Todd Wackford)


(Paul Chambers) #3

No worries @twack, thanks anyhow. I sent a short email off to support when I posted this, hopefully one of the ST devs will take a look soon. - Paul


(Ryan Applegate) #4

@paulchambers, Sorry for the delay in getting back to you. First of all, state is actually persisted as a JSON string so it is unable to marshall the entire list of devices that you are trying to store and most likely what is causing it to fail for you. You could store just a list of the ids (or specific data about the devices you need), but not sure that would help you much. Actually, whether you store the list of devices in state or manage it yourself in the app it probably won’t be a huge impact performance wise, but I see why you are trying to do it that way. Is it possible that you could share more of your code so we can get a better idea of what you are trying to do to propose a better solution?


(Todd Wackford) #5

@rappleg, That was my original answer, but then I saw that Paul has flattened the objects to a list which should work as a mapped json string, right?

Does the depth of the JSON string play a roll in this issue?


(Paul Chambers) #6

Here’s the most recent version of my code. This works around the problem by deferring the setup until later, rather than trying to do it once at initialization/update. I’m new to Groovy, so please excuse any ugliness:

/**
 *  External Web Service
 *
 *  Author: bod@bod.org
 *  Date: 2014-02-25
 */

import groovy.json.JsonBuilder

preferences {
	section("Which devices should be available?") {
		input "sensor", "capability.sensor", title: "Which sensors?", multiple: true, required: false
		input "presenceSensor", "capability.presenceSensor", title: "Which presence sensors?", multiple: true, required: false
		input "actuator", "capability.actuator", title: "Which actuators?", multiple: true, required: false
		input "switches", "capability.switch", title: "Which switches?", multiple: true, required: false
		input "colorControl", "capability.colorControl", title: "Which color controls?", multiple: true, required: false
		input "musicPlayer", "capability.musicPlayer", title: "Which music players?", multiple: true, required: false
		input "alarm", "capability.alarm", title: "Which alarms?", multiple: true, required: false
		input "energyMeter", "capability.energyMeter", title: "Which energy meters?", multiple: true, required: false
		input "indicator", "capability.indicator", title: "Which indicators?", multiple: true, required: false
		input "powerMeter", "capability.powerMeter", title: "Which power meters?", multiple: true, required: false
		input "smokeDetector", "capability.smokeDetector", title: "Which smoke detectors?", multiple: true, required: false
		input "carbonMonoxideDetector", "capability.carbonMonoxideDetector", title: "Which CO detectors?", multiple: true, required: false
		input "thermostat", "capability.thermostat", title: "Which thermostats?", multiple: true, required: false
	}
    /*
	section("or select by capability") {
		input "accelerationSensor", "capability.accelerationSensor", title: "Which acceleration sensors?", multiple: true, required: false
		input "battery", "capability.battery", title: "Which battery levels?", multiple: true, required: false
		input "button", "capability.button", title: "Which buttons?", multiple: true, required: false
		input "configuration", "capability.configuration", title: "Which configurations?", multiple: true, required: false
		input "contactSensor", "capability.contactSensor", title: "Which contact sensors?", multiple: true, required: false
		input "illuminanceMeasurement", "capability.illuminanceMeasurement", title: "Which illuminance measurements?", multiple: true, required: false
		input "imageCapture", "capability.imageCapture", title: "Which image captures?", multiple: true, required: false
		input "locationMode", "capability.locationMode", title: "Which location modes?", multiple: true, required: false
		input "lock", "capability.lock", title: "Which locks?", multiple: true, required: false
		input "lockCodes", "capability.lockCodes", title: "Which lock codes?", multiple: true, required: false
		input "momentary", "capability.momentary", title: "Which momentaries?", multiple: true, required: false
		input "motionSensor", "capability.motionSensor", title: "Which motion sensors?", multiple: true, required: false
		input "polling", "capability.polling", title: "Which pollings?", multiple: true, required: false
		input "refresh", "capability.refresh", title: "Which refreshes?", multiple: true, required: false
		input "relativeHumidity", "capability.relativeHumidityMeasurement", title: "Which relative humidity sensors?", multiple: true, required: false
		input "signalStrength", "capability.signalStrength", title: "Which signal strengths?", multiple: true, required: false
		input "switchLevel", "capability.switchLevel", title: "Which switch levels?", multiple: true, required: false
		input "temperatureMeasurement", "capability.temperatureMeasurement", title: "Which temperature sensors?", multiple: true, required: false
		input "threeAxis", "capability.threeAxis", title: "Which three axis sensors?", multiple: true, required: false
		input "tone", "capability.tone", title: "Which tones?", multiple: true, required: false
		input "valve", "capability.valve", title: "Which valves?", multiple: true, required: false
		input "waterSensor", "capability.waterSensor", title: "Which water sensors?", multiple: true, required: false
	}
    */
}

mappings {

	path("/events/:id") {
		action: [
			GET: "showEvents"
		]
	}

	path("/devices") {
		action: [
			GET: "showDevice"
		]
	}
	path("/device/:id") {
		action: [
			GET: "showDevice",
			PUT: "updateDevice"
		]
	}

	path("/subscriptions") {
		action: [
        	GET: "showSubscription"
		]
	}
	path("/subscription/:id") {
		action: [
        	GET: "showSubscription",
			POST: "subscribeToDevice",
			DELETE: "unsubscribeDevice"
		]
	}
	path("/subscribe/:id") {
		action: [
        	GET: "showSubscription",
			POST: "subscribeToDevice",
			DELETE: "unsubscribeDevice"
		]
	}

}

//

def allDevices() {
    def deviceMap = [:]
	def deviceList =    
	  [ accelerationSensor, actuator, alarm, battery, button, colorControl,
		carbonMonoxideDetector, configuration, contactSensor, energyMeter,
		illuminanceMeasurement, imageCapture, indicator, locationMode, lock,
		lockCodes, momentary, motionSensor, musicPlayer, polling, powerMeter,
		presenceSensor, refresh, relativeHumidity, sensor, signalStrength,
		smokeDetector, switches, switchLevel, temperatureMeasurement, tone,
		thermostat, threeAxis, valve, waterSensor
	  ].flatten()
	deviceList.retainAll( { it } )
    deviceList.unique( { it?.id } )
    deviceList.each{ deviceMap.putAt(it.id, it) }
	return deviceMap
}

def installed() {
	state.devices = [:]
    state.webhooks = [:]
	//state = allDevices()
    //state.devices.each { log.debug "$it" } 
	log.trace "Installed"
}

def updated() {
	state.devices = [:]
    state.webhooks = [:]
	//state = allDevices()
    //state.devices.each { log.debug "$it" } 
	log.trace "Updated"
}

def uninstalled() {
	log.trace "Uninstalled"
}

// this handler is subscribed with SmartThings to receive device events
// when called, it POSTs the event (as json) to the webhook subscribed for that device

def deviceHandler(evt) {
	if (!state.devices) {
    	state.devices = allDevices()
		log.debug "Updated device list"
    }
	def webhook = state.webhooks[evt.deviceId]
	if (webhook) {
		httpPostJson(uri: webhook, path: '', body: [evt: [value: evt.value]]) {
			log.debug "Event data posted"
		}
	} else {
		log.debug "Event handler called, but $evt.deviceId is not subscribed"
	}
}

// 

def showEvents() {
	log.debug "showEvents, request: ${request.JSON}, params: ${params}"
	if (!state.devices) {
    	state.devices = allDevices()
		log.debug "showEvents populated device list"
    }
    def json = new JsonBuilder( theDevice.events(params) )
	if (params.id) {
    	def theDevice = state.devices[params.id]
        if (theDevice) {
            json( theDevice.events(params) )
        }
        else {
            return httpError(404, "$params.id not found")
        }
    }
    return json.content
}

// 

def showSubscription() {
	log.debug "showSubscription, request: ${request.JSON}, params: ${params}"
	def json = new JsonBuilder()
	if (params.id) {
    	if (state.webhooks[params.id]) {
        	json.call( [ 'id' : params.id, 'url' : state.webhooks[params.id] ] )
        } else {
			return httpError(404, "$params.id not subscribed")
    	}        
    } else {
    	log.debug "webhooks: $state.webhooks"
		json.call( state.webhooks )
	}
    return json.content
}

//

def subscribeToDevice() {
	log.debug "subscribeToDevice, request: ${request.JSON}, params: ${params}"

	if (!state.devices) {
    	state.devices = allDevices()
		log.debug "subscribeToDevice populated device list"
    }
    if (params.id) {
        def theDevice = state?.devices[params.id]
        if (theDevice) {
            def callbackUrl = request.JSON?.url
            if ( state?.webhooks[theDevice.id] ) {
                log.debug "$device.displayName already subscribed, unsubscribing first"
                unsubscribe(theDevice)
            }

            log.debug "Subscribing $theDevice.displayName ($theDevice.id) to " + callbackUrl
            state?.webhooks[theDevice.id] = callbackUrl
            subscribe(theDevice, theDevice.supportedAttributes, deviceHandler)
        } else {
            return httpError(404, "$params.id not found")
        }
    } else {
    	return httpError(400, "a device ID is required")
    }
}

//

def unsubscribeDevice() {
	log.debug "unsubscribeDevice, request: ${request.JSON}, params: ${params}"

	if (params.id) {
        def webhook = state.webhooks[params.id]
        if (webhook) {
            log.debug "Unsubscribing $params.id"
            unsubscribe(theDevice)
            state.webhooks.remove(params.id)
        } else {
            return httpError(404, "$params.id not subscribed")
        }
    } else {
    	return httpError(400, "a device ID is required")
    }
}

//

def showDevice() {
	log.debug "showDevice, request: ${request.JSON}, params: ${params}"

	def returnValue
	if (!state.devices) {
    	state.devices = allDevices()
		log.debug "showDevice populated device list"
	}

	if (params.id) {
        def theDevice = state.devices[params.id]
        if (theDevice) {
		   	log.debug "(show device $params.id)"
            returnValue = [ theDevice.flatten(), theDevice.capabilities ]
        }
        else {
            return httpError(404, "$params.id not found")
        }
	} else {
	   	log.debug "(show all devices)"
		returnValue = state.devices.collect( { k,v -> v } )
    }
    log.debug "$returnValue"
    return returnValue
}

def void updateDevice() {
	log.debug "updateDevice, request: ${request.JSON}, params: ${params}"
	
	if (!state.devices) {
    	state.devices = allDevices()
		log.debug "updateDevice populated device list"
    }
    def command = request.JSON?.command
	if (command) {
		def theDevice = state.devices[params.id]
		if (theDevice) {
			theDevice."$command"()
		} else {
			httpError(404, "$params.id not found")
		}
	}
}

I’m about to return my SmartThings kit, have moved onto other things. If I may be so bold as to offer some feedback:

a) please consider creating a device type that’s an ‘official’ REST endpoint. Nothing fancy, just the usual mix of REST,OAuth,JSON and webhook support. Then SmartApps can help the outside world interact with the SmartThings world. I’m sure one day SmartThings will take over the world, but in the meantime, extend your reach by making it easy to connect with stuff outside your ecosystem. Another upside: if you implement it, then you’ll retain the ability to make it secure (e.g. against DDOS), behaves correctly, is an efficient use of your resources, etc. On the other hand, if you make external developers implement their own, you’ll end up with multiple implementations, each with their own imperfections. Sounds like a recipe for unnecessary pain and suffering.

b) please provide a method to get the device wrapper, given a device ID. Hopefully it’s obvious why :slight_smile:

c) if ‘state’ is the only persistent storage available, expect it to be abused in all kinds of unnatural ways :wink: If it’s not very robust, can I ask that you either make it so, or improve the error reporting so external developers can tell what broke and why?

d) I was looking for less granular capabilities to define which devices the user could specify to use with my SmartApp. I ended up enumerating capabilities, and the code is much uglier as a result (as you can see). I did find ‘sensor’ and ‘actuator’, but with no documentation, and insufficient devices to discover their definition empirically, I was hesitant to rely on them.

Anyhow, hope this helps (it’s meant constructively).

  • Paul

(Andrew Urman) #7

@paulchambers I wish you change your mind and stay! That is some of the best constructive criticism so far. I want to let you know that we are re-working our documentation from the ground up. We have a new person who’s sole job is documentation which will include examples.

We’re also looking at newer community tools and ways to make priorities and voices more public. Almost everything you mentioned is on the short list for being retooled.


(Chrisb) #8

@paulchambers

As a user, I’d second the request to stay… I think it’s obvious you give assets to the community and we’d be poorer if you left.


(Ben Edwards) #9

Agreed with @urman and @chrisb and I think everyone when they say we want you as part of the SmartThings community. Feel free to reach out to me directly for specifics around the focus on documentation and developer tool improvements being made starting last week and going for the next several months. My email is my first name at our company name :slight_smile:


(Paul Chambers) #10

Thanks, I’m flattered :slight_smile: Sadly my SmartThings hardware is already on its way back to Amazon. I’m between jobs (hopefully briefly), so much of my time is being invested in the search, directly or indirectly.

Glad the feedback was useful. I could provide more, if you’re interested… :slight_smile:

  • Paul