Compare commits

..

1 Commits

3 changed files with 300 additions and 552 deletions

View File

@@ -0,0 +1,300 @@
/**
* Copyright 2015 Eurotronic
*
* 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.
*
*/
metadata {
definition (name: "Eurotronic Z-Wave Thermostatic Valve", namespace: "Eurotronic", author: "Eurotronic") {
capability "Actuator"
capability "Temperature Measurement"
capability "Thermostat Heating Setpoint"
capability "Thermostat Mode"
capability "Thermostat Setpoint"
capability "Sensor"
capability "Switch Level"
command "energySave"
command "manual"
command "setWakeUpInterval", ["number"]
command "setEnergySavingSetpoint", ["number"]
attribute "energySavingSetpoint", "number"
fingerprint deviceId:"0x0806", inClusters:"0x43, 0x40, 0x31, 0x20, 0x26, 0x77, 0x80, 0x84, 0x72, 0x86"
}
tiles(scale: 1) {
valueTile("temperature", "device.temperature", width: 2, height: 2) {
state("a", label:'${currentValue}°', icon: "st.Weather.weather2",
backgroundColors:[
[value: 0, color: "#153591"],
[value: 7, color: "#1e9cbb"],
[value: 15, color: "#90d2a7"],
[value: 23, color: "#44b621"],
[value: 29, color: "#f1d801"],
[value: 35, color: "#d04e00"],
]
)
}
standardTile("mode", "device.thermostatMode") {
state("off", label:'${name}', action:"heat", nextState:"heat", backgroundColor:"#808080", icon:"st.Seasonal Fall.seasonal-fall-008")
state("heat", label:'${name}', action:"energySave", nextState:"energy save", backgroundColor:"#fdd470", icon:"st.Seasonal Winter.seasonal-winter-009")
state("energy save", label:'save', action:"manual", nextState:"manual", backgroundColor:"#afd574", icon:"st.Transportation.transportation1")
state("manual", label: '${name}', action:"off", nextState:"off", backgroundColor:"#8f00ff", icon:"st.Outdoor.outdoor18")
}
valueTile("battery", "device.battery", width: 1, height: 1) {
state ("battery", label:'${currentValue}%', unit:"%", icon: "st.Health & Wellness.health9",
backgroundColors:[
[value: 0, color: "#ce1010"],
[value: 50, color: "#3535ba"],
[value: 100, color: "#0d740d"]
]
)
}
controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, range:"(13..50)") {
state "a", action:"setHeatingSetpoint", backgroundColor:"#fdd470"
}
valueTile("heatingSetpoint", "device.heatingSetpoint") {
state ("a", label:'${currentValue}°', backgroundColor:"#fdd470")
}
controlTile("energySavingControl", "device.energySavingSetpoint", "slider", height: 1, width: 2, range:"(13..50)") {
state ("a", action:"setEnergySavingSetpoint", backgroundColor:"#afd574")
}
valueTile("energySavingSetpoint", "device.energySavingSetpoint" ) {
state ("a", label:'${currentValue}°', backgroundColor:"#afd574")
}
controlTile("switchControlSlider", "device.switch", "slider", height: 1, width: 2) {
state ("a", action:"setLevel", backgroundColor: "#8f00ff")
}
valueTile("switchValue", "device.switch") {
state ("a", label:'${currentValue}%', backgroundColor:"#8f00ff")
}
main "temperature"
details(["temperature", "mode", "battery", "heatSliderControl", "heatingSetpoint", "energySavingControl", "energySavingSetpoint", "switchControlSlider", "switchValue"])
}
}
def parse(String description)
{
log.debug "Parse description: $description"
def result = []
if (state.wakeUpInterval != 240 || state.nodeid != zwaveHubNodeId) {
result << response(zwave.wakeUpV2.wakeUpIntervalSet(nodeid: zwaveHubNodeId, seconds: 240))
result << response(zwave.wakeUpV2.wakeUpIntervalGet().format())
}
def zwe = []
if (description != "updated") {
zwe = zwaveEvent(zwave.parse(description, [0x20: 1, 0x26: 3, 0x31: 4, 0x40: 2, 0x43: 2, 0x77: 1, 0x80: 1, 0x84: 2, 0x72: 1, 0x86: 1]))
}
return result + zwe
}
// Multilevel Switch
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd){
log.debug "Switch multilevel get $cmd"
if (cmd.value == 255)
cmd.value = 100
return createEvent(name: "switch", value: cmd.value)
}
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelStartLevelChange cmd){
log.debug "SwitchMultilevelStartLevelChange notification $cmd"
}
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelStopLevelChange cmd){
log.debug "SwitchMultilevelStopLevelChange notification $cmd"
}
//Multilevel Sensor
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv4.SensorMultilevelReport cmd){
log.debug "Sensor multilevel get $cmd"
def map = [:]
if (cmd.sensorType == 1) {
map.value = cmd.scaledSensorValue
map.unit = cmd.scale
map.name = "temperature"
}
return createEvent(map)
}
//Thermostat
def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport cmd){
log.debug "Thermostat mode notification $cmd"
def map = [:]
switch (cmd.mode) {
case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_OFF:
map.value = "off"
break
case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_HEAT:
map.value = "heat"
break
case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_ENERGY_SAVE_HEAT:
map.value = "energy save"
break
case 0x1F:
map.value = "manual"
break
}
map.name = "thermostatMode"
return createEvent(map)
}
def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd){
log.debug "ThermostatSetpointReport notification $cmd"
def map = [:]
map.value = cmd.scaledValue
map.unit = cmd.scale
switch (cmd.setpointType) {
case 1:
map.name = "heatingSetpoint"
break;
case 11:
map.name = "energySavingSetpoint"
break;
default:
return [:]
}
// So we can respond with same format
state.size = cmd.size
state.scale = cmd.scale
state.precision = cmd.precision
return createEvent(map)
}
//Battery
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd){
log.debug "Battery notification $cmd"
return createEvent(name: "battery", value: cmd.batteryLevel)
}
//Wake up
def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpIntervalReport cmd){
log.debug "WakeUpInterval notification $cmd"
state.nodeid = cmd.nodeid
state.wakeUpInterval = cmd.seconds
return createEvent(name: "wakeUpIntervalSeconds", value: cmd.seconds)
}
def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd){
log.debug "Wake up notification $cmd $state"
def result = []
if (state.modeCommand) {
result << response(state.modeCommand)
result << response(zwave.thermostatModeV2.thermostatModeGet().format())
state.modeCommand = null
}
if (state.setLevelCommand) {
result << response(state.setLevelCommand)
result << response(zwave.switchMultilevelV3.switchMultilevelGet().format())
state.setLevelCommand = null
}
if (state.setHeatingSetpointCommand) {
result << response(state.setHeatingSetpointCommand)
result << response(zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: 1).format())
state.setHeatingSetpointCommand = null
}
if (state.setEnergySavingSetpointCommand) {
result << response(state.setEnergySavingSetpointCommand)
result << response(zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: 11).format())
state.setEnergySavingSetpointCommand = null
}
//For some reason response("delay 1200") is not functional which is really bad because hub is
//not able to request all the information from device which it needs. All the commands are
//sent at the same time.
result << response(zwave.sensorMultilevelV4.sensorMultilevelGet().format())
result << response(zwave.batteryV1.batteryGet().format())
result << response(zwave.switchMultilevelV3.switchMultilevelGet().format())
//result << response(zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: 1).format())
//result << response(zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: 11).format())
//result << response(zwave.thermostatModeV2.thermostatModeGet().format())
result << response(zwave.wakeUpV1.wakeUpNoMoreInformation().format())
result
}
//Unexpected
def zwaveEvent(physicalgraph.zwave.Command cmd) {
log.warn "Unexpected zwave command $cmd"
}
// Command Implementations
def off() {
log.debug "Thermostat mode setting to off"
state.modeCommand = zwave.thermostatModeV2.thermostatModeSet(mode: 0).format()
}
def heat() {
log.debug "Thermostat mode setting to heat"
state.modeCommand = zwave.thermostatModeV2.thermostatModeSet(mode: 1).format()
}
def energySave() {
log.debug "Thermostat mode setting to energy save"
state.modeCommand = zwave.thermostatModeV2.thermostatModeSet(mode: 11).format()
}
def manual() {
log.debug "Thermostat mode setting to manual"
state.modeCommand = zwave.thermostatModeV2.thermostatModeSet(mode: 31).format()
}
def setLevel(double lvl){
//by Eurotronic specification 100% is 0xFF and 99% is 0x63
if (lvl == 100.0)
lvl = 255
log.debug "Changing switch level to $lvl"
state.setLevelCommand = zwave.switchMultilevelV3.switchMultilevelSet(value: 256).format()
}
def setHeatingSetpoint(double sp){
log.debug "Setting setpoint to $sp"
def map = [
precision: 1,
scale: 0,
scaledValue: sp,
setpointType: 1,
size: 2,
]
state.setHeatingSetpointCommand = zwave.thermostatSetpointV2.thermostatSetpointSet(map).format()
}
def setEnergySavingSetpoint(double sp){
log.debug "Setting energy saving setpoint to $sp"
def map = [
precision: 1,
scale: 0,
scaledValue: sp,
setpointType: 11,
size: 2,
]
state.setEnergySavingSetpointCommand = zwave.thermostatSetpointV2.thermostatSetpointSet(map).format()
}

View File

@@ -1,163 +0,0 @@
/**
* PlantLink
*
* This device type takes sensor data and converts it into a json packet to send to myplantlink.com
* where its values will be computed for soil and plant type to show user readable values of how your
* specific plant is doing.
*
*
* 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
metadata {
definition (name: "PlantLink", namespace: "OsoTech", author: "Oso Technologies") {
capability "Sensor"
command "setStatusIcon"
command "setPlantFuelLevel"
command "setBatteryLevel"
command "setInstallSmartApp"
attribute "plantStatus","string"
attribute "plantFuelLevel","number"
attribute "linkBatteryLevel","string"
attribute "installSmartApp","string"
fingerprint profileId: "0104", inClusters: "0000,0001,0B04"
}
simulator {
status "battery": "read attr - raw: C0720100010A000021340A, dni: C072, endpoint: 01, cluster: 0001, size: 0A, attrId: 0000, encoding: 21, value: 0a34"
status "moisture": "read attr - raw: C072010B040A0001290000, dni: C072, endpoint: 01, cluster: 0B04, size: 0A, attrId: 0100, encoding: 29, value: 0000"
}
tiles {
standardTile("Title", "device.label") {
state("label", label:'PlantLink ${device.label}')
}
valueTile("plantMoistureTile", "device.plantFuelLevel", width: 1, height: 1) {
state("plantMoisture", label: '${currentValue}% Moisture')
}
valueTile("plantStatusTextTile", "device.plantStatus", decoration: "flat", width: 2, height: 2) {
state("plantStatusTextTile", label:'${currentValue}')
}
valueTile("battery", "device.linkBatteryLevel" ) {
state("battery", label:'${currentValue}% battery')
}
valueTile("installSmartApp","device.installSmartApp", decoration: "flat", width: 3, height: 1) {
state "needSmartApp", label:'Please install SmartApp "Required PlantLink Connector"', defaultState:true
state "connectedToSmartApp", label:'Connected to myplantlink.com'
}
main "plantStatusTextTile"
details(['plantStatusTextTile', "plantMoistureTile", "battery", "installSmartApp"])
}
}
def setStatusIcon(value){
def status = ''
switch (value) {
case '0':
status = 'Needs Water'
break
case '1':
status = 'Dry'
break
case '2':
case '3':
status = 'Good'
break
case '4':
status = 'Too Wet'
break
case 'No Soil':
status = 'Too Dry'
setPlantFuelLevel(0)
break
case 'Recently Watered':
status = 'Watered'
setPlantFuelLevel(100)
break
case 'Low Battery':
status = 'Low Battery'
break
case 'Waiting on First Measurement':
status = 'Calibrating'
break
default:
status = "?"
break
}
sendEvent("name":"plantStatus", "value":status, "description":statusText, displayed: true, isStateChange: true)
}
def setPlantFuelLevel(value){
sendEvent("name":"plantFuelLevel", "value":value, "description":statusText, displayed: true, isStateChange: true)
}
def setBatteryLevel(value){
sendEvent("name":"linkBatteryLevel", "value":value, "description":statusText, displayed: true, isStateChange: true)
}
def setInstallSmartApp(value){
sendEvent("name":"installSmartApp", "value":value)
}
def parse(String description) {
def description_map = parseDescriptionAsMap(description)
def event_name = ""
def measurement_map = [
type: "link",
signal: "0x00",
zigbeedeviceid: device.zigbeeId,
created: new Date().time /1000 as int
]
if (description_map.cluster == "0000"){
/* version number, not used */
} else if (description_map.cluster == "0001"){
/* battery voltage in mV (device needs minimium 2.1v to run) */
log.debug "PlantLink - id ${device.zigbeeId} battery ${description_map.value}"
event_name = "battery_status"
measurement_map["battery"] = "0x${description_map.value}"
} else if (description_map.cluster == "0B04"){
/* raw moisture reading (needs to be sent to plantlink for soil/plant type conversion) */
log.debug "PlantLink - id ${device.zigbeeId} raw moisture ${description_map.value}"
measurement_map["moisture"] = "0x${description_map.value}"
event_name = "moisture_status"
} else{
log.debug "PlantLink - id ${device.zigbeeId} Unknown '${description}'"
return
}
def json_builder = new JsonBuilder(measurement_map)
def result = createEvent(name: event_name, value: json_builder.toString())
return result
}
def parseDescriptionAsMap(description) {
(description - "read attr - ").split(",").inject([:]) { map, param ->
def nameAndValue = param.split(":")
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
}
}

View File

@@ -1,389 +0,0 @@
/**
* 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("&")
}