/** * LIFX * * Copyright 2015 LIFX * */ definition( name: "LIFX (Connect)", namespace: "smartthings", author: "LIFX", description: "Allows you to use LIFX smart light bulbs with SmartThings.", category: "Convenience", iconUrl: "https://cloud.lifx.com/images/lifx.png", iconX2Url: "https://cloud.lifx.com/images/lifx.png", iconX3Url: "https://cloud.lifx.com/images/lifx.png", oauth: true, singleInstance: true) { appSetting "clientId" appSetting "clientSecret" } preferences { page(name: "Credentials", title: "LIFX", content: "authPage", install: false) } mappings { path("/receivedToken") { action: [ POST: "oauthReceivedToken", GET: "oauthReceivedToken"] } path("/receiveToken") { action: [ POST: "oauthReceiveToken", GET: "oauthReceiveToken"] } path("/hookCallback") { action: [ POST: "hookEventHandler", GET: "hookEventHandler"] } path("/oauth/callback") { action: [ GET: "oauthCallback" ] } path("/oauth/initialize") { action: [ GET: "oauthInit"] } path("/test") { action: [ GET: "oauthSuccess" ] } } def getServerUrl() { return "https://graph.api.smartthings.com" } def apiURL(path = '/') { return "https://api.lifx.com/v1beta1${path}" } def buildRedirectUrl(page) { return "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/${page}" } def authPage() { log.debug "authPage" if (!state.lifxAccessToken) { log.debug "no LIFX access token" // This is the SmartThings access token if (!state.accessToken) { log.debug "no access token, create access token" createAccessToken() // predefined method } def description = "Tap to enter LIFX credentials" def redirectUrl = "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}" // this triggers oauthInit() below log.debug "app id: ${app.id}" log.debug "redirect url: ${redirectUrl}" return dynamicPage(name: "Credentials", title: "Connect to LIFX", nextPage: null, uninstall: true, install:false) { section { href(url:redirectUrl, required:true, title:"Connect to LIFX", description:"Tap here to connect your LIFX account") // href(url:buildRedirectUrl("test"), title: "Message test") } } } else { log.debug "have LIFX access token" def options = locationOptions() ?: [] def count = options.size() def refreshInterval = 3 return dynamicPage(name:"Credentials", title:"Select devices...", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) { section("Select your location") { input "selectedLocationId", "enum", required:true, title:"Select location (${count} found)", multiple:false, options:options } } } } // OAuth def oauthInit() { def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote_control:all", response_type: "code" ] log.info("Redirecting user to OAuth setup") redirect(location: "https://cloud.lifx.com/oauth/authorize?${toQueryString(oauthParams)}") } def oauthCallback() { def redirectUrl = null if (params.authQueryString) { redirectUrl = URLDecoder.decode(params.authQueryString.replaceAll(".+&redirect_url=", "")) } else { log.warn "No authQueryString" } if (state.lifxAccessToken) { log.debug "Access token already exists" success() } else { def code = params.code if (code) { if (code.size() > 6) { // LIFX code log.debug "Exchanging code for access token" oauthReceiveToken(redirectUrl) } else { // Initiate the LIFX OAuth flow. oauthInit() } } else { log.debug "This code should be unreachable" success() } } } def oauthReceiveToken(redirectUrl = null) { log.debug "receiveToken - params: ${params}" def oauthParams = [ client_id: "${appSettings.clientId}", client_secret: "${appSettings.clientSecret}", grant_type: "authorization_code", code: params.code, scope: params.scope ] // how is params.code valid here? def params = [ uri: "https://cloud.lifx.com/oauth/token", body: oauthParams, headers: [ "User-Agent": "SmartThings Integration" ] ] httpPost(params) { response -> state.lifxAccessToken = response.data.access_token } if (state.lifxAccessToken) { oauthSuccess() } else { oauthFailure() } } def oauthSuccess() { def message = """

Your LIFX Account is now connected to SmartThings!

Click 'Done' to finish setup.

""" oauthConnectionStatus(message) } def oauthFailure() { def message = """

The connection could not be established!

Click 'Done' to return to the menu.

""" oauthConnectionStatus(message) } def oauthReceivedToken() { def message = """

Your LIFX Account is already connected to SmartThings!

Click 'Done' to finish setup.

""" oauthConnectionStatus(message) } def oauthConnectionStatus(message, redirectUrl = null) { def redirectHtml = "" if (redirectUrl) { redirectHtml = """ """ } def html = """ SmartThings Connection ${redirectHtml}
LIFX icon connected device icon SmartThings logo

${message}

""" render contentType: 'text/html', data: html } String toQueryString(Map m) { return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") } // App lifecycle hooks def installed() { enableCallback() // wtf does this do? if (!state.accessToken) { createAccessToken() } else { initialize() } // Check for new devices and remove old ones every 3 hours runEvery3Hours('updateDevices') } // called after settings are changed def updated() { enableCallback() // not sure what this does if (!state.accessToken) { createAccessToken() } else { initialize() } } def uninstalled() { log.info("Uninstalling, removing child devices...") unschedule('updateDevices') removeChildDevices(getChildDevices()) } private removeChildDevices(devices) { devices.each { deleteChildDevice(it.deviceNetworkId) // 'it' is default } } // called after Done is hit after selecting a Location def initialize() { log.debug "initialize" updateDevices() } // Misc Map apiRequestHeaders() { return ["Authorization": "Bearer ${state.lifxAccessToken}", "Accept": "application/json", "Content-Type": "application/json", "User-Agent": "SmartThings Integration" ] } // Requests def logResponse(response) { log.info("Status: ${response.status}") log.info("Body: ${response.data}") } // API Requests // logObject is because log doesn't work if this method is being called from a Device def logErrors(options = [errorReturn: null, logObject: log], Closure c) { try { return c() } catch (groovyx.net.http.HttpResponseException e) { options.logObject.error("got error: ${e}, body: ${e.getResponse().getData()}") if (e.statusCode == 401) { // token is expired state.remove("lifxAccessToken") options.logObject.warn "Access token is not valid" } return options.errerReturn } catch (java.net.SocketTimeoutException e) { options.logObject.warn "Connection timed out, not much we can do here" return options.errerReturn } } def apiGET(path) { httpGet(uri: apiURL(path), headers: apiRequestHeaders()) {response -> logResponse(response) return response } } def apiPUT(path, body = [:]) { log.debug("Beginning API PUT: ${path}, ${body}") httpPutJson(uri: apiURL(path), body: new groovy.json.JsonBuilder(body).toString(), headers: apiRequestHeaders(), ) {response -> logResponse(response) return response } } def devicesList(selector = '') { logErrors([]) { def resp = apiGET("/lights/${selector}") if (resp.status == 200) { return resp.data } else { log.error("Non-200 from device list call. ${resp.status} ${resp.data}") return [] } } } Map locationOptions() { def options = [:] def devices = devicesList() devices.each { device -> options[device.location.id] = device.location.name } return options } def devicesInLocation() { return devicesList("location_id:${settings.selectedLocationId}") } // ensures the devices list is up to date def updateDevices() { if (!state.devices) { state.devices = [:] } def devices = devicesInLocation() def deviceIds = devices*.id devices.each { device -> def childDevice = getChildDevice(device.id) if (!childDevice) { log.info("Adding device ${device.id}: ${device.capabilities}") def data = [ label: device.label, level: sprintf("%f", (device.brightness ?: 1) * 100), switch: device.connected ? device.power : "unreachable", colorTemperature: device.color.kelvin ] if (device.capabilities.has_color) { data["color"] = colorUtil.hslToHex((device.color.hue / 3.6) as int, (device.color.saturation * 100) as int) data["hue"] = device.color.hue / 3.6 data["saturation"] = device.color.saturation * 100 childDevice = addChildDevice("smartthings", "LIFX Color Bulb", device.id, null, data) } else { childDevice = addChildDevice("smartthings", "LIFX White Bulb", device.id, null, data) } } } getChildDevices().findAll { !deviceIds.contains(it.deviceNetworkId) }.each { log.info("Deleting ${it.deviceNetworkId}") deleteChildDevice(it.deviceNetworkId) } runIn(1, 'refreshDevices') // Asynchronously refresh devices so we don't block } def refreshDevices() { log.info("Refreshing all devices...") getChildDevices().each { device -> device.refresh() } }