mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-22 05:10:52 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08ed9170d4 |
@@ -0,0 +1,195 @@
|
|||||||
|
/**
|
||||||
|
* Climax PSM-29ZBSR - Zigbee - Power Outlet Switch and Energy Meter
|
||||||
|
*
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* Thanks to SmartThings and its community!
|
||||||
|
*
|
||||||
|
* Product Page: http://www.climax.com.tw/psm29zb-zb.php
|
||||||
|
* Developer manual https://fccid.io/document.php?id=2553357
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Climax Power Outlet", namespace: "bortuzar", author: "bortuzar") {
|
||||||
|
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Power Meter"
|
||||||
|
|
||||||
|
fingerprint endpointId: "0x0A", profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0702", outClusters: "0000"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
// status messages
|
||||||
|
status "on": "on/off: 1"
|
||||||
|
status "off": "on/off: 0"
|
||||||
|
|
||||||
|
// reply messages
|
||||||
|
reply "zcl on-off on": "on/off: 1"
|
||||||
|
reply "zcl on-off off": "on/off: 0"
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
valueTile("power", "device.power", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "power", label: '${currentValue} W'
|
||||||
|
}
|
||||||
|
|
||||||
|
//standardTile("configure", "device.power", inactiveLabel: false, decoration: "flat") {
|
||||||
|
// state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
main(["switch", "power"])
|
||||||
|
details(["switch", "power", "refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description) {
|
||||||
|
|
||||||
|
log.debug "Parse Method Called"
|
||||||
|
log.trace description
|
||||||
|
|
||||||
|
if (description?.startsWith("catchall:")) {
|
||||||
|
def msg = zigbee.parse(description)
|
||||||
|
log.trace "data: $msg.data"
|
||||||
|
|
||||||
|
} else if (description?.startsWith("read attr -")) {
|
||||||
|
def descMap = parseDescriptionAsMap(description)
|
||||||
|
log.debug "Desc Map:"
|
||||||
|
log.debug descMap
|
||||||
|
|
||||||
|
if (descMap.cluster == "0006" && descMap.attrId == "0000") {
|
||||||
|
name = "switch"
|
||||||
|
value = descMap.value.endsWith("01") ? "on" : "off"
|
||||||
|
def result = createEvent(name: name, value: value)
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
} else if(descMap.cluster =="0702" && descMap.attrId == "0400") {
|
||||||
|
|
||||||
|
def value = convertHexToInt(descMap.value[-4..-1])/10
|
||||||
|
// Reading the last 4 characters of the string...Maybe 4 are needed. Needs further test.
|
||||||
|
// Dividing by 10 as the Divisor is 10000 and unit is kW for the device. AttrId: 0302 and 0300. Simplifying to 10
|
||||||
|
log.debug value
|
||||||
|
def name = "power"
|
||||||
|
def result = createEvent(name: name, value: value)
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
def name = description?.startsWith("on/off: ") ? "switch" : null
|
||||||
|
def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null
|
||||||
|
def result = createEvent(name: name, value: value)
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
log.debug "On Method called"
|
||||||
|
sendEvent(name: "switch", value: "on")
|
||||||
|
"st cmd 0x${device.deviceNetworkId} 1 6 1 {}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
log.debug "Off Method called"
|
||||||
|
sendEvent(name: "switch", value: "off")
|
||||||
|
"st cmd 0x${device.deviceNetworkId} 1 6 0 {}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
log.debug "Refresh Method called";
|
||||||
|
//read attribute 1024Decimal, translates to 400Hex.
|
||||||
|
[
|
||||||
|
"st rattr 0x${device.deviceNetworkId} 1 6 0", "delay 500",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} 1 0x0702 1024", , "delay 500"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
|
||||||
|
log.debug "Configure Method called"
|
||||||
|
|
||||||
|
String zigbeeId = swapEndianHex(device.hub.zigbeeId)
|
||||||
|
|
||||||
|
def configCmds = [
|
||||||
|
|
||||||
|
//Switch Reporting
|
||||||
|
"zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1", "delay 1000",
|
||||||
|
|
||||||
|
//bing to cluster 0x006. Switch On-Off
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 1000",
|
||||||
|
|
||||||
|
//bind to cluster 0x702. Power Consumption
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0702 {${device.zigbeeId}} {}", "delay 500",
|
||||||
|
|
||||||
|
]
|
||||||
|
return configCmds + refresh() // send refresh cmds as part of config
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parseDescriptionAsMap(description) {
|
||||||
|
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||||
|
def nameAndValue = param.split(":")
|
||||||
|
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private hex(value, width=2) {
|
||||||
|
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||||
|
while (s.size() < width) {
|
||||||
|
s = "0" + s
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer convertHexToInt(hex) {
|
||||||
|
Integer.parseInt(hex,16)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private String swapEndianHex(String hex) {
|
||||||
|
reverseArray(hex.decodeHex()).encodeHex()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private byte[] reverseArray(byte[] array) {
|
||||||
|
int i = 0;
|
||||||
|
int j = array.length - 1;
|
||||||
|
byte tmp;
|
||||||
|
while (j > i) {
|
||||||
|
tmp = array[j];
|
||||||
|
array[j] = array[i];
|
||||||
|
array[i] = tmp;
|
||||||
|
j--;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return array
|
||||||
|
}
|
||||||
@@ -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()]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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("&")
|
|
||||||
}
|
|
||||||
@@ -50,7 +50,7 @@ preferences {
|
|||||||
}
|
}
|
||||||
section("Send Notifications?") {
|
section("Send Notifications?") {
|
||||||
input("recipients", "contact", title: "Send notifications to") {
|
input("recipients", "contact", title: "Send notifications to") {
|
||||||
input "phone", "phone", title: "Send an SMS to this number?", required:false
|
input "phone", "phone", title: "Send an SMS to this number?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,9 +266,7 @@ def sendAway(msg) {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
sendPush(msg)
|
sendPush(msg)
|
||||||
if(phone){
|
sendSms(phone, msg)
|
||||||
sendSms(phone, msg)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,9 +280,7 @@ def sendHome(msg) {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
sendPush(msg)
|
sendPush(msg)
|
||||||
if(phone){
|
sendSms(phone, msg)
|
||||||
sendSms(phone, msg)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,4 +339,4 @@ private getTimeIntervalLabel() {
|
|||||||
|
|
||||||
private hideOptionsSection() {
|
private hideOptionsSection() {
|
||||||
(starting || ending || days || modes) ? false: true
|
(starting || ending || days || modes) ? false: true
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user