mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-09 05:11:52 +00:00
Compare commits
17 Commits
MSA-1478-1
...
PROD_2016.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e373b6f92e | ||
|
|
43a1ae6371 | ||
|
|
a441b94a33 | ||
|
|
2a58d7ff62 | ||
|
|
260917d515 | ||
|
|
c1478d3e96 | ||
|
|
8b9bff15dc | ||
|
|
75c1ede16c | ||
|
|
a7acc384a2 | ||
|
|
c6998e5f1d | ||
|
|
f95e906d6e | ||
|
|
4891e3b947 | ||
|
|
ae91f9bff5 | ||
|
|
bb87ad2cf0 | ||
|
|
5dff03fb69 | ||
|
|
dd7c6b90d5 | ||
|
|
fe2fbc3b97 |
@@ -19,7 +19,6 @@ metadata {
|
||||
|
||||
capability "Actuator"
|
||||
capability "Configuration"
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Switch"
|
||||
capability "Switch Level"
|
||||
@@ -97,14 +96,17 @@ def refresh() {
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig() + zigbee.levelConfig()
|
||||
}
|
||||
|
||||
def poll() {
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh()
|
||||
def healthPoll() {
|
||||
def cmds = zigbee.onOffRefresh() + zigbee.levelRefresh()
|
||||
cmds.each{ sendHubCommand(new physicalgraph.device.HubAction(it))}
|
||||
}
|
||||
|
||||
def configure() {
|
||||
unschedule()
|
||||
schedule("0 0/5 * * * ? *", "healthPoll")
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
// Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
|
||||
sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
|
||||
// Device-Watch allows 2 check-in misses from device
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee"])
|
||||
// minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||
zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh()
|
||||
}
|
||||
|
||||
@@ -67,6 +67,6 @@ def refresh() {
|
||||
|
||||
void poll() {
|
||||
log.debug "Executing 'poll' using parent SmartApp"
|
||||
parent.poll()
|
||||
parent.pollChild()
|
||||
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ def refresh() {
|
||||
|
||||
void poll() {
|
||||
log.debug "Executing 'poll' using parent SmartApp"
|
||||
parent.poll()
|
||||
parent.pollChild()
|
||||
}
|
||||
|
||||
def generateEvent(Map results) {
|
||||
|
||||
@@ -57,7 +57,7 @@ metadata {
|
||||
}
|
||||
|
||||
void installed() {
|
||||
sendEvent(name: "checkInterval", value: 60 * 15, data: [protocol: "lan"], displayed: false)
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, data: [protocol: "lan"], displayed: false)
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
|
||||
@@ -66,7 +66,7 @@ metadata {
|
||||
}
|
||||
|
||||
void installed() {
|
||||
sendEvent(name: "checkInterval", value: 60 * 15, data: [protocol: "lan"], displayed: false)
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, data: [protocol: "lan"], displayed: false)
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
|
||||
@@ -50,7 +50,7 @@ metadata {
|
||||
}
|
||||
|
||||
void installed() {
|
||||
sendEvent(name: "checkInterval", value: 60 * 15, data: [protocol: "lan"], displayed: false)
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, data: [protocol: "lan"], displayed: false)
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
|
||||
@@ -55,7 +55,7 @@ metadata {
|
||||
}
|
||||
|
||||
void installed() {
|
||||
sendEvent(name: "checkInterval", value: 60 * 15, data: [protocol: "lan"], displayed: false)
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, data: [protocol: "lan"], displayed: false)
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
|
||||
@@ -128,8 +128,8 @@ def refresh() {
|
||||
}
|
||||
|
||||
def configure() {
|
||||
// Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
|
||||
sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
|
||||
// Device-Watch allows 2 check-in misses from device
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee"])
|
||||
// OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||
zigbee.onOffConfig(0, 300) + powerConfig() + refresh()
|
||||
}
|
||||
|
||||
@@ -292,8 +292,8 @@ def refresh() {
|
||||
}
|
||||
|
||||
def configure() {
|
||||
// Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
|
||||
sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
|
||||
// Device-Watch allows 2 check-in misses from device
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee"])
|
||||
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
|
||||
@@ -303,8 +303,8 @@ def refresh() {
|
||||
}
|
||||
|
||||
def configure() {
|
||||
// Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
|
||||
sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
|
||||
// Device-Watch allows 2 check-in misses from device
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee"])
|
||||
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
|
||||
@@ -401,8 +401,8 @@ def refresh() {
|
||||
}
|
||||
|
||||
def configure() {
|
||||
// Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
|
||||
sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
|
||||
// Device-Watch allows 2 check-in misses from device
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee"])
|
||||
|
||||
log.debug "Configuring Reporting"
|
||||
|
||||
|
||||
@@ -255,8 +255,8 @@ def refresh() {
|
||||
}
|
||||
|
||||
def configure() {
|
||||
// Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
|
||||
sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
|
||||
// Device-Watch allows 2 check-in misses from device
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee"])
|
||||
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
|
||||
@@ -264,8 +264,8 @@ def refresh()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
// Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
|
||||
sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
|
||||
// Device-Watch allows 2 check-in misses from device
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee"])
|
||||
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
def humidityConfigCmds = [
|
||||
|
||||
@@ -89,8 +89,8 @@ def refresh() {
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
// Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
|
||||
sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
|
||||
// Device-Watch allows 2 check-in misses from device
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee"])
|
||||
// OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||
zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh()
|
||||
}
|
||||
|
||||
@@ -133,8 +133,8 @@ def refresh() {
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
// Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
|
||||
sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
|
||||
// Device-Watch allows 2 check-in misses from device
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee"])
|
||||
// OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||
zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01) + zigbee.readAttribute(0x0006, 0x00) + zigbee.readAttribute(0x0008, 0x00) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, 0x00) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||
}
|
||||
|
||||
@@ -113,8 +113,8 @@ def refresh() {
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
// Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
|
||||
sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
|
||||
// Device-Watch allows 2 check-in misses from device
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee"])
|
||||
// OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||
zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh()
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ mappings {
|
||||
path("/receivedToken") { action: [ POST: "receivedToken", GET: "receivedToken"] }
|
||||
path("/receiveToken") { action: [ POST: "receiveToken", GET: "receiveToken"] }
|
||||
path("/hookCallback") { action: [ POST: "hookEventHandler", GET: "hookEventHandler"] }
|
||||
path("/oauth/initialize") {action: [GET: "oauthInitUrl"]}
|
||||
path("/oauth/initialize") {action: [GET: "oauthInitUrl"]}
|
||||
path("/oauth/callback") { action: [ GET: "callback" ] }
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ def callback() {
|
||||
} else {
|
||||
log.warn "No authQueryString"
|
||||
}
|
||||
|
||||
|
||||
if (state.JawboneAccessToken) {
|
||||
log.debug "Access token already exists"
|
||||
setup()
|
||||
@@ -73,7 +73,7 @@ def callback() {
|
||||
|
||||
def authPage() {
|
||||
log.debug "authPage"
|
||||
def description = null
|
||||
def description = null
|
||||
if (state.JawboneAccessToken == null) {
|
||||
if (!state.accessToken) {
|
||||
log.debug "About to create access token"
|
||||
@@ -82,12 +82,13 @@ def authPage() {
|
||||
description = "Click to enter Jawbone Credentials"
|
||||
def redirectUrl = buildRedirectUrl
|
||||
log.debug "RedirectURL = ${redirectUrl}"
|
||||
def donebutton= state.JawboneAccessToken != null
|
||||
def donebutton= state.JawboneAccessToken != null
|
||||
return dynamicPage(name: "Credentials", title: "Jawbone UP", nextPage: null, uninstall: true, install: donebutton) {
|
||||
section { paragraph title: "Note:", "This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance." }
|
||||
section { href url:redirectUrl, style:"embedded", required:true, title:"Jawbone UP", state: hast ,description:description }
|
||||
}
|
||||
} else {
|
||||
description = "Jawbone Credentials Already Entered."
|
||||
description = "Jawbone Credentials Already Entered."
|
||||
return dynamicPage(name: "Credentials", title: "Jawbone UP", uninstall: true, install:true) {
|
||||
section { href url: buildRedirectUrl("receivedToken"), style:"embedded", state: "complete", title:"Jawbone UP", description:description }
|
||||
}
|
||||
@@ -107,7 +108,7 @@ def receiveToken(redirectUrl = null) {
|
||||
def params = [
|
||||
uri: "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}",
|
||||
]
|
||||
httpGet(params) { response ->
|
||||
httpGet(params) { response ->
|
||||
log.debug "${response.data}"
|
||||
log.debug "Setting access token to ${response.data.access_token}, refresh token to ${response.data.refresh_token}"
|
||||
state.JawboneAccessToken = response.data.access_token
|
||||
@@ -149,7 +150,7 @@ def connectionStatus(message, redirectUrl = null) {
|
||||
<meta http-equiv="refresh" content="3; url=${redirectUrl}" />
|
||||
"""
|
||||
}
|
||||
|
||||
|
||||
def html = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@@ -229,12 +230,12 @@ def validateCurrentToken() {
|
||||
log.debug "validateCurrentToken"
|
||||
def url = "https://jawbone.com/nudge/api/v.1.1/users/@me/refreshToken"
|
||||
def requestBody = "secret=${appSettings.clientSecret}"
|
||||
|
||||
|
||||
try {
|
||||
httpPost(uri: url, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ], body: requestBody) {response ->
|
||||
if (response.status == 200) {
|
||||
log.debug "${response.data}"
|
||||
log.debug "Setting refresh token to ${response.data.data.refresh_token}"
|
||||
log.debug "Setting refresh token"
|
||||
state.refreshToken = response.data.data.refresh_token
|
||||
}
|
||||
}
|
||||
@@ -258,7 +259,7 @@ def validateCurrentToken() {
|
||||
state.remove("refreshToken")
|
||||
}
|
||||
} else {
|
||||
log.debug "Setting access token to ${data.access_token}, refresh token to ${data.refresh_token}"
|
||||
log.debug "Setting access token"
|
||||
state.JawboneAccessToken = data.access_token
|
||||
state.refreshToken = data.refresh_token
|
||||
}
|
||||
@@ -271,10 +272,10 @@ def validateCurrentToken() {
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
log.debug "Callback URL - Webhook"
|
||||
def localServerUrl = getApiServerUrl()
|
||||
log.debug "Callback URL - Webhook"
|
||||
def localServerUrl = getApiServerUrl()
|
||||
def hookUrl = "${localServerUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/hookCallback"
|
||||
def webhook = "https://jawbone.com/nudge/api/v.1.1/users/@me/pubsub?webhook=$hookUrl"
|
||||
def webhook = "https://jawbone.com/nudge/api/v.1.1/users/@me/pubsub?webhook=$hookUrl"
|
||||
httpPost(uri: webhook, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ])
|
||||
}
|
||||
|
||||
@@ -284,16 +285,16 @@ def setup() {
|
||||
|
||||
if (state.JawboneAccessToken) {
|
||||
def urlmember = "https://jawbone.com/nudge/api/users/@me/"
|
||||
def member = null
|
||||
httpGet(uri: urlmember, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
||||
def member = null
|
||||
httpGet(uri: urlmember, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
||||
member = response.data.data
|
||||
}
|
||||
|
||||
|
||||
if (member) {
|
||||
state.member = member
|
||||
def externalId = "${app.id}.${member.xid}"
|
||||
|
||||
// find the appropriate child device based on my app id and the device network id
|
||||
// find the appropriate child device based on my app id and the device network id
|
||||
def deviceWrapper = getChildDevice("${externalId}")
|
||||
|
||||
// invoke the generatePresenceEvent method on the child device
|
||||
@@ -312,7 +313,7 @@ def setup() {
|
||||
}
|
||||
|
||||
def installed() {
|
||||
|
||||
|
||||
if (!state.accessToken) {
|
||||
log.debug "About to create access token"
|
||||
createAccessToken()
|
||||
@@ -324,7 +325,7 @@ def installed() {
|
||||
}
|
||||
|
||||
def updated() {
|
||||
|
||||
|
||||
if (!state.accessToken) {
|
||||
log.debug "About to create access token"
|
||||
createAccessToken()
|
||||
@@ -348,29 +349,29 @@ def uninstalled() {
|
||||
}
|
||||
|
||||
def pollChild(childDevice) {
|
||||
def member = state.member
|
||||
generatePollingEvents (member, childDevice)
|
||||
def member = state.member
|
||||
generatePollingEvents (member, childDevice)
|
||||
}
|
||||
|
||||
def generatePollingEvents (member, childDevice) {
|
||||
// lets figure out if the member is currently "home" (At the place)
|
||||
def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals"
|
||||
def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves"
|
||||
def urlsleeps = "https://jawbone.com/nudge/api/users/@me/sleeps"
|
||||
def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals"
|
||||
def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves"
|
||||
def urlsleeps = "https://jawbone.com/nudge/api/users/@me/sleeps"
|
||||
def goals = null
|
||||
def moves = null
|
||||
def sleeps = null
|
||||
httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
||||
def sleeps = null
|
||||
httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
||||
goals = response.data.data
|
||||
}
|
||||
httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
||||
}
|
||||
httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
||||
moves = response.data.data.items[0]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
try { // we are going to just ignore any errors
|
||||
log.debug "Member = ${member.first}"
|
||||
log.debug "Moves Goal = ${goals.move_steps} Steps"
|
||||
log.debug "Moves = ${moves.details.steps} Steps"
|
||||
log.debug "Moves = ${moves.details.steps} Steps"
|
||||
|
||||
childDevice?.sendEvent(name:"steps", value: moves.details.steps)
|
||||
childDevice?.sendEvent(name:"goal", value: goals.move_steps)
|
||||
@@ -378,29 +379,29 @@ def generatePollingEvents (member, childDevice) {
|
||||
}
|
||||
catch (e) {
|
||||
// eat it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def generateInitialEvent (member, childDevice) {
|
||||
// lets figure out if the member is currently "home" (At the place)
|
||||
def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals"
|
||||
def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves"
|
||||
def urlsleeps = "https://jawbone.com/nudge/api/users/@me/sleeps"
|
||||
def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals"
|
||||
def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves"
|
||||
def urlsleeps = "https://jawbone.com/nudge/api/users/@me/sleeps"
|
||||
def goals = null
|
||||
def moves = null
|
||||
def sleeps = null
|
||||
httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
||||
def sleeps = null
|
||||
httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
||||
goals = response.data.data
|
||||
}
|
||||
httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
||||
}
|
||||
httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
||||
moves = response.data.data.items[0]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
try { // we are going to just ignore any errors
|
||||
log.debug "Member = ${member.first}"
|
||||
log.debug "Moves Goal = ${goals.move_steps} Steps"
|
||||
log.debug "Moves = ${moves.details.steps} Steps"
|
||||
log.debug "Sleeping state = false"
|
||||
log.debug "Sleeping state = false"
|
||||
childDevice?.generateSleepingEvent(false)
|
||||
childDevice?.sendEvent(name:"steps", value: moves.details.steps)
|
||||
childDevice?.sendEvent(name:"goal", value: goals.move_steps)
|
||||
@@ -408,27 +409,27 @@ def generateInitialEvent (member, childDevice) {
|
||||
}
|
||||
catch (e) {
|
||||
// eat it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def setColor (steps,goal,childDevice) {
|
||||
def result = steps * 100 / goal
|
||||
if (result < 25)
|
||||
if (result < 25)
|
||||
childDevice?.sendEvent(name:"steps", value: "steps", label: steps)
|
||||
else if ((result >= 25) && (result < 50))
|
||||
else if ((result >= 25) && (result < 50))
|
||||
childDevice?.sendEvent(name:"steps", value: "steps1", label: steps)
|
||||
else if ((result >= 50) && (result < 75))
|
||||
else if ((result >= 50) && (result < 75))
|
||||
childDevice?.sendEvent(name:"steps", value: "steps1", label: steps)
|
||||
else if (result >= 75)
|
||||
childDevice?.sendEvent(name:"steps", value: "stepsgoal", label: steps)
|
||||
else if (result >= 75)
|
||||
childDevice?.sendEvent(name:"steps", value: "stepsgoal", label: steps)
|
||||
}
|
||||
|
||||
def hookEventHandler() {
|
||||
// log.debug "In hookEventHandler method."
|
||||
log.debug "request = ${request}"
|
||||
|
||||
def json = request.JSON
|
||||
|
||||
|
||||
def json = request.JSON
|
||||
|
||||
// get some stuff we need
|
||||
def userId = json.events.user_xid[0]
|
||||
def json_type = json.events.type[0]
|
||||
@@ -437,39 +438,39 @@ def hookEventHandler() {
|
||||
//log.debug json
|
||||
log.debug "Userid = ${userId}"
|
||||
log.debug "Notification Type: " + json_type
|
||||
log.debug "Notification Action: " + json_action
|
||||
|
||||
log.debug "Notification Action: " + json_action
|
||||
|
||||
// find the appropriate child device based on my app id and the device network id
|
||||
def externalId = "${app.id}.${userId}"
|
||||
def childDevice = getChildDevice("${externalId}")
|
||||
|
||||
|
||||
if (childDevice) {
|
||||
switch (json_action) {
|
||||
case "enter_sleep_mode":
|
||||
childDevice?.generateSleepingEvent(true)
|
||||
break
|
||||
case "exit_sleep_mode":
|
||||
childDevice?.generateSleepingEvent(false)
|
||||
break
|
||||
case "creation":
|
||||
switch (json_action) {
|
||||
case "enter_sleep_mode":
|
||||
childDevice?.generateSleepingEvent(true)
|
||||
break
|
||||
case "exit_sleep_mode":
|
||||
childDevice?.generateSleepingEvent(false)
|
||||
break
|
||||
case "creation":
|
||||
childDevice?.sendEvent(name:"steps", value: 0)
|
||||
break
|
||||
case "updation":
|
||||
def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals"
|
||||
def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves"
|
||||
def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals"
|
||||
def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves"
|
||||
def goals = null
|
||||
def moves = null
|
||||
httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
||||
def moves = null
|
||||
httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
||||
goals = response.data.data
|
||||
}
|
||||
httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
||||
}
|
||||
httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
||||
moves = response.data.data.items[0]
|
||||
}
|
||||
}
|
||||
log.debug "Goal = ${goals.move_steps} Steps"
|
||||
log.debug "Steps = ${moves.details.steps} Steps"
|
||||
log.debug "Steps = ${moves.details.steps} Steps"
|
||||
childDevice?.sendEvent(name:"steps", value: moves.details.steps)
|
||||
childDevice?.sendEvent(name:"goal", value: goals.move_steps)
|
||||
//setColor(moves.details.steps,goals.move_steps,childDevice)
|
||||
childDevice?.sendEvent(name:"goal", value: goals.move_steps)
|
||||
//setColor(moves.details.steps,goals.move_steps,childDevice)
|
||||
break
|
||||
case "deletion":
|
||||
app.delete()
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
* JLH - 02-15-2014 - Fuller use of ecobee API
|
||||
* 10-28-2015 DVCSMP-604 - accessory sensor, DVCSMP-1174, DVCSMP-1111 - not respond to routines
|
||||
*/
|
||||
include 'asynchttp_v1'
|
||||
|
||||
definition(
|
||||
name: "Ecobee (Connect)",
|
||||
namespace: "smartthings",
|
||||
@@ -246,7 +244,9 @@ def getEcobeeThermostats() {
|
||||
uri: apiEndpoint,
|
||||
path: "/1/thermostat",
|
||||
headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"],
|
||||
query: [json: toJson(bodyParams)]
|
||||
// TODO - the query string below is not consistent with the Ecobee docs:
|
||||
// https://www.ecobee.com/home/developer/api/documentation/v1/operations/get-thermostats.shtml
|
||||
query: [format: 'json', body: toJson(bodyParams)]
|
||||
]
|
||||
|
||||
def stats = [:]
|
||||
@@ -265,8 +265,9 @@ def getEcobeeThermostats() {
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
log.trace "Exception polling children: " + e.response.data.status
|
||||
if (e.response.data.status.code == 14) {
|
||||
atomicState.action = "getEcobeeThermostats"
|
||||
log.debug "Refreshing your auth_token!"
|
||||
refreshAuthToken([async: false, nextAction: "getEcobeeThermostats"])
|
||||
refreshAuthToken()
|
||||
}
|
||||
}
|
||||
atomicState.thermostats = stats
|
||||
@@ -357,22 +358,16 @@ def initialize() {
|
||||
atomicState.timeSendPush = null
|
||||
atomicState.reAttempt = 0
|
||||
|
||||
initialPoll() //first time polling data data from thermostat
|
||||
pollHandler() //first time polling data data from thermostat
|
||||
|
||||
//automatically update devices status every 5 mins
|
||||
runEvery5Minutes("poll")
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Polls the child devices (synchronously).
|
||||
* This is used during app install/update, and is synchronous
|
||||
* to maintain current behavior that will cause install/update to fail
|
||||
* if polling fails.
|
||||
*/
|
||||
def initialPoll() {
|
||||
log.debug "initialPoll()"
|
||||
pollChildrenSync() // Hit the ecobee API for update on all thermostats
|
||||
def pollHandler() {
|
||||
log.debug "pollHandler()"
|
||||
pollChildren(null) // Hit the ecobee API for update on all thermostats
|
||||
|
||||
atomicState.thermostats.each {stat ->
|
||||
def dni = stat.key
|
||||
@@ -385,101 +380,10 @@ def initialPoll() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Polls Ecobee (asynchronously) for updated device state data.
|
||||
* Called from within this Connect SmartApp as well as the child
|
||||
* devices.
|
||||
*/
|
||||
def poll() {
|
||||
log.debug "polling asynchronously"
|
||||
asynchttp_v1.get('asyncPollResponseHandler', getPollParams())
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a (synchronous) request to the Ecobee API to get the data for the thermostats.
|
||||
* This request is made synchronously here because it is called as part of the
|
||||
* install/updated lifecycle, and changing it to asynchronous during the install/update
|
||||
* lifecycle may change the behavior if there is an error in polling.
|
||||
*
|
||||
* If further analysis shows that polling can be done asynchronously during
|
||||
* install/update without any adverse consequences, this should then be made
|
||||
* asynchronous just as the scheduled polling is.
|
||||
*/
|
||||
def pollChildrenSync() {
|
||||
def pollChildren(child = null) {
|
||||
def thermostatIdsString = getChildDeviceIdsString()
|
||||
log.debug "polling children: $thermostatIdsString"
|
||||
|
||||
def params = getPollParams()
|
||||
params.query << ["Content-Type": "application/json"]
|
||||
|
||||
def result = false
|
||||
log.debug "making synchronous poll request"
|
||||
|
||||
try{
|
||||
httpGet(params) { resp ->
|
||||
if(resp.status == 200) {
|
||||
atomicState.remoteSensors = resp.data.thermostatList.remoteSensors
|
||||
updateSensorData()
|
||||
storeThermostatData(resp.data.thermostatList)
|
||||
result = true
|
||||
log.debug "updated ${atomicState.thermostats?.size()} stats: ${atomicState.thermostats}"
|
||||
}
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
log.trace "Exception polling children: " + e.response.data.status
|
||||
if (e.response.data.status.code == 14) {
|
||||
log.debug "Refreshing your auth_token!"
|
||||
refreshAuthToken([async: false, nextAction: "pollChildrenSync"])
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Response handler for asynchronous request to get thermostat data.
|
||||
* Given a successful response, updates the sensor data, stores the thermostat
|
||||
* data, and generates child device events.
|
||||
*
|
||||
* If the access token has expired, will issue a request to refresh the token
|
||||
* (and pending successful token refresh, the poll request will be made again).
|
||||
*/
|
||||
def asyncPollResponseHandler(response, data) {
|
||||
log.trace "async poll response handler"
|
||||
if (!response.hasError()) {
|
||||
if (response.status == 200) {
|
||||
def json
|
||||
try {
|
||||
json = response.getJson()
|
||||
} catch (e) {
|
||||
log.error ("error parsing JSON", e)
|
||||
}
|
||||
if (json) {
|
||||
atomicState.remoteSensors = json.thermostatList.remoteSensors
|
||||
updateSensorData()
|
||||
storeThermostatData(json.thermostatList)
|
||||
generateChildThermostatEvent()
|
||||
}
|
||||
} else {
|
||||
log.warn "Response returned non-200 response. Status: ${response.status}, data: ${response.getData()}"
|
||||
}
|
||||
} else {
|
||||
log.trace "Exception polling children: ${response.getErrorMessage()}"
|
||||
def errorJson
|
||||
try {
|
||||
errorJson = response.getErrorJson()
|
||||
} catch (e) {
|
||||
log.error("Unable to parse error json response", e)
|
||||
}
|
||||
if (errorJson?.status?.code == 14) {
|
||||
log.debug "Refreshing your auth_token!"
|
||||
refreshAuthToken([async: true, nextAction: "poll"])
|
||||
} else {
|
||||
log.warn "Error polling children that is not due to an expired token. Response: ${response.getErrorData()}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getPollParams() {
|
||||
def thermostatIdsString = getChildDeviceIdsString()
|
||||
def requestBody = [
|
||||
selection: [
|
||||
selectionType: "thermostats",
|
||||
@@ -490,32 +394,66 @@ private getPollParams() {
|
||||
includeSensors: true
|
||||
]
|
||||
]
|
||||
return [
|
||||
|
||||
def result = false
|
||||
|
||||
def pollParams = [
|
||||
uri: apiEndpoint,
|
||||
path: "/1/thermostat",
|
||||
headers: ["Authorization": "Bearer ${atomicState.authToken}"],
|
||||
query: [json: toJson(requestBody)]
|
||||
headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"],
|
||||
// TODO - the query string below is not consistent with the Ecobee docs:
|
||||
// https://www.ecobee.com/home/developer/api/documentation/v1/operations/get-thermostats.shtml
|
||||
query: [format: 'json', body: toJson(requestBody)]
|
||||
]
|
||||
|
||||
try{
|
||||
httpGet(pollParams) { resp ->
|
||||
if(resp.status == 200) {
|
||||
log.debug "poll results returned resp.data ${resp.data}"
|
||||
atomicState.remoteSensors = resp.data.thermostatList.remoteSensors
|
||||
updateSensorData()
|
||||
storeThermostatData(resp.data.thermostatList)
|
||||
result = true
|
||||
log.debug "updated ${atomicState.thermostats?.size()} stats: ${atomicState.thermostats}"
|
||||
}
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
log.trace "Exception polling children: " + e.response.data.status
|
||||
if (e.response.data.status.code == 14) {
|
||||
atomicState.action = "pollChildren"
|
||||
log.debug "Refreshing your auth_token!"
|
||||
refreshAuthToken()
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls each child thermostat device to generate an event with the thermostat
|
||||
* data.
|
||||
*/
|
||||
def generateChildThermostatEvent() {
|
||||
log.trace("generateChildThermostatEvent")
|
||||
getChildDevices().each { child ->
|
||||
if (!child.device.deviceNetworkId.startsWith("ecobee_sensor")){
|
||||
if(atomicState.thermostats[child.device.deviceNetworkId] != null) {
|
||||
def tData = atomicState.thermostats[child.device.deviceNetworkId]
|
||||
log.debug "calling child.generateEvent($tData.data)"
|
||||
child.generateEvent(tData.data) //parse received message from parent
|
||||
} else if(atomicState.thermostats[child.device.deviceNetworkId] == null) {
|
||||
log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId}"
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
// Poll Child is invoked from the Child Device itself as part of the Poll Capability
|
||||
def pollChild() {
|
||||
def devices = getChildDevices()
|
||||
|
||||
if (pollChildren()) {
|
||||
devices.each { child ->
|
||||
if (!child.device.deviceNetworkId.startsWith("ecobee_sensor")) {
|
||||
if(atomicState.thermostats[child.device.deviceNetworkId] != null) {
|
||||
def tData = atomicState.thermostats[child.device.deviceNetworkId]
|
||||
log.info "pollChild(child)>> data for ${child.device.deviceNetworkId} : ${tData.data}"
|
||||
child.generateEvent(tData.data) //parse received message from parent
|
||||
} else if(atomicState.thermostats[child.device.deviceNetworkId] == null) {
|
||||
log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId}"
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.info "ERROR: pollChildren()"
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void poll() {
|
||||
pollChild()
|
||||
}
|
||||
|
||||
def availableModes(child) {
|
||||
@@ -615,104 +553,47 @@ def toQueryString(Map m) {
|
||||
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the refresh token to get a new access token, then executes the nextAction.
|
||||
* @param options - a map of options. valid options are async: true/false, which
|
||||
* specifies if the refresh token request will be done asynchronously or not (default is false)
|
||||
* nextAction: "nameOfMethod" specifies what method to execute after
|
||||
* the token is refreshed (not required).
|
||||
* (note: using a map as the parameter because we need to call it from a schedueled
|
||||
* execution and we can only pass a data map to scheduled executions)
|
||||
*/
|
||||
private void refreshAuthToken(options) {
|
||||
if(!atomicState.refreshToken) {
|
||||
log.warn "Cannot not refresh OAuth token since there is no refreshToken stored"
|
||||
} else {
|
||||
def refreshParams = [
|
||||
uri : apiEndpoint,
|
||||
path : "/token",
|
||||
query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: smartThingsClientId],
|
||||
]
|
||||
if (options.async) {
|
||||
refreshAuthTokenAsync(refreshParams, options.nextAction)
|
||||
} else {
|
||||
refreshAuthTokenSync(refreshParams, options.nextAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
private refreshAuthToken() {
|
||||
log.debug "refreshing auth token"
|
||||
|
||||
private void refreshAuthTokenSync(params, nextAction = null) {
|
||||
try {
|
||||
httpPost(refreshParams) { resp ->
|
||||
if(resp.status == 200) {
|
||||
log.debug "Token refreshed...calling saved RestAction now!"
|
||||
debugEvent("Token refreshed ... calling saved RestAction now!")
|
||||
saveTokenAndResumeAction(resp.data, nextAction)
|
||||
if(!atomicState.refreshToken) {
|
||||
log.warn "Can not refresh OAuth token since there is no refreshToken stored"
|
||||
} else {
|
||||
def refreshParams = [
|
||||
method: 'POST',
|
||||
uri : apiEndpoint,
|
||||
path : "/token",
|
||||
query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: smartThingsClientId],
|
||||
]
|
||||
|
||||
def notificationMessage = "is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials."
|
||||
//changed to httpPost
|
||||
try {
|
||||
def jsonMap
|
||||
httpPost(refreshParams) { resp ->
|
||||
if(resp.status == 200) {
|
||||
log.debug "Token refreshed...calling saved RestAction now!"
|
||||
debugEvent("Token refreshed ... calling saved RestAction now!")
|
||||
saveTokenAndResumeAction(resp.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}"
|
||||
reauthTokenErrorHandler(e.statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshAuthTokenAsync(refreshParams, nextAction = null) {
|
||||
log.debug "making asynchronous refresh request"
|
||||
asynchttp_v1.post('refreshTokenResponseHandler', refreshParams, [nextAction: nextAction])
|
||||
}
|
||||
|
||||
/**
|
||||
* The response handler for the request to refresh the authorization handler.
|
||||
* Stores the new authorization token and refresh token, and executes any action
|
||||
* (method) that failed due to the authorization token expiring.
|
||||
*/
|
||||
private void refreshTokenResponseHandler(response, data) {
|
||||
if (!response.hasError()) {
|
||||
if (response.status == 200) {
|
||||
def json
|
||||
try {
|
||||
json = response.getJson()
|
||||
} catch (e) {
|
||||
log.error "error parsing json from response data: $response.data"
|
||||
}
|
||||
if (json) {
|
||||
log.debug "asnyc refreshTokenHandler: Token refreshed...calling saved RestAction now!"
|
||||
debugEvent("async Token refreshed ... calling saved RestAction now!")
|
||||
saveTokenAndResumeAction(json, data.nextAction)
|
||||
} else {
|
||||
log.warn "successfully parsed json but result is empty or null"
|
||||
}
|
||||
} else {
|
||||
log.debug "Non 200 response returned. Response code: ${response.code}, data: ${response.getData()}"
|
||||
}
|
||||
} else {
|
||||
log.debug "async refreshTokenHandler: RESPONSE ERROR: ${response.getErrorJson()}"
|
||||
reauthTokenErrorHandler(response.getErrorJson().code)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retries refreshing the authorization token. Will attempt to get the refresh
|
||||
* token later, in case there were errors retrieving it.
|
||||
* Will retry a fixed number of times before sending a push notification to the
|
||||
* user instructing them to reauthenticate
|
||||
*/
|
||||
private void reauthTokenErrorHandler(responseCode) {
|
||||
def retryInterval = 300 // in seconds
|
||||
def notificationMessage = "is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials."
|
||||
// might get non-401 error from exceeding 20 second app limit, connectivity issues, etc.
|
||||
if (responseCode != 401) {
|
||||
runIn(retryInterval, "refreshAuthToken", [async: true])
|
||||
} else if (responseCode == 401) { // unauthorized
|
||||
atomicState.reAttempt = atomicState.reAttempt + 1
|
||||
log.warn "reAttempt refreshAuthToken to try = ${atomicState.reAttempt}"
|
||||
if (atomicState.reAttempt <= 3) {
|
||||
runIn(retryInterval, "refreshAuthToken", [async: true])
|
||||
} else {
|
||||
sendPushAndFeeds(notificationMessage)
|
||||
atomicState.reAttempt = 0
|
||||
}
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}"
|
||||
def reAttemptPeriod = 300 // in sec
|
||||
if (e.statusCode != 401) { // this issue might comes from exceed 20sec app execution, connectivity issue etc.
|
||||
runIn(reAttemptPeriod, "refreshAuthToken")
|
||||
} else if (e.statusCode == 401) { // unauthorized
|
||||
atomicState.reAttempt = atomicState.reAttempt + 1
|
||||
log.warn "reAttempt refreshAuthToken to try = ${atomicState.reAttempt}"
|
||||
if (atomicState.reAttempt <= 3) {
|
||||
runIn(reAttemptPeriod, "refreshAuthToken")
|
||||
} else {
|
||||
sendPushAndFeeds(notificationMessage)
|
||||
atomicState.reAttempt = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -722,20 +603,20 @@ private void reauthTokenErrorHandler(responseCode) {
|
||||
*
|
||||
* @param json - an object representing the parsed JSON response from Ecobee
|
||||
*/
|
||||
private void saveTokenAndResumeAction(json, String nextAction) {
|
||||
def debugMessage = "token response, scope: ${json?.scope}, expires_in: ${json?.expires_in}, token_type: ${json?.token_type}"
|
||||
log.debug "debugMessage"
|
||||
private void saveTokenAndResumeAction(json) {
|
||||
log.debug "token response json: $json"
|
||||
if (json) {
|
||||
debugEvent(debugMessage)
|
||||
debugEvent("Response = $json")
|
||||
atomicState.refreshToken = json?.refresh_token
|
||||
atomicState.authToken = json?.access_token
|
||||
if (nextAction) {
|
||||
log.debug "got refresh token, will execute next action (passed in!): $nextAction"
|
||||
"$nextAction"()
|
||||
if (atomicState.action) {
|
||||
log.debug "got refresh token, executing next action: ${atomicState.action}"
|
||||
"${atomicState.action}"()
|
||||
}
|
||||
} else {
|
||||
log.warn "did not get response body from refresh token response"
|
||||
}
|
||||
atomicState.action = ""
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -875,6 +756,7 @@ private boolean sendCommandToEcobee(Map bodyParams) {
|
||||
try{
|
||||
httpPost(cmdParams) { resp ->
|
||||
if(resp.status == 200) {
|
||||
log.debug "updated ${resp.data}"
|
||||
def returnStatus = resp.data.status.code
|
||||
if (returnStatus == 0) {
|
||||
log.debug "Successful call to ecobee API."
|
||||
@@ -889,10 +771,11 @@ private boolean sendCommandToEcobee(Map bodyParams) {
|
||||
log.trace "Exception Sending Json: " + e.response.data.status
|
||||
debugEvent ("sent Json & got http status ${e.statusCode} - ${e.response.data.status.code}")
|
||||
if (e.response.data.status.code == 14) {
|
||||
// TODO - figure out why we're setting the next action to be poll
|
||||
// TODO - figure out why we're setting the next action to be pollChildren
|
||||
// after refreshing auth token. Is it to keep UI in sync, or just copy/paste error?
|
||||
atomicState.action = "pollChildren"
|
||||
log.debug "Refreshing your auth_token!"
|
||||
refreshAuthToken([async: true, nextAction: "poll"])
|
||||
refreshAuthToken()
|
||||
} else {
|
||||
debugEvent("Authentication error, invalid authentication method, lack of credentials, etc.")
|
||||
log.error "Authentication error, invalid authentication method, lack of credentials, etc."
|
||||
|
||||
@@ -801,10 +801,12 @@ def parse(childDevice, description) {
|
||||
}
|
||||
|
||||
// Philips Hue priority for color is xy > ct > hs
|
||||
// For SmartThings, try to always send hue, sat and hex
|
||||
private sendColorEvents(device, xy, hue, sat, ct, colormode = null) {
|
||||
if (device == null || (xy == null && hue == null && sat == null && ct == null))
|
||||
return
|
||||
|
||||
def events = [:]
|
||||
// For now, only care about changing color temperature if requested by user
|
||||
if (ct != null && (colormode == "ct" || (xy == null && hue == null && sat == null))) {
|
||||
// for some reason setting Hue to their specified minimum off 153 yields 154, dealt with below
|
||||
@@ -818,13 +820,13 @@ private sendColorEvents(device, xy, hue, sat, ct, colormode = null) {
|
||||
if (hue != null) {
|
||||
// 0-65535
|
||||
def value = Math.min(Math.round(hue * 100 / 65535), 65535) as int
|
||||
device.sendEvent([name: "hue", value: value, descriptionText: "Color has changed"])
|
||||
events["hue"] = [name: "hue", value: value, descriptionText: "Color has changed", displayed: false]
|
||||
}
|
||||
|
||||
if (sat != null) {
|
||||
// 0-254
|
||||
def value = Math.round(sat * 100 / 254) as int
|
||||
device.sendEvent([name: "saturation", value: value, descriptionText: "Color has changed"])
|
||||
events["saturation"] = [name: "saturation", value: value, descriptionText: "Color has changed", displayed: false]
|
||||
}
|
||||
|
||||
// Following is used to decide what to base hex calculations on since it is preferred to return a colorchange in hex
|
||||
@@ -836,17 +838,28 @@ private sendColorEvents(device, xy, hue, sat, ct, colormode = null) {
|
||||
def model = state.bulbs[id]?.modelid
|
||||
def hex = colorFromXY(xy, model)
|
||||
|
||||
// TODO Disabled until a solution for the jumping color picker can be figured out
|
||||
//device.sendEvent([name: "color", value: hex.toUpperCase(), descriptionText: "Color has changed", displayed: false])
|
||||
// Create Hue and Saturation events if not previously existing
|
||||
def hsv = hexToHsv(hex)
|
||||
if (events["hue"] == null)
|
||||
events["hue"] = [name: "hue", value: hsv[0], descriptionText: "Color has changed", displayed: false]
|
||||
if (events["saturation"] == null)
|
||||
events["saturation"] = [name: "saturation", value: hsv[1], descriptionText: "Color has changed", displayed: false]
|
||||
|
||||
events["color"] = [name: "color", value: hex.toUpperCase(), descriptionText: "Color has changed", displayed: true]
|
||||
} else if (colormode == "hs" || colormode == null) {
|
||||
// colormode is "hs" or "xy" is missing, default to follow hue/sat which is already handled above
|
||||
def hueValue = (hue != null) ? events["hue"].value : Integer.parseInt("$device.currentHue")
|
||||
def satValue = (sat != null) ? events["saturation"].value : Integer.parseInt("$device.currentSaturation")
|
||||
|
||||
// TODO Disabled until the standard behavior of lights is defined (hue and sat events are sent above)
|
||||
//def hex = colorUtil.hslToHex((int) device.currentHue, (int) device.currentSaturation)
|
||||
// device.sendEvent([name: "color", value: hex.toUpperCase(), descriptionText: "Color has changed"])
|
||||
|
||||
def hex = hsvToHex(hueValue, satValue)
|
||||
events["color"] = [name: "color", value: hex.toUpperCase(), descriptionText: "Color has changed", displayed: true]
|
||||
}
|
||||
|
||||
return debug
|
||||
boolean sendColorChanged = false
|
||||
events.each {
|
||||
device.sendEvent(it.value)
|
||||
}
|
||||
}
|
||||
|
||||
private sendBasicEvents(device, param, value) {
|
||||
@@ -887,8 +900,6 @@ private handleCommandResponse(body) {
|
||||
def updates = [:]
|
||||
|
||||
body.each { payload ->
|
||||
log.debug $payload
|
||||
|
||||
if (payload?.success) {
|
||||
def childDeviceNetworkId = app.id + "/"
|
||||
def eventType
|
||||
@@ -1101,26 +1112,22 @@ def setColor(childDevice, huesettings) {
|
||||
def sat = null
|
||||
def xy = null
|
||||
|
||||
// For now ignore model to get a consistent color if same color is set across multiple devices
|
||||
// def model = state.bulbs[getId(childDevice)]?.modelid
|
||||
if (huesettings.hex != null) {
|
||||
// Prefer hue/sat over hex to make sure it works with the majority of the smartapps
|
||||
if (huesettings.hue != null || huesettings.sat != null) {
|
||||
// If both hex and hue/sat are set, send all values to bridge to get hue/sat in response from bridge to
|
||||
// generate hue/sat events even though bridge will prioritize XY when setting color
|
||||
if (huesettings.hue != null)
|
||||
value.hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535)
|
||||
if (huesettings.saturation != null)
|
||||
value.sat = Math.min(Math.round(huesettings.saturation * 254 / 100), 254)
|
||||
} else if (huesettings.hex != null && false) {
|
||||
// For now ignore model to get a consistent color if same color is set across multiple devices
|
||||
// def model = state.bulbs[getId(childDevice)]?.modelid
|
||||
// value.xy = calculateXY(huesettings.hex, model)
|
||||
// Once groups, or scenes are introduced it might be a good idea to use unique models again
|
||||
value.xy = calculateXY(huesettings.hex)
|
||||
}
|
||||
|
||||
// If both hex and hue/sat are set, send all values to bridge to get hue/sat in response from bridge to
|
||||
// generate hue/sat events even though bridge will prioritize XY when setting color
|
||||
if (huesettings.hue != null)
|
||||
value.hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535)
|
||||
else
|
||||
value.hue = Math.min(Math.round(childDevice.device?.currentValue("hue") * 65535 / 100), 65535)
|
||||
|
||||
if (huesettings.saturation != null)
|
||||
value.sat = Math.min(Math.round(huesettings.saturation * 254 / 100), 254)
|
||||
else
|
||||
value.sat = Math.min(Math.round(childDevice.device?.currentValue("saturation") * 254 / 100), 254)
|
||||
|
||||
/* Disabled for now due to bad behavior via Lightning Wizard
|
||||
if (!value.xy) {
|
||||
// Below will translate values to hex->XY to take into account the color support of the different hue types
|
||||
@@ -1175,9 +1182,8 @@ private poll() {
|
||||
def host = getBridgeIP()
|
||||
def uri = "/api/${state.username}/lights/"
|
||||
log.debug "GET: $host$uri"
|
||||
sendHubCommand(new physicalgraph.device.HubAction("""GET ${uri} HTTP/1.1
|
||||
HOST: ${host}
|
||||
""", physicalgraph.device.Protocol.LAN, selectedHue))
|
||||
sendHubCommand(new physicalgraph.device.HubAction("GET ${uri} HTTP/1.1\r\n" +
|
||||
"HOST: ${host}\r\n\r\n", physicalgraph.device.Protocol.LAN, selectedHue))
|
||||
}
|
||||
|
||||
private isOnline(id) {
|
||||
@@ -1193,13 +1199,11 @@ private put(path, body) {
|
||||
log.debug "PUT: $host$uri"
|
||||
log.debug "BODY: ${bodyJSON}"
|
||||
|
||||
sendHubCommand(new physicalgraph.device.HubAction("""PUT $uri HTTP/1.1
|
||||
HOST: ${host}
|
||||
Content-Length: ${length}
|
||||
|
||||
${bodyJSON}
|
||||
""", physicalgraph.device.Protocol.LAN, "${selectedHue}"))
|
||||
|
||||
sendHubCommand(new physicalgraph.device.HubAction("PUT $uri HTTP/1.1\r\n" +
|
||||
"HOST: ${host}\r\n" +
|
||||
"Content-Length: ${length}\r\n" +
|
||||
"\r\n" +
|
||||
"${bodyJSON}", physicalgraph.device.Protocol.LAN, "${selectedHue}"))
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -1657,3 +1661,101 @@ private boolean checkPointInLampsReach(p, colorPoints) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an RGB color in hex to HSV.
|
||||
* Algorithm based on http://en.wikipedia.org/wiki/HSV_color_space.
|
||||
*
|
||||
* @param colorStr color value in hex (#ff03d3)
|
||||
*
|
||||
* @return HSV representation in an array (0-100) [hue, sat, value]
|
||||
*/
|
||||
def hexToHsv(colorStr){
|
||||
def r = Integer.valueOf( colorStr.substring( 1, 3 ), 16 ) / 255
|
||||
def g = Integer.valueOf( colorStr.substring( 3, 5 ), 16 ) / 255
|
||||
def b = Integer.valueOf( colorStr.substring( 5, 7 ), 16 ) / 255;
|
||||
|
||||
def max = Math.max(Math.max(r, g), b)
|
||||
def min = Math.min(Math.min(r, g), b)
|
||||
|
||||
def h, s, v = max;
|
||||
|
||||
def d = max - min;
|
||||
s = max == 0 ? 0 : d / max;
|
||||
|
||||
if(max == min){
|
||||
h = 0;
|
||||
}else{
|
||||
switch(max){
|
||||
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
|
||||
case g: h = (b - r) / d + 2; break;
|
||||
case b: h = (r - g) / d + 4; break;
|
||||
}
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
return [(h * 100).round(), (s * 100).round(), (v * 100).round()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts HSV color to RGB in hex.
|
||||
* Algorithm based on http://en.wikipedia.org/wiki/HSV_color_space.
|
||||
*
|
||||
* @param hue hue 0-100
|
||||
* @param sat saturation 0-100
|
||||
* @param value value 0-100 (defaults to 100)
|
||||
|
||||
* @return the color in hex (#ff03d3)
|
||||
*/
|
||||
def hsvToHex(hue, sat, value = 100){
|
||||
def r, g, b;
|
||||
def h = hue / 100
|
||||
def s = sat / 100
|
||||
def v = value / 100
|
||||
|
||||
def i = Math.floor(h * 6);
|
||||
def f = h * 6 - i;
|
||||
def p = v * (1 - s);
|
||||
def q = v * (1 - f * s);
|
||||
def t = v * (1 - (1 - f) * s);
|
||||
|
||||
switch (i % 6) {
|
||||
case 0:
|
||||
r = v
|
||||
g = t
|
||||
b = p
|
||||
break
|
||||
case 1:
|
||||
r = q
|
||||
g = v
|
||||
b = p
|
||||
break
|
||||
case 2:
|
||||
r = p
|
||||
g = v
|
||||
b = t
|
||||
break
|
||||
case 3:
|
||||
r = p
|
||||
g = q
|
||||
b = v
|
||||
break
|
||||
case 4:
|
||||
r = t
|
||||
g = p
|
||||
b = v
|
||||
break
|
||||
case 5:
|
||||
r = v
|
||||
g = p
|
||||
b = q
|
||||
break
|
||||
}
|
||||
|
||||
// Converting float components to int components.
|
||||
def r1 = String.format("%02X", (int) (r * 255.0f));
|
||||
def g1 = String.format("%02X", (int) (g * 255.0f));
|
||||
def b1 = String.format("%02X", (int) (b * 255.0f));
|
||||
|
||||
return "#$r1$g1$b1"
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ definition(
|
||||
}
|
||||
|
||||
preferences(oauthPage: "deviceAuthorization") {
|
||||
page(name: "Credentials", title: "Connect to your Logitech Harmony device", content: "authPage", install: false, nextPage: "deviceAuthorization")
|
||||
page(name: "Credentials", title: "Connect to your Logitech Harmony device", content: "authPage", install: false, nextPage: "deviceAuthorization")
|
||||
page(name: "deviceAuthorization", title: "Logitech Harmony device authorization", install: true) {
|
||||
section("Allow Logitech Harmony to control these things...") {
|
||||
input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false
|
||||
@@ -102,7 +102,8 @@ def authPage() {
|
||||
description = "Click to enter Harmony Credentials"
|
||||
def redirectUrl = buildRedirectUrl
|
||||
return dynamicPage(name: "Credentials", title: "Harmony", nextPage: null, uninstall: true, install:false) {
|
||||
section { href url:redirectUrl, style:"embedded", required:true, title:"Harmony", description:description }
|
||||
section { paragraph title: "Note:", "This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance." }
|
||||
section { href url:redirectUrl, style:"embedded", required:true, title:"Harmony", description:description }
|
||||
}
|
||||
} else {
|
||||
//device discovery request every 5 //25 seconds
|
||||
@@ -314,8 +315,6 @@ def installed() {
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
unschedule()
|
||||
if (!state.accessToken) {
|
||||
log.debug "About to create access token"
|
||||
createAccessToken()
|
||||
|
||||
@@ -86,6 +86,7 @@ def firstPage()
|
||||
def lightSwitchesDiscovered = lightSwitchesDiscovered()
|
||||
|
||||
return dynamicPage(name:"firstPage", title:"Discovery Started!", nextPage:"", refreshInterval: refreshInterval, install:true, uninstall: true) {
|
||||
section { paragraph title: "Note:", "This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance." }
|
||||
section("Select a device...") {
|
||||
input "selectedSwitches", "enum", required:false, title:"Select Wemo Switches \n(${switchesDiscovered.size() ?: 0} found)", multiple:true, options:switchesDiscovered
|
||||
input "selectedMotions", "enum", required:false, title:"Select Wemo Motions \n(${motionsDiscovered.size() ?: 0} found)", multiple:true, options:motionsDiscovered
|
||||
@@ -681,4 +682,4 @@ private Boolean hasAllHubsOver(String desiredFirmware) {
|
||||
|
||||
private List getRealHubFirmwareVersions() {
|
||||
return location.hubs*.firmwareVersionString.findAll { it }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user