mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-08 05:31:56 +00:00
Checked in new Withings manager from prod
This commit is contained in:
@@ -0,0 +1,775 @@
|
|||||||
|
/**
|
||||||
|
* Title: Withings Service Manager
|
||||||
|
* Description: Connect Your Withings Devices
|
||||||
|
*
|
||||||
|
* Author: steve
|
||||||
|
* Date: 1/9/15
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Copyright 2015 steve
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
definition(
|
||||||
|
name: "Withings Manager",
|
||||||
|
namespace: "smartthings",
|
||||||
|
author: "SmartThings",
|
||||||
|
description: "Connect With Withings",
|
||||||
|
category: "",
|
||||||
|
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/withings.png",
|
||||||
|
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/withings%402x.png",
|
||||||
|
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Partner/withings%402x.png",
|
||||||
|
oauth: true
|
||||||
|
) {
|
||||||
|
appSetting "consumerKey"
|
||||||
|
appSetting "consumerSecret"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// PAGES
|
||||||
|
// ========================================================
|
||||||
|
|
||||||
|
preferences {
|
||||||
|
page(name: "authPage")
|
||||||
|
}
|
||||||
|
|
||||||
|
def authPage() {
|
||||||
|
|
||||||
|
def installOptions = false
|
||||||
|
def description = "Required (tap to set)"
|
||||||
|
def authState
|
||||||
|
|
||||||
|
if (oauth_token()) {
|
||||||
|
// TODO: Check if it's valid
|
||||||
|
if (true) {
|
||||||
|
description = "Saved (tap to change)"
|
||||||
|
installOptions = true
|
||||||
|
authState = "complete"
|
||||||
|
} else {
|
||||||
|
// Worth differentiating here? (no longer valid vs. non-existent state.externalAuthToken?)
|
||||||
|
description = "Required (tap to set)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
dynamicPage(name: "authPage", install: installOptions, uninstall: true) {
|
||||||
|
section {
|
||||||
|
|
||||||
|
if (installOptions) {
|
||||||
|
input(name: "withingsLabel", type: "text", title: "Add a name", description: null, required: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
href url: shortUrl("authenticate"), style: "embedded", required: false, title: "Authenticate with Withings", description: description, state: authState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// MAPPINGS
|
||||||
|
// ========================================================
|
||||||
|
|
||||||
|
mappings {
|
||||||
|
path("/authenticate") {
|
||||||
|
action:
|
||||||
|
[
|
||||||
|
GET: "authenticate"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
path("/x") {
|
||||||
|
action:
|
||||||
|
[
|
||||||
|
GET: "exchangeTokenFromWithings"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
path("/n") {
|
||||||
|
action:
|
||||||
|
[POST: "notificationReceived"]
|
||||||
|
}
|
||||||
|
|
||||||
|
path("/test/:action") {
|
||||||
|
action:
|
||||||
|
[GET: "test"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def test() {
|
||||||
|
"${params.action}"()
|
||||||
|
}
|
||||||
|
|
||||||
|
def authenticate() {
|
||||||
|
// do not hit userAuthorizationUrl when the page is executed. It will replace oauth_tokens
|
||||||
|
// instead, redirect through here so we know for sure that the user wants to authenticate
|
||||||
|
// plus, the short-lived tokens that are used during authentication are only valid for 2 minutes
|
||||||
|
// so make sure we give the user as much of that 2 minutes as possible to enter their credentials and deal with network latency
|
||||||
|
log.trace "starting Withings authentication flow"
|
||||||
|
redirect location: userAuthorizationUrl()
|
||||||
|
}
|
||||||
|
|
||||||
|
def exchangeTokenFromWithings() {
|
||||||
|
// Withings hits us here during the oAuth flow
|
||||||
|
// log.trace "exchangeTokenFromWithings ${params}"
|
||||||
|
atomicState.userid = params.userid // TODO: restructure this for multi-user access
|
||||||
|
exchangeToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
def notificationReceived() {
|
||||||
|
// log.trace "notificationReceived params: ${params}"
|
||||||
|
|
||||||
|
def notificationParams = [
|
||||||
|
startdate: params.startdate,
|
||||||
|
userid : params.userid,
|
||||||
|
enddate : params.enddate,
|
||||||
|
]
|
||||||
|
|
||||||
|
def measures = wGetMeasures(notificationParams)
|
||||||
|
sendMeasureEvents(measures)
|
||||||
|
return [status: 0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// HANDLERS
|
||||||
|
// ========================================================
|
||||||
|
|
||||||
|
|
||||||
|
def installed() {
|
||||||
|
log.debug "Installed with settings: ${settings}"
|
||||||
|
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
def updated() {
|
||||||
|
log.debug "Updated with settings: ${settings}"
|
||||||
|
|
||||||
|
// wRevokeAllNotifications()
|
||||||
|
|
||||||
|
unsubscribe()
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
def initialize() {
|
||||||
|
if (!getChild()) { createChild() }
|
||||||
|
app.updateLabel(withingsLabel)
|
||||||
|
wCreateNotification()
|
||||||
|
backfillMeasures()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// CHILD DEVICE
|
||||||
|
// ========================================================
|
||||||
|
|
||||||
|
private getChild() {
|
||||||
|
def children = childDevices
|
||||||
|
children.size() ? children.first() : null
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createChild() {
|
||||||
|
def child = addChildDevice("smartthings", "Withings User", userid(), null, [name: app.label, label: withingsLabel])
|
||||||
|
atomicState.child = [dni: child.deviceNetworkId]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// URL HELPERS
|
||||||
|
// ========================================================
|
||||||
|
|
||||||
|
def stBaseUrl() {
|
||||||
|
if (!atomicState.serverUrl) {
|
||||||
|
stToken()
|
||||||
|
atomicState.serverUrl = buildActionUrl("").split(/api\//).first()
|
||||||
|
}
|
||||||
|
return atomicState.serverUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
def stToken() {
|
||||||
|
atomicState.accessToken ?: createAccessToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
def shortUrl(path = "", urlParams = [:]) {
|
||||||
|
attachParams("${stBaseUrl()}api/t/${stToken()}/s/${app.id}/${path}", urlParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
def noTokenUrl(path = "", urlParams = [:]) {
|
||||||
|
attachParams("${stBaseUrl()}api/smartapps/installations/${app.id}/${path}", urlParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
def attachParams(url, urlParams = [:]) {
|
||||||
|
[url, toQueryString(urlParams)].findAll().join("?")
|
||||||
|
}
|
||||||
|
|
||||||
|
String toQueryString(Map m = [:]) {
|
||||||
|
// log.trace "toQueryString. URLEncoder will be used on ${m}"
|
||||||
|
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// WITHINGS MEASURES
|
||||||
|
// ========================================================
|
||||||
|
|
||||||
|
def unixTime(date = new Date()) {
|
||||||
|
def unixTime = date.time / 1000 as int
|
||||||
|
// log.debug "converting ${date.time} to ${unixTime}"
|
||||||
|
unixTime
|
||||||
|
}
|
||||||
|
|
||||||
|
def backfillMeasures() {
|
||||||
|
// log.trace "backfillMeasures"
|
||||||
|
def measureParams = [startdate: unixTime(new Date() - 10)]
|
||||||
|
def measures = wGetMeasures(measureParams)
|
||||||
|
sendMeasureEvents(measures)
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is body measures. // TODO: get activity and others too
|
||||||
|
def wGetMeasures(measureParams = [:]) {
|
||||||
|
def baseUrl = "https://wbsapi.withings.net/measure"
|
||||||
|
def urlParams = [
|
||||||
|
action : "getmeas",
|
||||||
|
userid : userid(),
|
||||||
|
startdate : unixTime(new Date() - 5),
|
||||||
|
enddate : unixTime(),
|
||||||
|
oauth_token: oauth_token()
|
||||||
|
] + measureParams
|
||||||
|
def measureData = fetchDataFromWithings(baseUrl, urlParams)
|
||||||
|
// log.debug "measureData: ${measureData}"
|
||||||
|
measureData.body.measuregrps.collect { parseMeasureGroup(it) }.flatten()
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
[
|
||||||
|
body:[
|
||||||
|
measuregrps:[
|
||||||
|
[
|
||||||
|
category:1, // 1 for real measurements, 2 for user objectives.
|
||||||
|
grpid:310040317,
|
||||||
|
measures:[
|
||||||
|
[
|
||||||
|
unit:0, // Power of ten the "value" parameter should be multiplied to to get the real value. Eg : value = 20 and unit=-1 means the value really is 2.0
|
||||||
|
value:60, // Value for the measure in S.I units (kilogram, meters, etc.). Value should be multiplied by 10 to the power of "unit" (see below) to get the real value.
|
||||||
|
type:11 // 1 : Weight (kg), 4 : Height (meter), 5 : Fat Free Mass (kg), 6 : Fat Ratio (%), 8 : Fat Mass Weight (kg), 9 : Diastolic Blood Pressure (mmHg), 10 : Systolic Blood Pressure (mmHg), 11 : Heart Pulse (bpm), 54 : SP02(%)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
unit:-3,
|
||||||
|
value:-1000,
|
||||||
|
type:18
|
||||||
|
]
|
||||||
|
],
|
||||||
|
date:1422750210,
|
||||||
|
attrib:2
|
||||||
|
]
|
||||||
|
],
|
||||||
|
updatetime:1422750227
|
||||||
|
],
|
||||||
|
status:0
|
||||||
|
]
|
||||||
|
*/
|
||||||
|
|
||||||
|
def sendMeasureEvents(measures) {
|
||||||
|
// log.debug "measures: ${measures}"
|
||||||
|
measures.each {
|
||||||
|
if (it.name && it.value) {
|
||||||
|
sendEvent(userid(), it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parseMeasureGroup(measureGroup) {
|
||||||
|
long time = measureGroup.date // must be long. INT_MAX is too small
|
||||||
|
time *= 1000
|
||||||
|
measureGroup.measures.collect { parseMeasure(it) + [date: new Date(time)] }
|
||||||
|
}
|
||||||
|
|
||||||
|
def parseMeasure(measure) {
|
||||||
|
// log.debug "parseMeasure($measure)"
|
||||||
|
[
|
||||||
|
name : measureAttribute(measure),
|
||||||
|
value: measureValue(measure)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def measureValue(measure) {
|
||||||
|
def value = measure.value * 10.power(measure.unit)
|
||||||
|
if (measure.type == 1) { // Weight (kg)
|
||||||
|
value *= 2.20462262 // kg to lbs
|
||||||
|
}
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
String measureAttribute(measure) {
|
||||||
|
def attribute = ""
|
||||||
|
switch (measure.type) {
|
||||||
|
case 1: attribute = "weight"; break;
|
||||||
|
case 4: attribute = "height"; break;
|
||||||
|
case 5: attribute = "leanMass"; break;
|
||||||
|
case 6: attribute = "fatRatio"; break;
|
||||||
|
case 8: attribute = "fatMass"; break;
|
||||||
|
case 9: attribute = "diastolicPressure"; break;
|
||||||
|
case 10: attribute = "systolicPressure"; break;
|
||||||
|
case 11: attribute = "heartPulse"; break;
|
||||||
|
case 54: attribute = "SP02"; break;
|
||||||
|
}
|
||||||
|
return attribute
|
||||||
|
}
|
||||||
|
|
||||||
|
String measureDescription(measure) {
|
||||||
|
def description = ""
|
||||||
|
switch (measure.type) {
|
||||||
|
case 1: description = "Weight (kg)"; break;
|
||||||
|
case 4: description = "Height (meter)"; break;
|
||||||
|
case 5: description = "Fat Free Mass (kg)"; break;
|
||||||
|
case 6: description = "Fat Ratio (%)"; break;
|
||||||
|
case 8: description = "Fat Mass Weight (kg)"; break;
|
||||||
|
case 9: description = "Diastolic Blood Pressure (mmHg)"; break;
|
||||||
|
case 10: description = "Systolic Blood Pressure (mmHg)"; break;
|
||||||
|
case 11: description = "Heart Pulse (bpm)"; break;
|
||||||
|
case 54: description = "SP02(%)"; break;
|
||||||
|
}
|
||||||
|
return description
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// WITHINGS NOTIFICATIONS
|
||||||
|
// ========================================================
|
||||||
|
|
||||||
|
def wNotificationBaseUrl() { "https://wbsapi.withings.net/notify" }
|
||||||
|
|
||||||
|
def wNotificationCallbackUrl() { shortUrl("n") }
|
||||||
|
|
||||||
|
def wGetNotification() {
|
||||||
|
def userId = userid()
|
||||||
|
def url = wNotificationBaseUrl()
|
||||||
|
def params = [
|
||||||
|
action: "subscribe"
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: keep track of notification expiration
|
||||||
|
def wCreateNotification() {
|
||||||
|
def baseUrl = wNotificationBaseUrl()
|
||||||
|
def urlParams = [
|
||||||
|
action : "subscribe",
|
||||||
|
userid : userid(),
|
||||||
|
callbackurl: wNotificationCallbackUrl(),
|
||||||
|
oauth_token: oauth_token(),
|
||||||
|
comment : "hmm" // TODO: figure out what to do here. spaces seem to break the request
|
||||||
|
]
|
||||||
|
|
||||||
|
fetchDataFromWithings(baseUrl, urlParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
def wRevokeAllNotifications() {
|
||||||
|
def notifications = wListNotifications()
|
||||||
|
notifications.each {
|
||||||
|
wRevokeNotification([callbackurl: it.callbackurl]) // use the callbackurl Withings has on file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def wRevokeNotification(notificationParams = [:]) {
|
||||||
|
def baseUrl = wNotificationBaseUrl()
|
||||||
|
def urlParams = [
|
||||||
|
action : "revoke",
|
||||||
|
userid : userid(),
|
||||||
|
callbackurl: wNotificationCallbackUrl(),
|
||||||
|
oauth_token: oauth_token()
|
||||||
|
] + notificationParams
|
||||||
|
|
||||||
|
fetchDataFromWithings(baseUrl, urlParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
def wListNotifications() {
|
||||||
|
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
body: {
|
||||||
|
profiles: [
|
||||||
|
{
|
||||||
|
appli: 1,
|
||||||
|
expires: 2147483647,
|
||||||
|
callbackurl: "https://graph.api.smartthings.com/api/t/72ab3e57-5839-4cca-9562-dcc818f83bc9/s/537757a0-c4c8-40ea-8cea-aa283915bbd9/n",
|
||||||
|
comment: "hmm"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
status: 0
|
||||||
|
}*/
|
||||||
|
|
||||||
|
def baseUrl = wNotificationBaseUrl()
|
||||||
|
def urlParams = [
|
||||||
|
action : "list",
|
||||||
|
userid : userid(),
|
||||||
|
callbackurl: wNotificationCallbackUrl(),
|
||||||
|
oauth_token: oauth_token()
|
||||||
|
]
|
||||||
|
|
||||||
|
def notificationData = fetchDataFromWithings(baseUrl, urlParams)
|
||||||
|
notificationData.body.profiles
|
||||||
|
}
|
||||||
|
|
||||||
|
def defaultOauthParams() {
|
||||||
|
defaultParameterKeys().inject([:]) { keyMap, currentKey ->
|
||||||
|
keyMap[currentKey] = "${currentKey}"()
|
||||||
|
keyMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// WITHINGS DATA FETCHING
|
||||||
|
// ========================================================
|
||||||
|
|
||||||
|
def fetchDataFromWithings(baseUrl, urlParams) {
|
||||||
|
|
||||||
|
// log.debug "fetchDataFromWithings(${baseUrl}, ${urlParams})"
|
||||||
|
|
||||||
|
def defaultParams = defaultOauthParams()
|
||||||
|
def paramStrings = buildOauthParams(urlParams + defaultParams)
|
||||||
|
// log.debug "paramStrings: $paramStrings"
|
||||||
|
def url = buildOauthUrl(baseUrl, paramStrings, oauth_token_secret())
|
||||||
|
def json
|
||||||
|
// log.debug "about to make request to ${url}"
|
||||||
|
httpGet(uri: url, headers: ["Content-Type": "application/json"]) { response ->
|
||||||
|
json = new groovy.json.JsonSlurper().parse(response.data)
|
||||||
|
}
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// WITHINGS OAUTH LOGGING
|
||||||
|
// ========================================================
|
||||||
|
|
||||||
|
def wLogEnabled() { false } // For troubleshooting Oauth flow
|
||||||
|
|
||||||
|
void wLog(message = "") {
|
||||||
|
if (!wLogEnabled()) { return }
|
||||||
|
def wLogMessage = atomicState.wLogMessage
|
||||||
|
if (wLogMessage.length()) {
|
||||||
|
wLogMessage += "\n|"
|
||||||
|
}
|
||||||
|
wLogMessage += message
|
||||||
|
atomicState.wLogMessage = wLogMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
void wLogNew(seedMessage = "") {
|
||||||
|
if (!wLogEnabled()) { return }
|
||||||
|
def olMessage = atomicState.wLogMessage
|
||||||
|
if (oldMessage) {
|
||||||
|
log.debug "purging old wLogMessage: ${olMessage}"
|
||||||
|
}
|
||||||
|
atomicState.wLogMessage = seedMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
String wLogMessage() {
|
||||||
|
if (!wLogEnabled()) { return }
|
||||||
|
def wLogMessage = atomicState.wLogMessage
|
||||||
|
atomicState.wLogMessage = ""
|
||||||
|
wLogMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// WITHINGS OAUTH DESCRIPTION
|
||||||
|
// >>>>>> The user opens the authPage for this SmartApp
|
||||||
|
// STEP 1 get a token to be used in the url the user taps
|
||||||
|
// STEP 2 generate the url to be tapped by the user
|
||||||
|
// >>>>>> The user taps the url and logs in to Withings
|
||||||
|
// STEP 3 generate a token to be used for accessing user data
|
||||||
|
// STEP 4 access user data
|
||||||
|
// ========================================================
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// WITHINGS OAUTH STEP 1: get an oAuth "request token"
|
||||||
|
// ========================================================
|
||||||
|
|
||||||
|
def requestTokenUrl() {
|
||||||
|
wLogNew "WITHINGS OAUTH STEP 1: get an oAuth 'request token'"
|
||||||
|
|
||||||
|
def keys = defaultParameterKeys() + "oauth_callback"
|
||||||
|
def paramStrings = buildOauthParams(keys.sort())
|
||||||
|
|
||||||
|
buildOauthUrl("https://oauth.withings.com/account/request_token", paramStrings, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// WITHINGS OAUTH STEP 2: End-user authorization
|
||||||
|
// ========================================================
|
||||||
|
|
||||||
|
def userAuthorizationUrl() {
|
||||||
|
|
||||||
|
// get url from Step 1
|
||||||
|
def tokenUrl = requestTokenUrl()
|
||||||
|
|
||||||
|
// collect token from Withings
|
||||||
|
collectTokenFromWithings(tokenUrl)
|
||||||
|
|
||||||
|
wLogNew "WITHINGS OAUTH STEP 2: End-user authorization"
|
||||||
|
|
||||||
|
def keys = defaultParameterKeys() + "oauth_token"
|
||||||
|
def paramStrings = buildOauthParams(keys.sort())
|
||||||
|
|
||||||
|
buildOauthUrl("https://oauth.withings.com/account/authorize", paramStrings, oauth_token_secret())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// WITHINGS OAUTH STEP 3: Generating access token
|
||||||
|
// ========================================================
|
||||||
|
|
||||||
|
def exchangeTokenUrl() {
|
||||||
|
wLogNew "WITHINGS OAUTH STEP 3: Generating access token"
|
||||||
|
|
||||||
|
def keys = defaultParameterKeys() + ["oauth_token", "userid"]
|
||||||
|
def paramStrings = buildOauthParams(keys.sort())
|
||||||
|
|
||||||
|
buildOauthUrl("https://oauth.withings.com/account/access_token", paramStrings, oauth_token_secret())
|
||||||
|
}
|
||||||
|
|
||||||
|
def exchangeToken() {
|
||||||
|
|
||||||
|
def tokenUrl = exchangeTokenUrl()
|
||||||
|
// log.debug "about to hit ${tokenUrl}"
|
||||||
|
|
||||||
|
try {
|
||||||
|
// replace old token with a long-lived token
|
||||||
|
def token = collectTokenFromWithings(tokenUrl)
|
||||||
|
// log.debug "collected token from Withings: ${token}"
|
||||||
|
renderAction("authorized", "Withings Connection")
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
log.error e
|
||||||
|
renderAction("notAuthorized", "Withings Connection Failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// OAUTH 1.0
|
||||||
|
// ========================================================
|
||||||
|
|
||||||
|
def defaultParameterKeys() {
|
||||||
|
[
|
||||||
|
"oauth_consumer_key",
|
||||||
|
"oauth_nonce",
|
||||||
|
"oauth_signature_method",
|
||||||
|
"oauth_timestamp",
|
||||||
|
"oauth_version"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def oauth_consumer_key() { consumerKey }
|
||||||
|
|
||||||
|
def oauth_nonce() { nonce() }
|
||||||
|
|
||||||
|
def nonce() { UUID.randomUUID().toString().replaceAll("-", "") }
|
||||||
|
|
||||||
|
def oauth_signature_method() { "HMAC-SHA1" }
|
||||||
|
|
||||||
|
def oauth_timestamp() { (int) (new Date().time / 1000) }
|
||||||
|
|
||||||
|
def oauth_version() { 1.0 }
|
||||||
|
|
||||||
|
def oauth_callback() { shortUrl("x") }
|
||||||
|
|
||||||
|
def oauth_token() { atomicState.wToken?.oauth_token }
|
||||||
|
|
||||||
|
def oauth_token_secret() { atomicState.wToken?.oauth_token_secret }
|
||||||
|
|
||||||
|
def userid() { atomicState.userid }
|
||||||
|
|
||||||
|
String hmac(String oAuthSignatureBaseString, String oAuthSecret) throws java.security.SignatureException {
|
||||||
|
if (!oAuthSecret.contains("&")) { log.warn "Withings requires \"&\" to be included no matter what" }
|
||||||
|
// get an hmac_sha1 key from the raw key bytes
|
||||||
|
def signingKey = new javax.crypto.spec.SecretKeySpec(oAuthSecret.getBytes(), "HmacSHA1")
|
||||||
|
// get an hmac_sha1 Mac instance and initialize with the signing key
|
||||||
|
def mac = javax.crypto.Mac.getInstance("HmacSHA1")
|
||||||
|
mac.init(signingKey)
|
||||||
|
// compute the hmac on input data bytes
|
||||||
|
byte[] rawHmac = mac.doFinal(oAuthSignatureBaseString.getBytes())
|
||||||
|
return org.apache.commons.codec.binary.Base64.encodeBase64String(rawHmac)
|
||||||
|
}
|
||||||
|
|
||||||
|
Map parseResponseString(String responseString) {
|
||||||
|
// log.debug "parseResponseString: ${responseString}"
|
||||||
|
responseString.split("&").inject([:]) { c, it ->
|
||||||
|
def parts = it.split('=')
|
||||||
|
def k = parts[0]
|
||||||
|
def v = parts[1]
|
||||||
|
c[k] = v
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String applyParams(endpoint, oauthParams) { endpoint + "?" + oauthParams.sort().join("&") }
|
||||||
|
|
||||||
|
String buildSignature(endpoint, oAuthParams, oAuthSecret) {
|
||||||
|
def oAuthSignatureBaseParts = ["GET", endpoint, oAuthParams.join("&")]
|
||||||
|
def oAuthSignatureBaseString = oAuthSignatureBaseParts.collect { URLEncoder.encode(it) }.join("&")
|
||||||
|
wLog " ==> oAuth signature base string : \n${oAuthSignatureBaseString}"
|
||||||
|
wLog " .. applying hmac-sha1 to base string, with secret : ${oAuthSecret} (notice the \"&\")"
|
||||||
|
wLog " .. base64 encode then url-encode the hmac-sha1 hash"
|
||||||
|
String hmacResult = hmac(oAuthSignatureBaseString, oAuthSecret)
|
||||||
|
def signature = URLEncoder.encode(hmacResult)
|
||||||
|
wLog " ==> oauth_signature = ${signature}"
|
||||||
|
return signature
|
||||||
|
}
|
||||||
|
|
||||||
|
List buildOauthParams(List parameterKeys) {
|
||||||
|
wLog " .. adding oAuth parameters : "
|
||||||
|
def oauthParams = []
|
||||||
|
parameterKeys.each { key ->
|
||||||
|
def value = "${key}"()
|
||||||
|
wLog " ${key} = ${value}"
|
||||||
|
oauthParams << "${key}=${URLEncoder.encode(value.toString())}"
|
||||||
|
}
|
||||||
|
|
||||||
|
wLog " .. sorting all request parameters alphabetically "
|
||||||
|
oauthParams.sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
List buildOauthParams(Map parameters) {
|
||||||
|
wLog " .. adding oAuth parameters : "
|
||||||
|
def oauthParams = []
|
||||||
|
parameters.each { k, v ->
|
||||||
|
wLog " ${k} = ${v}"
|
||||||
|
oauthParams << "${k}=${URLEncoder.encode(v.toString())}"
|
||||||
|
}
|
||||||
|
|
||||||
|
wLog " .. sorting all request parameters alphabetically "
|
||||||
|
oauthParams.sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
String buildOauthUrl(String endpoint, List parameterStrings, String oAuthTokenSecret) {
|
||||||
|
wLog "Api endpoint : ${endpoint}"
|
||||||
|
|
||||||
|
wLog "Signing request :"
|
||||||
|
def oAuthSecret = "${consumerSecret}&${oAuthTokenSecret}"
|
||||||
|
def signature = buildSignature(endpoint, parameterStrings, oAuthSecret)
|
||||||
|
|
||||||
|
parameterStrings << "oauth_signature=${signature}"
|
||||||
|
|
||||||
|
def finalUrl = applyParams(endpoint, parameterStrings)
|
||||||
|
wLog "Result: ${finalUrl}"
|
||||||
|
if (wLogEnabled()) {
|
||||||
|
log.debug wLogMessage()
|
||||||
|
}
|
||||||
|
return finalUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
def collectTokenFromWithings(tokenUrl) {
|
||||||
|
// get token from Withings using the url generated in Step 1
|
||||||
|
def tokenString
|
||||||
|
httpGet(uri: tokenUrl) { resp -> // oauth_token=<token_key>&oauth_token_secret=<token_secret>
|
||||||
|
tokenString = resp.data.toString()
|
||||||
|
// log.debug "collectTokenFromWithings: ${tokenString}"
|
||||||
|
}
|
||||||
|
def token = parseResponseString(tokenString)
|
||||||
|
atomicState.wToken = token
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// APP SETTINGS
|
||||||
|
// ========================================================
|
||||||
|
|
||||||
|
def getConsumerKey() { appSettings.consumerKey }
|
||||||
|
|
||||||
|
def getConsumerSecret() { appSettings.consumerSecret }
|
||||||
|
|
||||||
|
// figure out how to put this in settings
|
||||||
|
def getUserId() { atomicState.wToken?.userid }
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// HTML rendering
|
||||||
|
// ========================================================
|
||||||
|
|
||||||
|
def renderAction(action, title = "") {
|
||||||
|
log.debug "renderAction: $action"
|
||||||
|
renderHTML(title) {
|
||||||
|
head { "${action}HtmlHead"() }
|
||||||
|
body { "${action}HtmlBody"() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def authorizedHtmlHead() {
|
||||||
|
log.trace "authorizedHtmlHead"
|
||||||
|
"""
|
||||||
|
<style type="text/css">
|
||||||
|
@font-face {
|
||||||
|
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?#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.ttf') format('truetype'),
|
||||||
|
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
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?#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.ttf') format('truetype'),
|
||||||
|
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
/*width: 560px;
|
||||||
|
padding: 40px;*/
|
||||||
|
/*background: #eee;*/
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
vertical-align: middle;
|
||||||
|
max-width:20%;
|
||||||
|
}
|
||||||
|
img:nth-child(2) {
|
||||||
|
margin: 0 30px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
/*font-size: 1.2em;*/
|
||||||
|
font-family: 'Swiss 721 W01 Thin';
|
||||||
|
text-align: center;
|
||||||
|
color: #666666;
|
||||||
|
padding: 0 10px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
p:last-child {
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
span {
|
||||||
|
font-family: 'Swiss 721 W01 Light';
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
|
def authorizedHtmlBody() {
|
||||||
|
"""
|
||||||
|
<div class="container">
|
||||||
|
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/withings@2x.png" alt="withings icon" />
|
||||||
|
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" />
|
||||||
|
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" />
|
||||||
|
<p>Your Withings scale is now connected to SmartThings!</p>
|
||||||
|
<p>Click 'Done' to finish setup.</p>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
|
def notAuthorizedHtmlHead() {
|
||||||
|
log.trace "notAuthorizedHtmlHead"
|
||||||
|
authorizedHtmlHead()
|
||||||
|
}
|
||||||
|
|
||||||
|
def notAuthorizedHtmlBody() {
|
||||||
|
"""
|
||||||
|
<div class="container">
|
||||||
|
<p>There was an error connecting to SmartThings!</p>
|
||||||
|
<p>Click 'Done' to try again.</p>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user