LiFX - change shardUrl param to apiServerUrl

This commit is contained in:
Warodom Khamphanchai
2015-11-16 16:33:07 -08:00
parent 8b6942525d
commit 5e93bca030
3 changed files with 239 additions and 190 deletions

View File

@@ -17,44 +17,50 @@ metadata {
} }
simulator { simulator {
// TODO: define status and reply messages here
} }
tiles { tiles(scale: 2) {
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
state "unreachable", label: "?", action:"refresh.refresh", icon:"st.switches.light.off", backgroundColor:"#666666" tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
state "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff" attributeState "unreachable", label: "?", action:"refresh.refresh", icon:"http://hosted.lifx.co/smartthings/v1/196xUnreachable.png", backgroundColor:"#666666"
state "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" attributeState "on", label:'${name}', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#79b821", nextState:"turningOff"
state "turningOn", label:'Turning on', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff" attributeState "off", label:'${name}', action:"switch.on", icon:"http://hosted.lifx.co/smartthings/v1/196xOff.png", backgroundColor:"#ffffff", nextState:"turningOn"
state "turningOff", label:'Turning off', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" attributeState "turningOn", label:'Turning on', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "turningOff", label:'Turning off', action:"switch.on", icon:"http://hosted.lifx.co/smartthings/v1/196xOff.png", backgroundColor:"#ffffff", nextState:"turningOn"
}
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
attributeState "level", action:"switch level.setLevel"
}
tileAttribute ("device.color", key: "COLOR_CONTROL") {
attributeState "color", action:"setColor"
}
tileAttribute ("device.model", key: "SECONDARY_CONTROL") {
attributeState "model", label: '${currentValue}'
}
} }
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
} }
valueTile("null", "device.switch", inactiveLabel: false, decoration: "flat") { valueTile("null", "device.switch", inactiveLabel: false, decoration: "flat") {
state "default", label:'' state "default", label:''
} }
controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) { controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 2, width: 4, inactiveLabel: false, range:"(2700..9000)") {
state "color", action:"setColor"
}
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false, range:"(0..100)") {
state "level", action:"switch level.setLevel"
}
valueTile("level", "device.level", inactiveLabel: false, icon: "st.illuminance.illuminance.light", decoration: "flat") {
state "level", label: '${currentValue}%'
}
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..9000)") {
state "colorTemp", action:"color temperature.setColorTemperature" state "colorTemp", action:"color temperature.setColorTemperature"
} }
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat") {
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", height: 2, width: 2) {
state "colorTemp", label: '${currentValue}K' state "colorTemp", label: '${currentValue}K'
} }
main(["switch"]) main "switch"
details(["switch", "refresh", "level", "levelSliderControl", "rgbSelector", "colorTempSliderControl", "colorTemp"]) details(["switch", "colorTempSliderControl", "colorTemp", "refresh"])
} }
} }
@@ -70,7 +76,7 @@ def parse(String description) {
def setHue(percentage) { def setHue(percentage) {
log.debug "setHue ${percentage}" log.debug "setHue ${percentage}"
parent.logErrors(logObject: log) { parent.logErrors(logObject: log) {
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "hue:${percentage * 3.6}"]) def resp = parent.apiPUT("/lights/${selector()}/state", [color: "hue:${percentage * 3.6}", power: "on"])
if (resp.status < 300) { if (resp.status < 300) {
sendEvent(name: "hue", value: percentage) sendEvent(name: "hue", value: percentage)
sendEvent(name: "switch", value: "on") sendEvent(name: "switch", value: "on")
@@ -83,7 +89,7 @@ def setHue(percentage) {
def setSaturation(percentage) { def setSaturation(percentage) {
log.debug "setSaturation ${percentage}" log.debug "setSaturation ${percentage}"
parent.logErrors(logObject: log) { parent.logErrors(logObject: log) {
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "saturation:${percentage / 100}"]) def resp = parent.apiPUT("/lights/${selector()}/state", [color: "saturation:${percentage / 100}", power: "on"])
if (resp.status < 300) { if (resp.status < 300) {
sendEvent(name: "saturation", value: percentage) sendEvent(name: "saturation", value: percentage)
sendEvent(name: "switch", value: "on") sendEvent(name: "switch", value: "on")
@@ -114,7 +120,7 @@ def setColor(Map color) {
} }
} }
parent.logErrors(logObject:log) { parent.logErrors(logObject:log) {
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: attrs.join(" ")]) def resp = parent.apiPUT("/lights/${selector()}/state", [color: attrs.join(" "), power: "on"])
if (resp.status < 300) { if (resp.status < 300) {
sendEvent(name: "color", value: color.hex) sendEvent(name: "color", value: color.hex)
sendEvent(name: "switch", value: "on") sendEvent(name: "switch", value: "on")
@@ -135,9 +141,10 @@ def setLevel(percentage) {
return off() // if the brightness is set to 0, just turn it off return off() // if the brightness is set to 0, just turn it off
} }
parent.logErrors(logObject:log) { parent.logErrors(logObject:log) {
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", ["color": "brightness:${percentage / 100}"]) def resp = parent.apiPUT("/lights/${selector()}/state", ["brightness": percentage / 100, "power": "on"])
if (resp.status < 300) { if (resp.status < 300) {
sendEvent(name: "level", value: percentage) sendEvent(name: "level", value: percentage)
sendEvent(name: "switch.setLevel", value: percentage)
sendEvent(name: "switch", value: "on") sendEvent(name: "switch", value: "on")
} else { } else {
log.error("Bad setLevel result: [${resp.status}] ${resp.data}") log.error("Bad setLevel result: [${resp.status}] ${resp.data}")
@@ -148,7 +155,7 @@ def setLevel(percentage) {
def setColorTemperature(kelvin) { def setColorTemperature(kelvin) {
log.debug "Executing 'setColorTemperature' to ${kelvin}" log.debug "Executing 'setColorTemperature' to ${kelvin}"
parent.logErrors() { parent.logErrors() {
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "kelvin:${kelvin}"]) def resp = parent.apiPUT("/lights/${selector()}/state", [color: "kelvin:${kelvin}", power: "on"])
if (resp.status < 300) { if (resp.status < 300) {
sendEvent(name: "colorTemperature", value: kelvin) sendEvent(name: "colorTemperature", value: kelvin)
sendEvent(name: "color", value: "#ffffff") sendEvent(name: "color", value: "#ffffff")
@@ -163,7 +170,7 @@ def setColorTemperature(kelvin) {
def on() { def on() {
log.debug "Device setOn" log.debug "Device setOn"
parent.logErrors() { parent.logErrors() {
if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "on"]) != null) { if (parent.apiPUT("/lights/${selector()}/state", [power: "on"]) != null) {
sendEvent(name: "switch", value: "on") sendEvent(name: "switch", value: "on")
} }
} }
@@ -172,7 +179,7 @@ def on() {
def off() { def off() {
log.debug "Device setOff" log.debug "Device setOff"
parent.logErrors() { parent.logErrors() {
if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "off"]) != null) { if (parent.apiPUT("/lights/${selector()}/state", [power: "off"]) != null) {
sendEvent(name: "switch", value: "off") sendEvent(name: "switch", value: "off")
} }
} }
@@ -180,19 +187,26 @@ def off() {
def poll() { def poll() {
log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}" log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}"
def resp = parent.apiGET("/lights/${device.deviceNetworkId}") def resp = parent.apiGET("/lights/${selector()}")
if (resp.status != 200) { if (resp.status == 404) {
sendEvent(name: "switch", value: "unreachable")
return []
} else if (resp.status != 200) {
log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}") log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}")
return [] return []
} }
def data = resp.data def data = resp.data[0]
log.debug("Data: ${data}")
sendEvent(name: "level", value: sprintf("%.1f", (data.brightness ?: 1) * 100)) sendEvent(name: "label", value: data.label)
sendEvent(name: "level", value: Math.round((data.brightness ?: 1) * 100))
sendEvent(name: "switch.setLevel", value: Math.round((data.brightness ?: 1) * 100))
sendEvent(name: "switch", value: data.connected ? data.power : "unreachable") sendEvent(name: "switch", value: data.connected ? data.power : "unreachable")
sendEvent(name: "color", value: colorUtil.hslToHex((data.color.hue / 3.6) as int, (data.color.saturation * 100) as int)) sendEvent(name: "color", value: colorUtil.hslToHex((data.color.hue / 3.6) as int, (data.color.saturation * 100) as int))
sendEvent(name: "hue", value: data.color.hue / 3.6) sendEvent(name: "hue", value: data.color.hue / 3.6)
sendEvent(name: "saturation", value: data.color.saturation * 100) sendEvent(name: "saturation", value: data.color.saturation * 100)
sendEvent(name: "colorTemperature", value: data.color.kelvin) sendEvent(name: "colorTemperature", value: data.color.kelvin)
sendEvent(name: "model", value: "${data.product.company} ${data.product.name}")
return [] return []
} }
@@ -201,3 +215,11 @@ def refresh() {
log.debug "Executing 'refresh'" log.debug "Executing 'refresh'"
poll() poll()
} }
def selector() {
if (device.deviceNetworkId.contains(":")) {
return device.deviceNetworkId
} else {
return "id:${device.deviceNetworkId}"
}
}

View File

@@ -16,41 +16,44 @@ metadata {
} }
simulator { simulator {
// TODO: define status and reply messages here
} }
tiles { tiles(scale: 2) {
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
state "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff" tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
state "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" attributeState "unreachable", label: "?", action:"refresh.refresh", icon:"http://hosted.lifx.co/smartthings/v1/196xUnreachable.png", backgroundColor:"#666666"
state "turningOn", label:'Turning on', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff" attributeState "on", label:'${name}', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#79b821", nextState:"turningOff"
state "turningOff", label:'Turning off', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" attributeState "off", label:'${name}', action:"switch.on", icon:"http://hosted.lifx.co/smartthings/v1/196xOff.png", backgroundColor:"#ffffff", nextState:"turningOn"
state "unreachable", label: "?", action:"refresh.refresh", icon:"st.switches.light.off", backgroundColor:"#666666" attributeState "turningOn", label:'Turning on', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "turningOff", label:'Turning off', action:"switch.on", icon:"http://hosted.lifx.co/smartthings/v1/196xOff.png", backgroundColor:"#ffffff", nextState:"turningOn"
}
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
attributeState "level", action:"switch level.setLevel"
}
} }
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
} }
valueTile("null", "device.switch", inactiveLabel: false, decoration: "flat") { valueTile("null", "device.switch", inactiveLabel: false, decoration: "flat") {
state "default", label:'' state "default", label:''
} }
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false, range:"(0..100)") { controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 2, width: 4, inactiveLabel: false, range:"(2700..9000)") {
state "level", action:"switch level.setLevel"
}
valueTile("level", "device.level", inactiveLabel: false, icon: "st.illuminance.illuminance.light", decoration: "flat") {
state "level", label: '${currentValue}%'
}
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..6500)") {
state "colorTemp", action:"color temperature.setColorTemperature" state "colorTemp", action:"color temperature.setColorTemperature"
} }
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat") {
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", height: 2, width: 2) {
state "colorTemp", label: '${currentValue}K' state "colorTemp", label: '${currentValue}K'
} }
main(["switch"]) main "switch"
details(["switch", "refresh", "level", "levelSliderControl", "colorTempSliderControl", "colorTemp"]) details(["switch", "colorTempSliderControl", "colorTemp", "refresh"])
} }
} }
// parse events into attributes // parse events into attributes
@@ -72,9 +75,10 @@ def setLevel(percentage) {
return off() // if the brightness is set to 0, just turn it off return off() // if the brightness is set to 0, just turn it off
} }
parent.logErrors(logObject:log) { parent.logErrors(logObject:log) {
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", ["color": "brightness:${percentage / 100}"]) def resp = parent.apiPUT("/lights/${selector()}/state", [brightness: percentage / 100, power: "on"])
if (resp.status < 300) { if (resp.status < 300) {
sendEvent(name: "level", value: percentage) sendEvent(name: "level", value: percentage)
sendEvent(name: "switch.setLevel", value: percentage)
sendEvent(name: "switch", value: "on") sendEvent(name: "switch", value: "on")
} else { } else {
log.error("Bad setLevel result: [${resp.status}] ${resp.data}") log.error("Bad setLevel result: [${resp.status}] ${resp.data}")
@@ -85,7 +89,7 @@ def setLevel(percentage) {
def setColorTemperature(kelvin) { def setColorTemperature(kelvin) {
log.debug "Executing 'setColorTemperature' to ${kelvin}" log.debug "Executing 'setColorTemperature' to ${kelvin}"
parent.logErrors() { parent.logErrors() {
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "kelvin:${kelvin}"]) def resp = parent.apiPUT("/lights/${selector()}/state", [color: "kelvin:${kelvin}", power: "on"])
if (resp.status < 300) { if (resp.status < 300) {
sendEvent(name: "colorTemperature", value: kelvin) sendEvent(name: "colorTemperature", value: kelvin)
sendEvent(name: "color", value: "#ffffff") sendEvent(name: "color", value: "#ffffff")
@@ -100,7 +104,7 @@ def setColorTemperature(kelvin) {
def on() { def on() {
log.debug "Device setOn" log.debug "Device setOn"
parent.logErrors() { parent.logErrors() {
if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "on"]) != null) { if (parent.apiPUT("/lights/${selector()}/state", [power: "on"]) != null) {
sendEvent(name: "switch", value: "on") sendEvent(name: "switch", value: "on")
} }
} }
@@ -109,7 +113,7 @@ def on() {
def off() { def off() {
log.debug "Device setOff" log.debug "Device setOff"
parent.logErrors() { parent.logErrors() {
if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "off"]) != null) { if (parent.apiPUT("/lights/${selector()}/state", [power: "off"]) != null) {
sendEvent(name: "switch", value: "off") sendEvent(name: "switch", value: "off")
} }
} }
@@ -117,16 +121,22 @@ def off() {
def poll() { def poll() {
log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}" log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}"
def resp = parent.apiGET("/lights/${device.deviceNetworkId}") def resp = parent.apiGET("/lights/${selector()}")
if (resp.status != 200) { if (resp.status == 404) {
sendEvent(name: "switch", value: "unreachable")
return []
} else if (resp.status != 200) {
log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}") log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}")
return [] return []
} }
def data = resp.data def data = resp.data[0]
sendEvent(name: "level", value: sprintf("%f", (data.brightness ?: 1) * 100)) sendEvent(name: "label", value: data.label)
sendEvent(name: "level", value: Math.round((data.brightness ?: 1) * 100))
sendEvent(name: "switch.setLevel", value: Math.round((data.brightness ?: 1) * 100))
sendEvent(name: "switch", value: data.connected ? data.power : "unreachable") sendEvent(name: "switch", value: data.connected ? data.power : "unreachable")
sendEvent(name: "colorTemperature", value: data.color.kelvin) sendEvent(name: "colorTemperature", value: data.color.kelvin)
sendEvent(name: "model", value: data.product.name)
return [] return []
} }
@@ -135,3 +145,11 @@ def refresh() {
log.debug "Executing 'refresh'" log.debug "Executing 'refresh'"
poll() poll()
} }
def selector() {
if (device.deviceNetworkId.contains(":")) {
return device.deviceNetworkId
} else {
return "id:${device.deviceNetworkId}"
}
}

View File

@@ -5,23 +5,23 @@
* *
*/ */
definition( definition(
name: "LIFX (Connect)", name: "LIFX (Connect)",
namespace: "smartthings", namespace: "smartthings",
author: "LIFX", author: "LIFX",
description: "Allows you to use LIFX smart light bulbs with SmartThings.", description: "Allows you to use LIFX smart light bulbs with SmartThings.",
category: "Convenience", category: "Convenience",
iconUrl: "https://cloud.lifx.com/images/lifx.png", iconUrl: "https://cloud.lifx.com/images/lifx.png",
iconX2Url: "https://cloud.lifx.com/images/lifx.png", iconX2Url: "https://cloud.lifx.com/images/lifx.png",
iconX3Url: "https://cloud.lifx.com/images/lifx.png", iconX3Url: "https://cloud.lifx.com/images/lifx.png",
oauth: true, oauth: true,
singleInstance: true) { singleInstance: true) {
appSetting "clientId" appSetting "clientId"
appSetting "clientSecret" appSetting "clientSecret"
} }
preferences { preferences {
page(name: "Credentials", title: "LIFX", content: "authPage", install: false) page(name: "Credentials", title: "LIFX", content: "authPage", install: true)
} }
mappings { mappings {
@@ -33,29 +33,29 @@ mappings {
path("/test") { action: [ GET: "oauthSuccess" ] } path("/test") { action: [ GET: "oauthSuccess" ] }
} }
def getServerUrl() { return "https://graph.api.smartthings.com" } def getServerUrl() { return "https://graph.api.smartthings.com" }
def apiURL(path = '/') { return "https://api.lifx.com/v1beta1${path}" } def getCallbackUrl() { return "https://graph.api.smartthings.com/oauth/callback"}
def buildRedirectUrl(page) { def apiURL(path = '/') { return "https://api.lifx.com/v1${path}" }
return "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/${page}" def getSecretKey() { return appSettings.secretKey }
} def getClientId() { return appSettings.clientId }
def authPage() { def authPage() {
log.debug "authPage" log.debug "authPage test1"
if (!state.lifxAccessToken) { if (!state.lifxAccessToken) {
log.debug "no LIFX access token" log.debug "no LIFX access token"
// This is the SmartThings access token // This is the SmartThings access token
if (!state.accessToken) { if (!state.accessToken) {
log.debug "no access token, create access token" log.debug "no access token, create access token"
createAccessToken() // predefined method state.accessToken = createAccessToken() // predefined method
} }
def description = "Tap to enter LIFX credentials" def description = "Tap to enter LIFX credentials"
def redirectUrl = "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}" // this triggers oauthInit() below def redirectUrl = "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" // this triggers oauthInit() below
log.debug "app id: ${app.id}" // def redirectUrl = "${apiServerUrl}"
log.debug "app id: ${app.id}"
log.debug "redirect url: ${redirectUrl}" log.debug "redirect url: ${redirectUrl}"
return dynamicPage(name: "Credentials", title: "Connect to LIFX", nextPage: null, uninstall: true, install:false) { return dynamicPage(name: "Credentials", title: "Connect to LIFX", nextPage: null, uninstall: true, install:true) {
section { section {
href(url:redirectUrl, required:true, title:"Connect to LIFX", description:"Tap here to connect your LIFX account") 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 { } else {
@@ -63,17 +63,15 @@ def authPage() {
def options = locationOptions() ?: [] def options = locationOptions() ?: []
def count = options.size() def count = options.size()
def refreshInterval = 3
return dynamicPage(name:"Credentials", title:"Select devices...", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) { return dynamicPage(name:"Credentials", title:"", nextPage:"", install:true, uninstall: true) {
section("Select your location") { section("Select your location") {
input "selectedLocationId", "enum", required:true, title:"Select location (${count} found)", multiple:false, options:options input "selectedLocationId", "enum", required:true, title:"Select location (${count} found)", multiple:false, options:options, submitOnChange: true
} }
} }
} }
} }
// OAuth // OAuth
def oauthInit() { def oauthInit() {
@@ -112,7 +110,7 @@ def oauthCallback() {
} }
def oauthReceiveToken(redirectUrl = null) { def oauthReceiveToken(redirectUrl = null) {
// Not sure what redirectUrl is for
log.debug "receiveToken - params: ${params}" 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 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 = [ def params = [
@@ -135,25 +133,25 @@ def oauthReceiveToken(redirectUrl = null) {
def oauthSuccess() { def oauthSuccess() {
def message = """ def message = """
<p>Your LIFX Account is now connected to SmartThings!</p> <p>Your LIFX Account is now connected to SmartThings!</p>
<p>Click 'Done' to finish setup.</p> <p>Click 'Done' to finish setup.</p>
""" """
oauthConnectionStatus(message) oauthConnectionStatus(message)
} }
def oauthFailure() { def oauthFailure() {
def message = """ def message = """
<p>The connection could not be established!</p> <p>The connection could not be established!</p>
<p>Click 'Done' to return to the menu.</p> <p>Click 'Done' to return to the menu.</p>
""" """
oauthConnectionStatus(message) oauthConnectionStatus(message)
} }
def oauthReceivedToken() { def oauthReceivedToken() {
def message = """ def message = """
<p>Your LIFX Account is already connected to SmartThings!</p> <p>Your LIFX Account is already connected to SmartThings!</p>
<p>Click 'Done' to finish setup.</p> <p>Click 'Done' to finish setup.</p>
""" """
oauthConnectionStatus(message) oauthConnectionStatus(message)
} }
@@ -161,74 +159,74 @@ def oauthConnectionStatus(message, redirectUrl = null) {
def redirectHtml = "" def redirectHtml = ""
if (redirectUrl) { if (redirectUrl) {
redirectHtml = """ redirectHtml = """
<meta http-equiv="refresh" content="3; url=${redirectUrl}" /> <meta http-equiv="refresh" content="3; url=${redirectUrl}" />
""" """
} }
def html = """ def html = """
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
<title>SmartThings Connection</title> <title>SmartThings Connection</title>
<style type="text/css"> <style type="text/css">
@font-face { @font-face {
font-family: 'Swiss 721 W01 Thin'; font-family: 'Swiss 721 W01 Thin';
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot'); src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'), src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'), url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'), url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg'); url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: 'Swiss 721 W01 Light'; font-family: 'Swiss 721 W01 Light';
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot'); src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'), src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'), url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'), url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg'); url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
.container { .container {
width: 280; width: 280;
padding: 20px; padding: 20px;
text-align: center; text-align: center;
} }
img { img {
vertical-align: middle; vertical-align: middle;
} }
img:nth-child(2) { img:nth-child(2) {
margin: 0 15px; margin: 0 15px;
} }
p { p {
font-size: 1.2em; font-size: 1.2em;
font-family: 'Swiss 721 W01 Thin'; font-family: 'Swiss 721 W01 Thin';
text-align: center; text-align: center;
color: #666666; color: #666666;
padding: 0 20px; padding: 0 20px;
margin-bottom: 0; margin-bottom: 0;
} }
span { span {
font-family: 'Swiss 721 W01 Light'; font-family: 'Swiss 721 W01 Light';
} }
</style> </style>
${redirectHtml} ${redirectHtml}
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<img src='https://cloud.lifx.com/images/lifx.png' alt='LIFX icon' width='100'/> <img src='https://cloud.lifx.com/images/lifx.png' alt='LIFX icon' width='100'/>
<img src='https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png' alt='connected device icon' width="40"/> <img src='https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png' alt='connected device icon' width="40"/>
<img src='https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png' alt='SmartThings logo' width="100"/> <img src='https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png' alt='SmartThings logo' width="100"/>
<p> <p>
${message} ${message}
</p> </p>
</div> </div>
</body> </body>
</html> </html>
""" """
render contentType: 'text/html', data: html render contentType: 'text/html', data: html
} }
@@ -239,7 +237,6 @@ String toQueryString(Map m) {
// App lifecycle hooks // App lifecycle hooks
def installed() { def installed() {
enableCallback() // wtf does this do?
if (!state.accessToken) { if (!state.accessToken) {
createAccessToken() createAccessToken()
} else { } else {
@@ -251,7 +248,6 @@ def installed() {
// called after settings are changed // called after settings are changed
def updated() { def updated() {
enableCallback() // not sure what this does
if (!state.accessToken) { if (!state.accessToken) {
createAccessToken() createAccessToken()
} else { } else {
@@ -305,27 +301,36 @@ def logErrors(options = [errorReturn: null, logObject: log], Closure c) {
state.remove("lifxAccessToken") state.remove("lifxAccessToken")
options.logObject.warn "Access token is not valid" options.logObject.warn "Access token is not valid"
} }
return options.errerReturn return options.errorReturn
} catch (java.net.SocketTimeoutException e) { } catch (java.net.SocketTimeoutException e) {
options.logObject.warn "Connection timed out, not much we can do here" options.logObject.warn "Connection timed out, not much we can do here"
return options.errerReturn return options.errorReturn
} }
} }
def apiGET(path) { def apiGET(path) {
httpGet(uri: apiURL(path), headers: apiRequestHeaders()) {response -> try {
logResponse(response) httpGet(uri: apiURL(path), headers: apiRequestHeaders()) {response ->
return response logResponse(response)
return response
}
} catch (groovyx.net.http.HttpResponseException e) {
logResponse(e.response)
return e.response
} }
} }
def apiPUT(path, body = [:]) { def apiPUT(path, body = [:]) {
log.debug("Beginning API PUT: ${path}, ${body}") try {
httpPutJson(uri: apiURL(path), body: new groovy.json.JsonBuilder(body).toString(), headers: apiRequestHeaders(), ) {response -> log.debug("Beginning API PUT: ${path}, ${body}")
logResponse(response) httpPutJson(uri: apiURL(path), body: new groovy.json.JsonBuilder(body).toString(), headers: apiRequestHeaders(), ) {response ->
return response logResponse(response)
} return response
} }
} catch (groovyx.net.http.HttpResponseException e) {
logResponse(e.response)
return e.response
}}
def devicesList(selector = '') { def devicesList(selector = '') {
logErrors([]) { logErrors([]) {
@@ -340,12 +345,12 @@ def devicesList(selector = '') {
} }
Map locationOptions() { Map locationOptions() {
def options = [:] def options = [:]
def devices = devicesList() def devices = devicesList()
devices.each { device -> devices.each { device ->
options[device.location.id] = device.location.name options[device.location.id] = device.location.name
} }
log.debug("Locations: ${options}")
return options return options
} }
@@ -359,28 +364,32 @@ def updateDevices() {
state.devices = [:] state.devices = [:]
} }
def devices = devicesInLocation() def devices = devicesInLocation()
def deviceIds = devices*.id def selectors = []
log.debug("All selectors: ${selectors}")
devices.each { device -> devices.each { device ->
def childDevice = getChildDevice(device.id) def childDevice = getChildDevice(device.id)
selectors.add("${device.id}")
if (!childDevice) { if (!childDevice) {
log.info("Adding device ${device.id}: ${device.capabilities}") log.info("Adding device ${device.id}: ${device.product}")
def data = [ def data = [
label: device.label, label: device.label,
level: sprintf("%f", (device.brightness ?: 1) * 100), level: Math.round((device.brightness ?: 1) * 100),
switch: device.connected ? device.power : "unreachable", switch: device.connected ? device.power : "unreachable",
colorTemperature: device.color.kelvin colorTemperature: device.color.kelvin
] ]
if (device.capabilities.has_color) { if (device.product.capabilities.has_color) {
data["color"] = colorUtil.hslToHex((device.color.hue / 3.6) as int, (device.color.saturation * 100) as int) 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["hue"] = device.color.hue / 3.6
data["saturation"] = device.color.saturation * 100 data["saturation"] = device.color.saturation * 100
childDevice = addChildDevice("smartthings", "LIFX Color Bulb", device.id, null, data) childDevice = addChildDevice(app.namespace, "LIFX Color Bulb", device.id, null, data)
} else { } else {
childDevice = addChildDevice("smartthings", "LIFX White Bulb", device.id, null, data) childDevice = addChildDevice(app.namespace, "LIFX White Bulb", device.id, null, data)
} }
} }
} }
getChildDevices().findAll { !deviceIds.contains(it.deviceNetworkId) }.each { getChildDevices().findAll { !selectors.contains("${it.deviceNetworkId}") }.each {
log.info("Deleting ${it.deviceNetworkId}") log.info("Deleting ${it.deviceNetworkId}")
deleteChildDevice(it.deviceNetworkId) deleteChildDevice(it.deviceNetworkId)
} }
@@ -392,4 +401,4 @@ def refreshDevices() {
getChildDevices().each { device -> getChildDevices().each { device ->
device.refresh() device.refresh()
} }
} }