Compare commits

...

8 Commits

Author SHA1 Message Date
JANG JAEWON
70019075c2 MSA-1759: To use a motion sensor light only when the place is dark, normally we need an additional luminosity sensor.
This app activate motion sensor and light when designated lights are off.
2017-02-02 09:45:47 -08:00
Vinay Rao
de1894bfbf Merge pull request #1630 from SmartThingsCommunity/staging
Rolling down staging to master
2017-01-31 13:47:45 -08:00
Jason Botello
d0f8ec87bd Merge pull request #1616 from jasonbio/plantlink-exceptions
Plant link Exceptions
2017-01-30 15:01:18 -08:00
Jason Botello
1383ab1e07 Merge pull request #1615 from jasonbio/netatmo-exceptions
Netatmo Exceptions
2017-01-30 15:01:05 -08:00
Vinay Rao
d4f21b95d7 Merge pull request #1621 from varzac/zll-bulb-reporting
[DPROT-227] Manually refresh on/off status after setLevel
2017-01-27 16:37:34 -08:00
Zach Varberg
be2e19e406 Manually refresh on/off status after setLevel
This is because ZLL bulbs do not report or allow for configuration of
reporting of attributes.  As a result if we change a value, we have to
manually call a read of that attribute to update our status.

This is a part of: https://smartthings.atlassian.net/browse/DPROT-227
2017-01-26 11:27:51 -06:00
Jason Botello
41adc9777a Plant link Exceptions
Adding try/catch blocks to reduce HTTP exceptions due to rate limits or
unauthorized errors.
2017-01-24 14:53:23 -08:00
Jason Botello
f334f6505a Netatmo Exceptions
Adding try/catch blocks to reduce HTTP exceptions due to rate limits or
unauthorized errors. Also updating the “devicelist” endpoint to
“getstationdata” as the former is deprecated
2017-01-24 14:30:59 -08:00
8 changed files with 194 additions and 99 deletions

View File

@@ -82,7 +82,7 @@ def on() {
}
def setLevel(value) {
zigbee.setLevel(value) + ["delay 500"] + zigbee.levelRefresh() //adding refresh because of ZLL bulb not conforming to send-me-a-report
zigbee.setLevel(value) + zigbee.onOffRefresh() + zigbee.levelRefresh() //adding refresh because of ZLL bulb not conforming to send-me-a-report
}
/**

View File

@@ -89,7 +89,7 @@ def on() {
}
def setLevel(value) {
zigbee.setLevel(value) + ["delay 1500"] + zigbee.levelRefresh() //adding refresh because of ZLL bulb not conforming to send-me-a-report
zigbee.setLevel(value) + zigbee.onOffRefresh() + zigbee.levelRefresh() //adding refresh because of ZLL bulb not conforming to send-me-a-report
}
def refresh() {

View File

@@ -115,7 +115,7 @@ def refreshAttributes() {
}
def setLevel(value) {
zigbee.setLevel(value) + ["delay 1500"] + zigbee.levelRefresh() //adding refresh because of ZLL bulb not conforming to send-me-a-report
zigbee.setLevel(value) + zigbee.onOffRefresh() + zigbee.levelRefresh() //adding refresh because of ZLL bulb not conforming to send-me-a-report
}
def setColor(value){

View File

@@ -135,7 +135,7 @@ def setColorTemperature(value) {
}
def setLevel(value) {
zigbee.setLevel(value) + ["delay 1500"] + zigbee.levelRefresh() //adding refresh because of ZLL bulb not conforming to send-me-a-report
zigbee.setLevel(value) + zigbee.onOffRefresh() + zigbee.levelRefresh() //adding refresh because of ZLL bulb not conforming to send-me-a-report
}
def setColor(value){

View File

@@ -90,7 +90,7 @@ def on() {
}
def setLevel(value) {
zigbee.setLevel(value) + ["delay 1500"] + zigbee.levelRefresh()
zigbee.setLevel(value) + zigbee.onOffRefresh() + zigbee.levelRefresh()
}
def refresh() {

View File

@@ -73,7 +73,7 @@ def authPage() {
return dynamicPage(name: "Credentials", title: "Authorize Connection", nextPage:"listDevices", uninstall: uninstallAllowed, install:false) {
section() {
paragraph "Tap below to log in to the netatmo and authorize SmartThings access."
href url:redirectUrl, style:"embedded", required:false, title:"Connect to ${getVendorName()}:", description:description
href url:redirectUrl, style:"embedded", required:false, title:"Connect to ${getVendorName()}", description:description
}
}
} else {
@@ -146,19 +146,24 @@ def callback() {
// log.debug "PARAMS: ${params}"
httpPost(params) { resp ->
try {
httpPost(params) { resp ->
def slurper = new JsonSlurper()
def slurper = new JsonSlurper()
resp.data.each { key, value ->
def data = slurper.parseText(key)
state.refreshToken = data.refresh_token
state.authToken = data.access_token
state.tokenExpires = now() + (data.expires_in * 1000)
// log.debug "swapped token: $resp.data"
}
}
resp.data.each { key, value ->
def data = slurper.parseText(key)
log.debug "Data: $data"
state.refreshToken = data.refresh_token
state.authToken = data.access_token
//state.accessToken = data.access_token
state.tokenExpires = now() + (data.expires_in * 1000)
// log.debug "swapped token: $resp.data"
}
}
} catch (Exception e) {
log.debug "callback: Call failed $e"
}
// Handle success and failure here, and render stuff accordingly
if (state.authToken) {
@@ -387,18 +392,18 @@ def getDeviceList() {
state.deviceDetail = [:]
state.deviceState = [:]
apiGet("/api/devicelist") { response ->
apiGet("/api/getstationsdata") { response ->
response.data.body.devices.each { value ->
def key = value._id
deviceList[key] = "${value.station_name}: ${value.module_name}"
state.deviceDetail[key] = value
state.deviceState[key] = value.dashboard_data
}
response.data.body.modules.each { value ->
def key = value._id
deviceList[key] = "${state.deviceDetail[value.main_device].station_name}: ${value.module_name}"
state.deviceDetail[key] = value
state.deviceState[key] = value.dashboard_data
value.modules.each { value2 ->
def key2 = value2._id
deviceList[key2] = "${value.station_name}: ${value2.module_name}"
state.deviceDetail[key2] = value2
state.deviceState[key2] = value2.dashboard_data
}
}
}
@@ -448,6 +453,7 @@ def listDevices() {
}
def apiGet(String path, Map query, Closure callback) {
if(now() >= state.tokenExpires) {
refreshToken();
}
@@ -467,12 +473,16 @@ def apiGet(String path, Map query, Closure callback) {
} catch (Exception e) {
// This is most likely due to an invalid token. Try to refresh it and try again.
log.debug "apiGet: Call failed $e"
if(refreshToken()) {
log.debug "apiGet: Trying again after refreshing token"
httpGet(params) { response ->
callback.call(response)
}
}
if(refreshToken()) {
log.debug "apiGet: Trying again after refreshing token"
try {
httpGet(params) { response ->
callback.call(response)
}
} catch (Exception f) {
log.debug "apiGet: Call failed $f"
}
}
}
}
@@ -561,4 +571,4 @@ private Boolean hasAllHubsOver(String desiredFirmware) {
private List getRealHubFirmwareVersions() {
return location.hubs*.firmwareVersionString.findAll { it }
}
}

View File

@@ -0,0 +1,61 @@
/**
* Smart Motion Light
*
* Copyright 2017 JANG JAEWON
*
* 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: "Smart Motion Light",
namespace: "ipse",
author: "JANG JAEWON",
description: "Activate motion sensor light only when specific lights are off. Light turns off in 1 minute.",
category: "Convenience",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png")
preferences {
section ("Where") {
input "motion1", "capability.motionSensor", title: "Which Motion Sensor?",required:true
}
section ("When these lights are off...") {
input "darkSwitches", "capability.switch", title: "Which?",required:true,multiple:true
}
section ("Turn on this light...") {
input "onSwitch", "capability.switch", title: "Which?",required:true
}
}
def installed()
{
subscribe(motion1, "motion.active", motionActiveHandler)
}
def updated()
{
unsubscribe()
subscribe(motion1, "motion.active", motionActiveHandler)
}
def motionActiveHandler(evt) {
def currSwitches = darkSwitches.currentSwitch
def countSwitches = currSwitches.findAll{it == "off"?true:false}
if (darkSwitches.size() == countSwitches.size()) {
onSwitch.on()
runIn(60,turnOff)
}
}
def turnOff(){
onSwitch.off()
}

View File

@@ -57,7 +57,7 @@ def authPage(){
atomicState.accessToken = state.accessToken
}
def redirectUrl = oauthInitUrl()
def redirectUrl = oauthInitUrl()
def uninstallAllowed = false
def oauthTokenProvided = false
if(atomicState.authToken){
@@ -78,9 +78,9 @@ def authPage(){
}
}else{
return dynamicPage(name: "auth", title: "Step 1 of 2 - Completed", nextPage:"deviceList", uninstall:uninstallAllowed) {
section(){
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")
href(url:redirectUrl, title:"Or", description:"tap to switch accounts")
}
}
}
@@ -137,36 +137,44 @@ def dock_sensor(device_serial, expected_plant_name) {
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
try {
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()
try {
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
}
}
} catch (Exception f) {
log.debug "call failed $f"
}
} 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)
}
} 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)
}
}
} catch (Exception e) {
log.debug "call failed $e"
}
return true
}
def checkAndUpdatePlantIfNeeded(plant, expected_plant_name){
def plant_put_params = [
uri : appSettings.https_plantLinkServer,
uri : appSettings.https_plantLinkServer,
headers : ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
contentType : "application/json"
]
@@ -174,12 +182,16 @@ def checkAndUpdatePlantIfNeeded(plant, 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
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')
try {
httpPut(plant_put_params) { plant_put_response ->
parse_api_response(plant_put_response, 'updating plant name')
}
} catch (Exception e) {
log.debug "call failed $e"
}
}
}
@@ -198,25 +210,29 @@ def moistureHandler(event){
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)
try {
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)
}
}
}
}
}
} catch (Exception e) {
log.debug "call failed $e"
}
}
}
@@ -235,8 +251,12 @@ def batteryHandler(event){
contentType: "application/json",
body: event.value
]
httpPost(measurement_post_params) { measurement_post_response ->
parse_api_response(measurement_post_response, 'creating battery measurement')
try {
httpPost(measurement_post_params) { measurement_post_response ->
parse_api_response(measurement_post_response, 'creating battery measurement')
}
} catch (Exception e) {
log.debug "call failed $e"
}
}
}
@@ -248,7 +268,7 @@ def getDeviceSerialFromEvent(event){
}
def oauthInitUrl(){
atomicState.oauthInitState = UUID.randomUUID().toString()
atomicState.oauthInitState = UUID.randomUUID().toString()
def oauthParams = [
response_type: "code",
client_id: appSettings.client_id,
@@ -275,8 +295,12 @@ def swapToken(){
]
def jsonMap
httpPost(postParams) { resp ->
jsonMap = resp.data
try {
httpPost(postParams) { resp ->
jsonMap = resp.data
}
} catch (Exception e) {
log.debug "call failed $e"
}
atomicState.refreshToken = jsonMap.refresh_token
@@ -287,33 +311,33 @@ def swapToken(){
<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;
}
.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 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>
<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>
"""