mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-08 05:31:56 +00:00
Needed vars for testing: client_id = 6479182133460992 clident_secret = a0c318b6-042f-4a91-8f56-654a6cc37c9a https_plantLinkServer = https://hardware-dot-oso-tech.appspot.com
389 lines
14 KiB
Groovy
389 lines
14 KiB
Groovy
/**
|
|
* Required PlantLink Connector
|
|
* This SmartApp forwards the raw data of the deviceType to myplantlink.com
|
|
* and returns it back to your device after calculating soil and plant type.
|
|
*
|
|
* Copyright 2015 Oso Technologies
|
|
*
|
|
* 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.
|
|
*
|
|
*/
|
|
import groovy.json.JsonBuilder
|
|
import java.util.regex.Matcher
|
|
import java.util.regex.Pattern
|
|
|
|
definition(
|
|
name: "PlantLink Connector",
|
|
namespace: "Osotech",
|
|
author: "Oso Technologies",
|
|
description: "This SmartApp connects to myplantlink.com and forwards the device data to it so it can calculate easy to read plant status for your specific plant's needs.",
|
|
category: "Convenience",
|
|
iconUrl: "https://dashboard.myplantlink.com/images/apple-touch-icon-76x76-precomposed.png",
|
|
iconX2Url: "https://dashboard.myplantlink.com/images/apple-touch-icon-120x120-precomposed.png",
|
|
iconX3Url: "https://dashboard.myplantlink.com/images/apple-touch-icon-152x152-precomposed.png"
|
|
) {
|
|
appSetting "client_id"
|
|
appSetting "client_secret"
|
|
appSetting "https_plantLinkServer"
|
|
}
|
|
|
|
preferences {
|
|
page(name: "auth", title: "Step 1 of 2", nextPage:"deviceList", content:"authPage")
|
|
page(name: "deviceList", title: "Step 2 of 2", install:true, uninstall:false){
|
|
section {
|
|
input "plantlinksensors", "capability.sensor", title: "Select PlantLink sensors", multiple: true
|
|
}
|
|
}
|
|
}
|
|
|
|
mappings {
|
|
path("/swapToken") {
|
|
action: [
|
|
GET: "swapToken"
|
|
]
|
|
}
|
|
}
|
|
|
|
def authPage(){
|
|
if(!atomicState.accessToken){
|
|
createAccessToken()
|
|
atomicState.accessToken = state.accessToken
|
|
}
|
|
|
|
def redirectUrl = oauthInitUrl()
|
|
def uninstallAllowed = false
|
|
def oauthTokenProvided = false
|
|
if(atomicState.authToken){
|
|
uninstallAllowed = true
|
|
oauthTokenProvided = true
|
|
}
|
|
|
|
if (!oauthTokenProvided) {
|
|
return dynamicPage(name: "auth", title: "Step 1 of 2", nextPage:null, uninstall:uninstallAllowed) {
|
|
section(){
|
|
href(name:"login",
|
|
url:redirectUrl,
|
|
style:"embedded",
|
|
title:"PlantLink",
|
|
image:"https://dashboard.myplantlink.com/images/PLlogo.png",
|
|
description:"Tap to login to myplantlink.com")
|
|
}
|
|
}
|
|
}else{
|
|
return dynamicPage(name: "auth", title: "Step 1 of 2 - Completed", nextPage:"deviceList", uninstall:uninstallAllowed) {
|
|
section(){
|
|
paragraph "You are logged in to myplantlink.com, tap next to continue", image: iconUrl
|
|
href(url:redirectUrl, title:"Or", description:"tap to switch accounts")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
def installed() {
|
|
log.debug "Installed with settings: ${settings}"
|
|
initialize()
|
|
}
|
|
|
|
def updated() {
|
|
log.debug "Updated with settings: ${settings}"
|
|
unsubscribe()
|
|
initialize()
|
|
}
|
|
|
|
def uninstalled() {
|
|
if (plantlinksensors){
|
|
plantlinksensors.each{ sensor_device ->
|
|
sensor_device.setInstallSmartApp("needSmartApp")
|
|
}
|
|
}
|
|
}
|
|
|
|
def initialize() {
|
|
atomicState.attached_sensors = [:]
|
|
if (plantlinksensors){
|
|
subscribe(plantlinksensors, "moisture_status", moistureHandler)
|
|
subscribe(plantlinksensors, "battery_status", batteryHandler)
|
|
plantlinksensors.each{ sensor_device ->
|
|
sensor_device.setStatusIcon("Waiting on First Measurement")
|
|
sensor_device.setInstallSmartApp("connectedToSmartApp")
|
|
}
|
|
}
|
|
}
|
|
|
|
def dock_sensor(device_serial, expected_plant_name) {
|
|
def docking_body_json_builder = new JsonBuilder([version: '1c', smartthings_device_id: device_serial])
|
|
def docking_params = [
|
|
uri : appSettings.https_plantLinkServer,
|
|
path : "/api/v1/smartthings/links",
|
|
headers : ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
|
|
contentType: "application/json",
|
|
body: docking_body_json_builder.toString()
|
|
]
|
|
def plant_post_body_map = [
|
|
plant_type_key: 999999,
|
|
soil_type_key : 1000004
|
|
]
|
|
def plant_post_params = [
|
|
uri : appSettings.https_plantLinkServer,
|
|
path : "/api/v1/plants",
|
|
headers : ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
|
|
contentType: "application/json",
|
|
]
|
|
log.debug "Creating new plant on myplantlink.com - ${expected_plant_name}"
|
|
httpPost(docking_params) { docking_response ->
|
|
if (parse_api_response(docking_response, "Docking a link")) {
|
|
if (docking_response.data.plants.size() == 0) {
|
|
log.debug "creating plant for - ${expected_plant_name}"
|
|
plant_post_body_map["name"] = expected_plant_name
|
|
plant_post_body_map['links_key'] = [docking_response.data.key]
|
|
def plant_post_body_json_builder = new JsonBuilder(plant_post_body_map)
|
|
plant_post_params["body"] = plant_post_body_json_builder.toString()
|
|
httpPost(plant_post_params) { plant_post_response ->
|
|
if(parse_api_response(plant_post_response, 'creating plant')){
|
|
def attached_map = atomicState.attached_sensors
|
|
attached_map[device_serial] = plant_post_response.data
|
|
atomicState.attached_sensors = attached_map
|
|
}
|
|
}
|
|
} else {
|
|
def plant = docking_response.data.plants[0]
|
|
def attached_map = atomicState.attached_sensors
|
|
attached_map[device_serial] = plant
|
|
atomicState.attached_sensors = attached_map
|
|
checkAndUpdatePlantIfNeeded(plant, expected_plant_name)
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
def checkAndUpdatePlantIfNeeded(plant, expected_plant_name){
|
|
def plant_put_params = [
|
|
uri : appSettings.https_plantLinkServer,
|
|
headers : ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
|
|
contentType : "application/json"
|
|
]
|
|
if (plant.name != expected_plant_name) {
|
|
log.debug "updating plant for - ${expected_plant_name}"
|
|
plant_put_params["path"] = "/api/v1/plants/${plant.key}"
|
|
def plant_put_body_map = [
|
|
name: expected_plant_name
|
|
]
|
|
def plant_put_body_json_builder = new JsonBuilder(plant_put_body_map)
|
|
plant_put_params["body"] = plant_put_body_json_builder.toString()
|
|
httpPut(plant_put_params) { plant_put_response ->
|
|
parse_api_response(plant_put_response, 'updating plant name')
|
|
}
|
|
}
|
|
}
|
|
|
|
def moistureHandler(event){
|
|
def expected_plant_name = "SmartThings - ${event.displayName}"
|
|
def device_serial = getDeviceSerialFromEvent(event)
|
|
|
|
if (!atomicState.attached_sensors.containsKey(device_serial)){
|
|
dock_sensor(device_serial, expected_plant_name)
|
|
}else{
|
|
def measurement_post_params = [
|
|
uri: appSettings.https_plantLinkServer,
|
|
path: "/api/v1/smartthings/links/${device_serial}/measurements",
|
|
headers: ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
|
|
contentType: "application/json",
|
|
body: event.value
|
|
]
|
|
httpPost(measurement_post_params) { measurement_post_response ->
|
|
if (parse_api_response(measurement_post_response, 'creating moisture measurement') &&
|
|
measurement_post_response.data.size() >0){
|
|
def measurement = measurement_post_response.data[0]
|
|
def plant = measurement.plant
|
|
log.debug plant
|
|
checkAndUpdatePlantIfNeeded(plant, expected_plant_name)
|
|
plantlinksensors.each{ sensor_device ->
|
|
if (sensor_device.id == event.deviceId){
|
|
sensor_device.setStatusIcon(plant.status)
|
|
if (plant.last_measurements && plant.last_measurements[0].moisture){
|
|
sensor_device.setPlantFuelLevel(plant.last_measurements[0].moisture * 100 as int)
|
|
}
|
|
if (plant.last_measurements && plant.last_measurements[0].battery){
|
|
sensor_device.setBatteryLevel(plant.last_measurements[0].battery * 100 as int)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
def batteryHandler(event){
|
|
def expected_plant_name = "SmartThings - ${event.displayName}"
|
|
def device_serial = getDeviceSerialFromEvent(event)
|
|
|
|
if (!atomicState.attached_sensors.containsKey(device_serial)){
|
|
dock_sensor(device_serial, expected_plant_name)
|
|
}else{
|
|
def measurement_post_params = [
|
|
uri: appSettings.https_plantLinkServer,
|
|
path: "/api/v1/smartthings/links/${device_serial}/measurements",
|
|
headers: ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
|
|
contentType: "application/json",
|
|
body: event.value
|
|
]
|
|
httpPost(measurement_post_params) { measurement_post_response ->
|
|
parse_api_response(measurement_post_response, 'creating battery measurement')
|
|
}
|
|
}
|
|
}
|
|
|
|
def getDeviceSerialFromEvent(event){
|
|
def pattern = /.*"zigbeedeviceid"\s*:\s*"(\w+)".*/
|
|
def match_result = (event.value =~ pattern)
|
|
return match_result[0][1]
|
|
}
|
|
|
|
def oauthInitUrl(){
|
|
atomicState.oauthInitState = UUID.randomUUID().toString()
|
|
def oauthParams = [
|
|
response_type: "code",
|
|
client_id: appSettings.client_id,
|
|
state: atomicState.oauthInitState,
|
|
redirect_uri: buildRedirectUrl()
|
|
]
|
|
return appSettings.https_plantLinkServer + "/oauth/oauth2/authorize?" + toQueryString(oauthParams)
|
|
}
|
|
|
|
def buildRedirectUrl(){
|
|
return getServerUrl() + "/api/token/${atomicState.accessToken}/smartapps/installations/${app.id}/swapToken"
|
|
}
|
|
|
|
def swapToken(){
|
|
def code = params.code
|
|
def oauthState = params.state
|
|
def stcid = appSettings.client_id
|
|
def postParams = [
|
|
method: 'POST',
|
|
uri: "https://oso-tech.appspot.com",
|
|
path: "/api/v1/oauth-token",
|
|
query: [grant_type:'authorization_code', code:params.code, client_id:stcid,
|
|
client_secret:appSettings.client_secret, redirect_uri: buildRedirectUrl()],
|
|
]
|
|
|
|
def jsonMap
|
|
httpPost(postParams) { resp ->
|
|
jsonMap = resp.data
|
|
}
|
|
|
|
atomicState.refreshToken = jsonMap.refresh_token
|
|
atomicState.authToken = jsonMap.access_token
|
|
|
|
def html = """
|
|
<html>
|
|
<head>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<style>
|
|
.container {
|
|
padding:25px;
|
|
}
|
|
.flex1 {
|
|
width:33%;
|
|
float:left;
|
|
text-align: center;
|
|
}
|
|
p {
|
|
font-size: 2em;
|
|
font-family: Verdana, Geneva, sans-serif;
|
|
text-align: center;
|
|
color: #777;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="flex1"><img src="https://dashboard.myplantlink.com/images/PLlogo.png" alt="PlantLink" height="75"/></div>
|
|
<div class="flex1"><img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected to" height="25" style="padding-top:25px;" /></div>
|
|
<div class="flex1"><img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings" height="75"/></div>
|
|
<br clear="all">
|
|
</div>
|
|
<div class="container">
|
|
<p>Your PlantLink Account is now connected to SmartThings!</p>
|
|
<p style="color:green;">Click <strong>Done</strong> at the top right to finish setup.</p>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
render contentType: 'text/html', data: html
|
|
}
|
|
|
|
private refreshAuthToken() {
|
|
def stcid = appSettings.client_id
|
|
def refreshParams = [
|
|
method: 'POST',
|
|
uri: "https://hardware-dot-oso-tech.appspot.com",
|
|
path: "/api/v1/oauth-token",
|
|
query: [grant_type:'refresh_token', code:"${atomicState.refreshToken}", client_id:stcid,
|
|
client_secret:appSettings.client_secret],
|
|
]
|
|
try{
|
|
def jsonMap
|
|
httpPost(refreshParams) { resp ->
|
|
if(resp.status == 200){
|
|
log.debug "OAuth Token refreshed"
|
|
jsonMap = resp.data
|
|
if (resp.data) {
|
|
atomicState.refreshToken = resp?.data?.refresh_token
|
|
atomicState.authToken = resp?.data?.access_token
|
|
if (data?.action && data?.action != "") {
|
|
log.debug data.action
|
|
"{data.action}"()
|
|
data.action = ""
|
|
}
|
|
}
|
|
data.action = ""
|
|
}else{
|
|
log.debug "refresh failed ${resp.status} : ${resp.status.code}"
|
|
}
|
|
}
|
|
}
|
|
catch(Exception e){
|
|
log.debug "caught exception refreshing auth token: " + e
|
|
}
|
|
}
|
|
|
|
def parse_api_response(resp, message) {
|
|
if (resp.status == 200) {
|
|
return true
|
|
} else {
|
|
log.error "sent ${message} Json & got http status ${resp.status} - ${resp.status.code}"
|
|
if (resp.status == 401) {
|
|
refreshAuthToken()
|
|
return false
|
|
} else {
|
|
debugEvent("Authentication error, invalid authentication method, lack of credentials, etc.", true)
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
def getServerUrl() {
|
|
return "https://graph.api.smartthings.com"
|
|
}
|
|
|
|
def debugEvent(message, displayEvent) {
|
|
def results = [
|
|
name: "appdebug",
|
|
descriptionText: message,
|
|
displayed: displayEvent
|
|
]
|
|
log.debug "Generating AppDebug Event: ${results}"
|
|
sendEvent (results)
|
|
}
|
|
|
|
def toQueryString(Map m){
|
|
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
|
|
} |