MSA-999: Plantlink is a sensor that monitors water moisture in a plant, to do this it take measurements and sends them to myplantlink.com where our servers calculate soil type, plant type, and past records to come up with a moisture value. Our servers also handle notification of users via email, sms, and push notifications to water their plant. This new updated version removes alot of the docking mechanism from the smart app (first time connecting a device and creating links/plants to upload to) and makes it just sends whatever values it has to our server and lets us automatically create what is needed. This fix will improve end users experience by auto correcting issues like a sensor sending up data to a plant that doesn't exist and creating multiple plants for one sensor.

This commit is contained in:
Oso Technologies
2016-03-25 18:08:49 -05:00
parent e5841fb3cb
commit 5a29c8efd1

View File

@@ -87,13 +87,10 @@ def authPage(){
} }
def installed() { def installed() {
log.debug "Installed with settings: ${settings}"
initialize() initialize()
} }
def updated() { def updated() {
log.debug "Updated with settings: ${settings}"
unsubscribe()
initialize() initialize()
} }
@@ -106,72 +103,24 @@ def uninstalled() {
} }
def initialize() { def initialize() {
atomicState.attached_sensors = [:] unsubscribe()
if (plantlinksensors){ if (plantlinksensors){
subscribe(plantlinksensors, "moisture_status", moistureHandler)
subscribe(plantlinksensors, "battery_status", batteryHandler)
plantlinksensors.each{ sensor_device -> plantlinksensors.each{ sensor_device ->
sensor_device.setStatusIcon("Waiting on First Measurement") subscribe(sensor_device, "moisture_status", moistureHandler)
subscribe(sensor_device, "battery_status", batteryHandler)
sensor_device.setInstallSmartApp("connectedToSmartApp") sensor_device.setInstallSmartApp("connectedToSmartApp")
} }
} }
} }
def dock_sensor(device_serial, expected_plant_name) { def updatePlantNameIfNeeded(plant, 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 = [ def plant_put_params = [
uri : appSettings.https_plantLinkServer, uri : appSettings.https_plantLinkServer,
headers : ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"], headers : ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
contentType : "application/json" contentType : "application/json"
] ]
if (plant.name != expected_plant_name) { if (plant.name != expected_plant_name) {
log.debug "updating plant for - ${expected_plant_name}" log.debug "renaming plant ${plant.key} - ${expected_plant_name}"
plant_put_params["path"] = "/api/v1/plants/${plant.key}" plant_put_params["path"] = "/api/v1/plants/${plant.key}"
def plant_put_body_map = [ def plant_put_body_map = [
name: expected_plant_name name: expected_plant_name
@@ -185,32 +134,39 @@ def checkAndUpdatePlantIfNeeded(plant, expected_plant_name){
} }
def moistureHandler(event){ def moistureHandler(event){
def expected_plant_name = "SmartThings - ${event.displayName}" log.debug "moistureHandler - ${event.value}"
def device_serial = getDeviceSerialFromEvent(event)
if (!atomicState.attached_sensors.containsKey(device_serial)){ def expected_plant_name = "${event.displayName} (ST)"
dock_sensor(device_serial, expected_plant_name) def device_serial = getDeviceSerialFromEvent(event)
def device_battery = atomicState["battery${device_serial}"]
if ( device_battery == null){
log.error "Missing Battery Voltage - next cycle should have it"
}else{ }else{
// {"type":"link","signal":"0x00","zigbeedeviceid":"0022A3000003D75A","created":1458843686,"moisture":"0x1987"}
def appendedEventWithBatteryInfo = event.value.replace('}',",\"battery\":\"${device_battery}\"}")
log.debug "payload - ${appendedEventWithBatteryInfo}"
def measurement_post_params = [ def measurement_post_params = [
uri: appSettings.https_plantLinkServer, uri: appSettings.https_plantLinkServer,
path: "/api/v1/smartthings/links/${device_serial}/measurements", path: "/api/v1/smartthings/links/${device_serial}/measurements",
headers: ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"], headers: ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
contentType: "application/json", contentType: "application/json",
body: event.value body: appendedEventWithBatteryInfo
] ]
httpPost(measurement_post_params) { measurement_post_response -> httpPost(measurement_post_params) { measurement_post_response ->
if (parse_api_response(measurement_post_response, 'creating moisture measurement') && if (parse_api_response(measurement_post_response, 'creating moisture measurement') && measurement_post_response.data.size() >0){
measurement_post_response.data.size() >0){
def measurement = measurement_post_response.data[0] def measurement = measurement_post_response.data[0]
def plant = measurement.plant def plant = measurement.plant
log.debug plant
checkAndUpdatePlantIfNeeded(plant, expected_plant_name) updatePlantNameIfNeeded(plant, expected_plant_name)
plantlinksensors.each{ sensor_device -> plantlinksensors.each{ sensor_device ->
if (sensor_device.id == event.deviceId){ if (sensor_device.id == event.deviceId){
sensor_device.setStatusIcon(plant.status) sensor_device.setStatusIcon(plant.status)
if (plant.last_measurements && plant.last_measurements[0].moisture){ if (plant.last_measurements && plant.last_measurements[0].moisture){
sensor_device.setPlantFuelLevel(plant.last_measurements[0].moisture * 100 as int) sensor_device.setPlantFuelLevel(plant.last_measurements[0].moisture * 100 as int)
} }
if (plant.last_measurements && plant.last_measurements[0].battery){ if (plant.last_measurements && plant.last_measurements[0].battery){
sensor_device.setBatteryLevel(plant.last_measurements[0].battery * 100 as int) sensor_device.setBatteryLevel(plant.last_measurements[0].battery * 100 as int)
} }
@@ -224,21 +180,7 @@ def moistureHandler(event){
def batteryHandler(event){ def batteryHandler(event){
def expected_plant_name = "SmartThings - ${event.displayName}" def expected_plant_name = "SmartThings - ${event.displayName}"
def device_serial = getDeviceSerialFromEvent(event) def device_serial = getDeviceSerialFromEvent(event)
atomicState["battery${device_serial}"] = getDeviceBatteryFromEvent(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 getDeviceSerialFromEvent(event){
@@ -247,6 +189,12 @@ def getDeviceSerialFromEvent(event){
return match_result[0][1] return match_result[0][1]
} }
def getDeviceBatteryFromEvent(event){
def pattern = /.*"battery"\s*:\s*"(\w+)".*/
def match_result = (event.value =~ pattern)
return match_result[0][1]
}
def oauthInitUrl(){ def oauthInitUrl(){
atomicState.oauthInitState = UUID.randomUUID().toString() atomicState.oauthInitState = UUID.randomUUID().toString()
def oauthParams = [ def oauthParams = [
@@ -263,12 +211,13 @@ def buildRedirectUrl(){
} }
def swapToken(){ def swapToken(){
log.debug "PlantLink Connector - OAuth Token"
def code = params.code def code = params.code
def oauthState = params.state def oauthState = params.state
def stcid = appSettings.client_id def stcid = appSettings.client_id
def postParams = [ def postParams = [
method: 'POST', method: 'POST',
uri: "https://oso-tech.appspot.com", uri: appSettings.https_plantLinkServer,
path: "/api/v1/oauth-token", path: "/api/v1/oauth-token",
query: [grant_type:'authorization_code', code:params.code, client_id:stcid, query: [grant_type:'authorization_code', code:params.code, client_id:stcid,
client_secret:appSettings.client_secret, redirect_uri: buildRedirectUrl()], client_secret:appSettings.client_secret, redirect_uri: buildRedirectUrl()],
@@ -321,10 +270,11 @@ def swapToken(){
} }
private refreshAuthToken() { private refreshAuthToken() {
log.debug "PlantLink Connector - Refresh OAuth"
def stcid = appSettings.client_id def stcid = appSettings.client_id
def refreshParams = [ def refreshParams = [
method: 'POST', method: 'POST',
uri: "https://hardware-dot-oso-tech.appspot.com", uri: appSettings.https_plantLinkServer,
path: "/api/v1/oauth-token", path: "/api/v1/oauth-token",
query: [grant_type:'refresh_token', code:"${atomicState.refreshToken}", client_id:stcid, query: [grant_type:'refresh_token', code:"${atomicState.refreshToken}", client_id:stcid,
client_secret:appSettings.client_secret], client_secret:appSettings.client_secret],
@@ -333,7 +283,6 @@ private refreshAuthToken() {
def jsonMap def jsonMap
httpPost(refreshParams) { resp -> httpPost(refreshParams) { resp ->
if(resp.status == 200){ if(resp.status == 200){
log.debug "OAuth Token refreshed"
jsonMap = resp.data jsonMap = resp.data
if (resp.data) { if (resp.data) {
atomicState.refreshToken = resp?.data?.refresh_token atomicState.refreshToken = resp?.data?.refresh_token
@@ -346,12 +295,12 @@ private refreshAuthToken() {
} }
data.action = "" data.action = ""
}else{ }else{
log.debug "refresh failed ${resp.status} : ${resp.status.code}" log.debug "PlantLink Server - ${resp.status} : ${resp.status.code}"
} }
} }
} }
catch(Exception e){ catch(Exception e){
log.debug "caught exception refreshing auth token: " + e log.debug "PlantLink Connector - OAuth Refresh Failed: " + e
} }
} }
@@ -364,7 +313,7 @@ def parse_api_response(resp, message) {
refreshAuthToken() refreshAuthToken()
return false return false
} else { } else {
debugEvent("Authentication error, invalid authentication method, lack of credentials, etc.", true) debugEvent("Plantlink Error: ${resp.status} - ${resp.status.code}", true)
return false return false
} }
} }