Compare commits

...

18 Commits

Author SHA1 Message Date
Oso Technologies
5a29c8efd1 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. 2016-03-25 18:08:49 -05:00
Amol Mundayoor
e5841fb3cb Merge pull request #702 from larsfinander/DVCSMP-1667-Update-Hue-user-creation
DVCSMP-1667 Authentication needs to be updated for Philips Hue
2016-03-25 11:28:19 -07:00
Lars Finander
805b870447 DVCSMP-1667 Authentication needs to be updated for Philips Hue 2016-03-25 10:12:48 -07:00
Vinay Rao
10245315ee Merge pull request #697 from SmartThingsCommunity/staging
Rolling down staging commits to master
2016-03-24 13:22:33 -07:00
Vinay Rao
0b239d4686 Merge pull request #696 from SmartThingsCommunity/revert-668-staging_remove_hue
Revert "Revert "DVCSMP-1615 & DEVC-372""
2016-03-24 13:19:48 -07:00
Vinay Rao
9374290d64 Revert "Revert "DVCSMP-1615 & DEVC-372"" 2016-03-24 13:12:23 -07:00
Vinay Rao
c45129170a Merge pull request #668 from larsfinander/staging_remove_hue
Revert "DVCSMP-1615 & DEVC-372"
2016-03-24 12:56:35 -07:00
Lars Finander
53406ada8e Merge pull request #690 from larsfinander/DVCSMP-1615-Improve-Hue-error-handling
DVCSMP-1615 Hue setColor throws exceptions when missing parameters
2016-03-23 18:57:32 -07:00
Lars Finander
ffd0dd1545 DVCSMP-1615 Hue setColor throws exceptions when missing parameters
-DVCSMP-1637 Color picker set the light on but status not updated on the detail page
-DVCSMP-1638 Temperature control selection set the light on but status not updated on the detail page
-Added a lot of error checking for input parameters
-Fixed some data parsing exceptions
2016-03-23 16:10:11 -07:00
Vinay Rao
5c1236a21a Merge pull request #693 from SmartThingsCommunity/staging
[DVCSMP-1657] Syncing down changes from staging to master
2016-03-23 15:24:13 -07:00
Vinay Rao
0911651f71 Merge pull request #692 from workingmonk/bug/hotfix_multi
[DVCSMP-1657] Bug/hotfix multi
2016-03-23 15:19:18 -07:00
Vinay Rao
9cc92b1987 fixing issue with list of list of maps with multi parsing
adding return type to methods
2016-03-23 15:13:33 -07:00
Vinay Rao
1e27dc1824 Merge pull request #674 from workingmonk/feature/zigbee_library_refactor
[DPROT-20] ZigBee Refactor
2016-03-23 11:44:42 -07:00
Tom Manley
4bf3679942 Merge pull request #376 from tpmanley/feature/zigbee_refactor
[DPROT-20] Feature/zigbee refactor
2016-03-23 09:51:37 -05:00
Tom Manley
c714720578 Update zigbee-lock, zigbee-dimmer and zigbee-dimmer-power to use ZB lib API
https://smartthings.atlassian.net/browse/DPROT-20
2016-03-23 09:51:02 -05:00
Vinay Rao
281fc939ac refactoring code to get it inline with the new zigbee apis 2016-03-23 00:27:23 -07:00
Lars Finander
633bef2ac5 Revert "DVCSMP-1615 & DEVC-372"
This reverts commit 6fbef3b297.
(temporary for staging, MArch 25 deploy)
2016-03-22 14:29:32 -07:00
Vinay Rao
a8357e7644 Merge pull request #652 from SmartThingsCommunity/master
Rolling up changes from master to staging
2016-03-18 13:38:31 -07:00
11 changed files with 219 additions and 246 deletions

View File

@@ -103,62 +103,104 @@ void nextLevel() {
} }
void setLevel(percent) { void setLevel(percent) {
log.debug "Executing 'setLevel'" log.debug "Executing 'setLevel'"
parent.setLevel(this, percent) if (verifyPercent(percent)) {
sendEvent(name: "level", value: percent, descriptionText: "Level has changed to ${percent}%") parent.setLevel(this, percent)
sendEvent(name: "level", value: percent, descriptionText: "Level has changed to ${percent}%")
sendEvent(name: "switch", value: "on")
}
} }
void setSaturation(percent) { void setSaturation(percent) {
log.debug "Executing 'setSaturation'" log.debug "Executing 'setSaturation'"
parent.setSaturation(this, percent) if (verifyPercent(percent)) {
sendEvent(name: "saturation", value: percent, displayed: false) parent.setSaturation(this, percent)
sendEvent(name: "saturation", value: percent, displayed: false)
}
} }
void setHue(percent) { void setHue(percent) {
log.debug "Executing 'setHue'" log.debug "Executing 'setHue'"
parent.setHue(this, percent) if (verifyPercent(percent)) {
sendEvent(name: "hue", value: percent, displayed: false) parent.setHue(this, percent)
sendEvent(name: "hue", value: percent, displayed: false)
}
} }
void setColor(value) { void setColor(value) {
log.debug "setColor: ${value}, $this" log.debug "setColor: ${value}, $this"
parent.setColor(this, value) def events = []
if (value.hue) { sendEvent(name: "hue", value: value.hue, displayed: false)} def validValues = [:]
if (value.saturation) { sendEvent(name: "saturation", value: value.saturation, displayed: false)}
if (value.hex) { sendEvent(name: "color", value: value.hex)} if (verifyPercent(value.hue)) {
if (value.level) { sendEvent(name: "level", value: value.level, descriptionText: "Level has changed to ${value.level}%")} events << createEvent(name: "hue", value: value.hue, displayed: false)
sendEvent(name: "switch", value: "on") validValues.hue = value.hue
}
if (verifyPercent(value.saturation)) {
events << createEvent(name: "saturation", value: value.saturation, displayed: false)
validValues.saturation = value.saturation
}
if (value.hex != null) {
if (value.hex ==~ /^\#([A-Fa-f0-9]){6}$/) {
events << createEvent(name: "color", value: value.hex)
validValues.hex = value.hex
} else {
log.warn "$value.hex is not a valid color"
}
}
if (verifyPercent(value.level)) {
events << createEvent(name: "level", value: value.level, descriptionText: "Level has changed to ${value.level}%")
validValues.level = value.level
}
if (value.switch == "off" || (value.level != null && value.level <= 0)) {
events << createEvent(name: "switch", value: "off")
validValues.switch = "off"
} else {
events << createEvent(name: "switch", value: "on")
validValues.switch = "on"
}
if (!events.isEmpty()) {
parent.setColor(this, validValues)
}
events.each {
sendEvent(it)
}
} }
void reset() { void reset() {
log.debug "Executing 'reset'" log.debug "Executing 'reset'"
def value = [level:100, saturation:56, hue:23] def value = [level:100, saturation:56, hue:23]
setAdjustedColor(value) setAdjustedColor(value)
parent.poll() parent.poll()
} }
void setAdjustedColor(value) { void setAdjustedColor(value) {
if (value) { if (value) {
log.trace "setAdjustedColor: ${value}" log.trace "setAdjustedColor: ${value}"
def adjusted = value + [:] def adjusted = value + [:]
adjusted.hue = adjustOutgoingHue(value.hue) adjusted.hue = adjustOutgoingHue(value.hue)
// Needed because color picker always sends 100 // Needed because color picker always sends 100
adjusted.level = null adjusted.level = null
setColor(adjusted) setColor(adjusted)
} else {
log.warn "Invalid color input"
} }
} }
void setColorTemperature(value) { void setColorTemperature(value) {
if (value) { if (value) {
log.trace "setColorTemperature: ${value}k" log.trace "setColorTemperature: ${value}k"
parent.setColorTemperature(this, value) parent.setColorTemperature(this, value)
sendEvent(name: "colorTemperature", value: value) sendEvent(name: "colorTemperature", value: value)
} sendEvent(name: "switch", value: "on")
} else {
log.warn "Invalid color temperature"
}
} }
void refresh() { void refresh() {
log.debug "Executing 'refresh'" log.debug "Executing 'refresh'"
parent.manualRefresh() parent.manualRefresh()
} }
def adjustOutgoingHue(percent) { def adjustOutgoingHue(percent) {
@@ -177,3 +219,14 @@ def adjustOutgoingHue(percent) {
log.info "percent: $percent, adjusted: $adjusted" log.info "percent: $percent, adjusted: $adjusted"
adjusted adjusted
} }
def verifyPercent(percent) {
if (percent == null)
return false
else if (percent >= 0 && percent <= 100) {
return true
} else {
log.warn "$percent is not 0-100"
return false
}
}

View File

@@ -79,8 +79,12 @@ void off() {
void setLevel(percent) { void setLevel(percent) {
log.debug "Executing 'setLevel'" log.debug "Executing 'setLevel'"
parent.setLevel(this, percent) if (percent != null && percent >= 0 && percent <= 100) {
sendEvent(name: "level", value: percent) parent.setLevel(this, percent)
sendEvent(name: "level", value: percent)
} else {
log.warn "$percent is not 0-100"
}
} }
void refresh() { void refresh() {

View File

@@ -203,7 +203,7 @@ private List parseContactMessage(String description) {
parts.each { part -> parts.each { part ->
part = part.trim() part = part.trim()
if (part.startsWith('contactState:')) { if (part.startsWith('contactState:')) {
results << getContactResult(part, description) results.addAll(getContactResult(part, description))
} }
else if (part.startsWith('accelerationState:')) { else if (part.startsWith('accelerationState:')) {
results << getAccelerationResult(part, description) results << getAccelerationResult(part, description)
@@ -316,7 +316,7 @@ private List getContactResult(part, description) {
results results
} }
private getAccelerationResult(part, description) { private Map getAccelerationResult(part, description) {
def name = "acceleration" def name = "acceleration"
def value = part.endsWith("1") ? "active" : "inactive" def value = part.endsWith("1") ? "active" : "inactive"
def linkText = getLinkText(device) def linkText = getLinkText(device)
@@ -335,7 +335,7 @@ private getAccelerationResult(part, description) {
] ]
} }
private getTempResult(part, description) { private Map getTempResult(part, description) {
def name = "temperature" def name = "temperature"
def temperatureScale = getTemperatureScale() def temperatureScale = getTemperatureScale()
def value = zigbee.parseSmartThingsTemperatureValue(part, "temp: ", temperatureScale) def value = zigbee.parseSmartThingsTemperatureValue(part, "temp: ", temperatureScale)
@@ -360,7 +360,7 @@ private getTempResult(part, description) {
] ]
} }
private getXyzResult(results, description) { private Map getXyzResult(results, description) {
def name = "threeAxis" def name = "threeAxis"
def value = "${results.x},${results.y},${results.z}" def value = "${results.x},${results.y},${results.z}"
def linkText = getLinkText(device) def linkText = getLinkText(device)
@@ -379,7 +379,7 @@ private getXyzResult(results, description) {
] ]
} }
private getBatteryResult(part, description) { private Map getBatteryResult(part, description) {
def batteryDivisor = description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"} ? description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"}.split(":")[1].trim() : null def batteryDivisor = description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"} ? description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"}.split(":")[1].trim() : null
def name = "battery" def name = "battery"
def value = zigbee.parseSmartThingsBatteryValue(part, batteryDivisor) def value = zigbee.parseSmartThingsBatteryValue(part, batteryDivisor)
@@ -400,7 +400,7 @@ private getBatteryResult(part, description) {
] ]
} }
private getRssiResult(part, description, lastHop=false) { private Map getRssiResult(part, description, lastHop=false) {
def name = lastHop ? "lastHopRssi" : "rssi" def name = lastHop ? "lastHopRssi" : "rssi"
def valueString = part.split(":")[1].trim() def valueString = part.split(":")[1].trim()
def value = (Integer.parseInt(valueString) - 128).toString() def value = (Integer.parseInt(valueString) - 128).toString()
@@ -431,7 +431,7 @@ private getRssiResult(part, description, lastHop=false) {
* Note: To make the signal strength indicator more accurate, we could combine * Note: To make the signal strength indicator more accurate, we could combine
* LQI with RSSI. * LQI with RSSI.
*/ */
private getLqiResult(part, description, lastHop=false) { private Map getLqiResult(part, description, lastHop=false) {
def name = lastHop ? "lastHopLqi" : "lqi" def name = lastHop ? "lastHopLqi" : "lqi"
def valueString = part.split(":")[1].trim() def valueString = part.split(":")[1].trim()
def percentageOf = 255 def percentageOf = 255

View File

@@ -56,21 +56,17 @@ metadata {
def parse(String description) { def parse(String description) {
log.debug "description is $description" log.debug "description is $description"
def resultMap = zigbee.getKnownDescription(description) def event = zigbee.getEvent(description)
if (resultMap) { if (event) {
log.info resultMap log.info event
if (resultMap.type == "update") { if (event.name == "power") {
log.info "$device updates: ${resultMap.value}"
}
else if (resultMap.type == "power") {
def powerValue
if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power
powerValue = (resultMap.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration event.value = (event.value as Integer) / 10 //TODO: The divisor value needs to be set as part of configuration
sendEvent(name: "power", value: powerValue) sendEvent(event)
} }
} }
else { else {
sendEvent(name: resultMap.type, value: resultMap.value) sendEvent(event)
} }
} }
else { else {

View File

@@ -51,15 +51,9 @@ metadata {
def parse(String description) { def parse(String description) {
log.debug "description is $description" log.debug "description is $description"
def resultMap = zigbee.getKnownDescription(description) def event = zigbee.getEvent(description)
if (resultMap) { if (event) {
log.info resultMap sendEvent(event)
if (resultMap.type == "update") {
log.info "$device updates: ${resultMap.value}"
}
else {
sendEvent(name: resultMap.type, value: resultMap.value)
}
} }
else { else {
log.warn "DID NOT PARSE MESSAGE for description : $description" log.warn "DID NOT PARSE MESSAGE for description : $description"

View File

@@ -83,32 +83,19 @@ def uninstalled() {
} }
def configure() { def configure() {
/*
def cmds = def cmds =
zigbee.configSetup("${CLUSTER_DOORLOCK}", "${DOORLOCK_ATTR_LOCKSTATE}", zigbee.configureReporting(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE,
"${TYPE_ENUM8}", 0, 3600, "{01}") + TYPE_ENUM8, 0, 3600, null) +
zigbee.configSetup("${CLUSTER_POWER}", "${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING}", zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING,
"${TYPE_U8}", 600, 21600, "{01}") TYPE_U8, 600, 21600, 0x01)
*/
def zigbeeId = device.zigbeeId
def cmds =
[
"zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 1 ${CLUSTER_DOORLOCK} {$zigbeeId} {}", "delay 200",
"zcl global send-me-a-report ${CLUSTER_DOORLOCK} ${DOORLOCK_ATTR_LOCKSTATE} ${TYPE_ENUM8} 0 3600 {01}", "delay 200",
"send 0x${device.deviceNetworkId} 1 0x${device.endpointId}", "delay 200",
"zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 1 ${CLUSTER_POWER} {$zigbeeId} {}", "delay 200",
"zcl global send-me-a-report ${CLUSTER_POWER} ${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING} ${TYPE_U8} 600 21600 {01}", "delay 200",
"send 0x${device.deviceNetworkId} 1 0x${device.endpointId}", "delay 200",
]
log.info "configure() --- cmds: $cmds" log.info "configure() --- cmds: $cmds"
return cmds + refresh() // send refresh cmds as part of config return cmds + refresh() // send refresh cmds as part of config
} }
def refresh() { def refresh() {
def cmds = def cmds =
zigbee.refreshData("${CLUSTER_DOORLOCK}", "${DOORLOCK_ATTR_LOCKSTATE}") + zigbee.readAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE) +
zigbee.refreshData("${CLUSTER_POWER}", "${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING}") zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING)
log.info "refresh() --- cmds: $cmds" log.info "refresh() --- cmds: $cmds"
return cmds return cmds
} }
@@ -121,34 +108,27 @@ def parse(String description) {
map = parseReportAttributeMessage(description) map = parseReportAttributeMessage(description)
} }
log.debug "parse() --- Parse returned $map"
def result = map ? createEvent(map) : null def result = map ? createEvent(map) : null
log.debug "parse() --- returned: $result"
return result return result
} }
// Lock capability commands // Lock capability commands
def lock() { def lock() {
//def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_LOCK_DOOR}", "{}") def cmds = zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_LOCK_DOOR)
//log.info "lock() -- cmds: $cmds" log.info "lock() -- cmds: $cmds"
//return cmds return cmds
"st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_LOCK_DOOR} {}"
} }
def unlock() { def unlock() {
//def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_UNLOCK_DOOR}", "{}") def cmds = zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_UNLOCK_DOOR)
//log.info "unlock() -- cmds: $cmds" log.info "unlock() -- cmds: $cmds"
//return cmds return cmds
"st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_UNLOCK_DOOR} {}"
} }
// Private methods // Private methods
private Map parseReportAttributeMessage(String description) { private Map parseReportAttributeMessage(String description) {
log.trace "parseReportAttributeMessage() --- description: $description"
Map descMap = zigbee.parseDescriptionAsMap(description) Map descMap = zigbee.parseDescriptionAsMap(description)
log.debug "parseReportAttributeMessage() --- descMap: $descMap"
Map resultMap = [:] Map resultMap = [:]
if (descMap.clusterInt == CLUSTER_POWER && descMap.attrInt == POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) { if (descMap.clusterInt == CLUSTER_POWER && descMap.attrInt == POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) {
resultMap.name = "battery" resultMap.name = "battery"
@@ -156,18 +136,24 @@ private Map parseReportAttributeMessage(String description) {
if (device.getDataValue("manufacturer") == "Yale") { //Handling issue with Yale locks incorrect battery reporting if (device.getDataValue("manufacturer") == "Yale") { //Handling issue with Yale locks incorrect battery reporting
resultMap.value = Integer.parseInt(descMap.value, 16) resultMap.value = Integer.parseInt(descMap.value, 16)
} }
log.info "parseReportAttributeMessage() --- battery: ${resultMap.value}"
} }
else if (descMap.clusterInt == CLUSTER_DOORLOCK && descMap.attrInt == DOORLOCK_ATTR_LOCKSTATE) { else if (descMap.clusterInt == CLUSTER_DOORLOCK && descMap.attrInt == DOORLOCK_ATTR_LOCKSTATE) {
def value = Integer.parseInt(descMap.value, 16) def value = Integer.parseInt(descMap.value, 16)
def linkText = getLinkText(device)
resultMap.name = "lock" resultMap.name = "lock"
resultMap.putAll([0:["value":"unknown", if (value == 0) {
"descriptionText":"Not fully locked"], resultMap.value = "unknown"
1:["value":"locked"], resultMap.descriptionText = "${linkText} is not fully locked"
2:["value":"unlocked"]].get(value, } else if (value == 1) {
["value":"unknown", resultMap.value = "locked"
"descriptionText":"Unknown lock state"])) resultMap.descriptionText = "${linkText} is locked"
log.info "parseReportAttributeMessage() --- lock: ${resultMap.value}" } else if (value == 2) {
resultMap.value = "unlocked"
resultMap.descriptionText = "${linkText} is unlocked"
} else {
resultMap.value = "unknown"
resultMap.descriptionText = "${linkText} is in unknown lock state"
}
} }
else { else {
log.debug "parseReportAttributeMessage() --- ignoring attribute" log.debug "parseReportAttributeMessage() --- ignoring attribute"

View File

@@ -51,22 +51,15 @@ metadata {
// Parse incoming device messages to generate events // Parse incoming device messages to generate events
def parse(String description) { def parse(String description) {
log.debug "description is $description" log.debug "description is $description"
def event = zigbee.getEvent(description)
def resultMap = zigbee.getKnownDescription(description) if (event) {
if (resultMap) { if (event.name == "power") {
log.info resultMap
if (resultMap.type == "update") {
log.info "$device updates: ${resultMap.value}"
}
else if (resultMap.type == "power") {
def powerValue def powerValue
if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power powerValue = (event.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration
powerValue = (resultMap.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration sendEvent(name: "power", value: powerValue)
sendEvent(name: "power", value: powerValue)
}
} }
else { else {
sendEvent(name: resultMap.type, value: resultMap.value) sendEvent(event)
} }
} }
else { else {

View File

@@ -53,16 +53,9 @@ metadata {
// Parse incoming device messages to generate events // Parse incoming device messages to generate events
def parse(String description) { def parse(String description) {
log.debug "description is $description" log.debug "description is $description"
def event = zigbee.getEvent(description)
def resultMap = zigbee.getKnownDescription(description) if (event) {
if (resultMap) { sendEvent(event)
log.info resultMap
if (resultMap.type == "update") {
log.info "$device updates: ${resultMap.value}"
}
else {
sendEvent(name: resultMap.type, value: resultMap.value)
}
} }
else { else {
log.warn "DID NOT PARSE MESSAGE for description : $description" log.warn "DID NOT PARSE MESSAGE for description : $description"

View File

@@ -73,16 +73,9 @@ metadata {
// Parse incoming device messages to generate events // Parse incoming device messages to generate events
def parse(String description) { def parse(String description) {
log.debug "description is $description" log.debug "description is $description"
def event = zigbee.getEvent(description)
def finalResult = zigbee.getKnownDescription(description) if (event) {
if (finalResult) { sendEvent(event)
log.info finalResult
if (finalResult.type == "update") {
log.info "$device updates: ${finalResult.value}"
}
else {
sendEvent(name: finalResult.type, value: finalResult.value)
}
} }
else { else {
log.warn "DID NOT PARSE MESSAGE for description : $description" log.warn "DID NOT PARSE MESSAGE for description : $description"

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
} }
} }

View File

@@ -161,7 +161,7 @@ private sendDeveloperReq() {
headers: [ headers: [
HOST: host HOST: host
], ],
body: [devicetype: "$token-0", username: "$token-0"]], "${selectedHue}")) body: [devicetype: "$token-0"]], "${selectedHue}"))
} }
private discoverHueBulbs() { private discoverHueBulbs() {
@@ -657,39 +657,53 @@ def setColorTemperature(childDevice, huesettings) {
} }
def setColor(childDevice, huesettings) { def setColor(childDevice, huesettings) {
log.debug "Executing 'setColor($huesettings)'" log.debug "Executing 'setColor($huesettings)'"
def value = [:]
def hue = null def hue = null
def sat = null def sat = null
def xy = null def xy = null
if (huesettings.hex) {
xy = getHextoXY(huesettings.hex) if (huesettings.hex != null) {
} else if (huesettings.hue && huesettings.saturation) { value.xy = getHextoXY(huesettings.hex)
hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535) } else {
sat = Math.min(Math.round(huesettings.saturation * 255 / 100), 255) if (huesettings.hue != null)
value.hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535)
if (huesettings.saturation != null)
value.sat = Math.min(Math.round(huesettings.saturation * 255 / 100), 255)
} }
def alert = huesettings.alert ? huesettings.alert : "none"
def transition = huesettings.transition ? huesettings.transition : 4
def value = [xy: xy, sat: sat, hue: hue, alert: alert, transitiontime: transition, on: true] // Default behavior is to turn light on
value.on = true
if (huesettings.level != null) { if (huesettings.level != null) {
if (huesettings.level == 1) value.bri = 1 else value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255) if (huesettings.level <= 0)
value.on = value.bri > 0 value.on = false
} else if (huesettings.level == 1)
value.bri = 1
else
value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255)
}
value.alert = huesettings.alert ? huesettings.alert : "none"
value.transition = huesettings.transition ? huesettings.transition : 4
log.debug "sending command $value" // Make sure to turn off light if requested
put("lights/${getId(childDevice)}/state", value) if (huesettings.switch == "off")
value.on = false
log.debug "sending command $value"
put("lights/${getId(childDevice)}/state", value)
return "Color set to $value"
} }
def nextLevel(childDevice) { def nextLevel(childDevice) {
def level = device.latestValue("level") as Integer ?: 0 def level = device.latestValue("level") as Integer ?: 0
if (level < 100) { if (level < 100) {
level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer
} } else {
else { level = 25
level = 25 }
} setLevel(childDevice,level)
setLevel(childDevice,level)
} }
private getId(childDevice) { private getId(childDevice) {
@@ -788,16 +802,14 @@ private getHextoXY(String colorStr) {
// Make green more vivid // Make green more vivid
if (normalizedToOne[1] > 0.04045) { if (normalizedToOne[1] > 0.04045) {
green = (float) Math.pow((normalizedToOne[1] + 0.055) green = (float) Math.pow((normalizedToOne[1] + 0.055) / (1.0 + 0.055), 2.4);
/ (1.0 + 0.055), 2.4);
} else { } else {
green = (float) (normalizedToOne[1] / 12.92); green = (float) (normalizedToOne[1] / 12.92);
} }
// Make blue more vivid // Make blue more vivid
if (normalizedToOne[2] > 0.04045) { if (normalizedToOne[2] > 0.04045) {
blue = (float) Math.pow((normalizedToOne[2] + 0.055) blue = (float) Math.pow((normalizedToOne[2] + 0.055) / (1.0 + 0.055), 2.4);
/ (1.0 + 0.055), 2.4);
} else { } else {
blue = (float) (normalizedToOne[2] / 12.92); blue = (float) (normalizedToOne[2] / 12.92);
} }
@@ -806,8 +818,8 @@ private getHextoXY(String colorStr) {
float Y = (float) (red * 0.234327 + green * 0.743075 + blue * 0.022598); float Y = (float) (red * 0.234327 + green * 0.743075 + blue * 0.022598);
float Z = (float) (red * 0.0000000 + green * 0.053077 + blue * 1.035763); float Z = (float) (red * 0.0000000 + green * 0.053077 + blue * 1.035763);
float x = X / (X + Y + Z); float x = (X != 0 ? X / (X + Y + Z) : 0);
float y = Y / (X + Y + Z); float y = (Y != 0 ? Y / (X + Y + Z) : 0);
double[] xy = new double[2]; double[] xy = new double[2];
xy[0] = x; xy[0] = x;