Compare commits

..

1 Commits

Author SHA1 Message Date
Cety
f8ac40d532 MSA-996: Philio Smart Energy Plug in Switch 2016-03-24 06:42:50 -05:00
3 changed files with 289 additions and 40 deletions

View File

@@ -0,0 +1,198 @@
/**
* Copyright 2015 Astralink
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
*/
metadata {
definition (name: "Philio Smart Energy Plug in Switch", namespace: "astralink", author: "AstraLink"){
capability "Relay Switch"
capability "Power Meter"
capability "Energy Meter"
capability "Actuator"
capability "Switch"
capability "Configuration"
capability "Polling"
capability "Refresh"
capability "Sensor"
attribute "ManufacturerCode", "string"
attribute "ProductCode", "string"
attribute "ProduceTypeCode", "string"
attribute "WirelessConfig", "string"
attribute "WakeUp", "string"
fingerprint deviceId: "0x1001", inClusters: "0x5E, 0x86, 0x72, 0x98, 0x5A, 0x85, 0x59, 0x73, 0x25, 0x20, 0x27, 0x32, 0x70, 0x71, 0x75, 0x7A"
}
// simulator metadata
simulator {
status "on": "command: 2003, payload: FF"
status "off": "command: 2003, payload: 00"
// reply messages
reply "2001FF,delay 100,2502": "command: 2503, payload: FF"
reply "200100,delay 100,2502": "command: 2503, payload: 00"
}
// 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"
state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff"
}
valueTile("power", "device.power", decoration: "flat") {
state "default", label:'${currentValue} W'
}
valueTile("energy", "device.energy", decoration: "flat") {
state "default", label:'${currentValue} kWh'
}
standardTile("configure", "device.power", inactiveLabel: false, decoration: "flat") {
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
}
standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") {
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
}
main "switch"
details(["switch","power","energy","refresh","configure"])
}
}
def installed() {
zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
}
def parse(String description) {
def result = null
def cmd = zwave.parse(description, [0x20: 1, 0x70: 1])
log.debug cmd
if (cmd) {
result = createEvent(zwaveEvent(cmd))
}
if (result?.name == 'hail' && hubFirmwareLessThan("000.011.00602")) {
result = [result, response(zwave.basicV1.basicGet())]
log.debug "Was hailed: requesting state update"
} else {
log.debug "Parse returned ${result?.descriptionText}"
}
return result
}
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
[name: "switch", value: cmd.value ? "on" : "off", type: "physical"]
}
def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) {
[name: "switch", value: cmd.value ? "on" : "off", type: "digital"]
}
def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) {
log.debug "MeterReport ${cmd}"
if (cmd.scale == 0) {
[name: "energy", value: cmd.scaledMeterValue, unit: "kWh"]
} else if (cmd.scale == 1) {
[name: "energy", value: cmd.scaledMeterValue, unit: "kVAh"]
}
else {
[name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W"]
}
}
def configure() {
delayBetween([
zwave.manufacturerSpecificV2.manufacturerSpecificGet().format(),
zwave.associationV2.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]).format(),
zwave.configurationV1.configurationSet(parameterNumber: 1, size: 2, scaledConfigurationValue: 12).format(), // Watt Meter report 5s per unit
zwave.configurationV1.configurationSet(parameterNumber: 2, size: 2, scaledConfigurationValue: 2).format() // KWH report 10mins per unit
])
}
def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) {
def value = "when off"
if (cmd.configurationValue[0] == 1) {value = "when on"}
if (cmd.configurationValue[0] == 2) {value = "never"}
[name: "indicatorStatus", value: value, display: false]
}
def zwaveEvent(physicalgraph.zwave.commands.hailv1.Hail cmd) {
[name: "hail", value: "hail", descriptionText: "Switch button was pressed", displayed: false]
}
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
def result = []
def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
def ManufacturerCode = String.format("%04X", cmd.manufacturerId)
def ProductCode = String.format("%04X", cmd.productId)
def ProduceTypeCode = String.format("%04X", cmd.productTypeId)
def WirelessConfig = "ZWP"
sendEvent(name: "ManufacturerCode", value: ManufacturerCode)
sendEvent(name: "ProductCode", value: ProductCode)
sendEvent(name: "ProduceTypeCode", value: ProduceTypeCode)
sendEvent(name: "WirelessConfig", value: WirelessConfig)
result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false)
return result
}
def zwaveEvent(physicalgraph.zwave.Command cmd) {
return createEvent(descriptionText: "$device.displayName: $cmd", displayed: false)
}
def on() {
delayBetween([
zwave.basicV1.basicSet(value: 0xFF).format(),
zwave.switchBinaryV1.switchBinaryGet().format()
])
}
def off() {
delayBetween([
zwave.basicV1.basicSet(value: 0x00).format(),
zwave.switchBinaryV1.switchBinaryGet().format()
])
}
def poll() {
zwave.switchBinaryV1.switchBinaryGet().format()
}
def refresh() {
delayBetween([
zwave.switchBinaryV1.switchBinaryGet().format(),
zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
])
}
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x85: 2, 0x70: 1])
log.debug "encapsulated: $encapsulatedCommand"
if (encapsulatedCommand) {
state.sec = 1
zwaveEvent(encapsulatedCommand)
}
}
private secure(physicalgraph.zwave.Command cmd) {
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
}
private secureSequence(commands, delay=200) {
delayBetween(commands.collect{ secure(it) }, delay)
}

View File

@@ -87,10 +87,13 @@ 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()
} }
@@ -103,24 +106,72 @@ def uninstalled() {
} }
def initialize() { def initialize() {
unsubscribe() atomicState.attached_sensors = [:]
if (plantlinksensors){ if (plantlinksensors){
subscribe(plantlinksensors, "moisture_status", moistureHandler)
subscribe(plantlinksensors, "battery_status", batteryHandler)
plantlinksensors.each{ sensor_device -> plantlinksensors.each{ sensor_device ->
subscribe(sensor_device, "moisture_status", moistureHandler) sensor_device.setStatusIcon("Waiting on First Measurement")
subscribe(sensor_device, "battery_status", batteryHandler)
sensor_device.setInstallSmartApp("connectedToSmartApp") sensor_device.setInstallSmartApp("connectedToSmartApp")
} }
} }
} }
def updatePlantNameIfNeeded(plant, expected_plant_name){ 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 = [ 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 "renaming plant ${plant.key} - ${expected_plant_name}" log.debug "updating plant for - ${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
@@ -134,39 +185,32 @@ def updatePlantNameIfNeeded(plant, expected_plant_name){
} }
def moistureHandler(event){ def moistureHandler(event){
log.debug "moistureHandler - ${event.value}" def expected_plant_name = "SmartThings - ${event.displayName}"
def expected_plant_name = "${event.displayName} (ST)"
def device_serial = getDeviceSerialFromEvent(event) def device_serial = getDeviceSerialFromEvent(event)
def device_battery = atomicState["battery${device_serial}"]
if ( device_battery == null){ if (!atomicState.attached_sensors.containsKey(device_serial)){
log.error "Missing Battery Voltage - next cycle should have it" dock_sensor(device_serial, expected_plant_name)
}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: appendedEventWithBatteryInfo body: event.value
] ]
httpPost(measurement_post_params) { measurement_post_response -> httpPost(measurement_post_params) { measurement_post_response ->
if (parse_api_response(measurement_post_response, 'creating moisture measurement') && measurement_post_response.data.size() >0){ if (parse_api_response(measurement_post_response, 'creating moisture measurement') &&
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
updatePlantNameIfNeeded(plant, expected_plant_name) checkAndUpdatePlantIfNeeded(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)
} }
@@ -180,7 +224,21 @@ 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){
@@ -189,12 +247,6 @@ 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 = [
@@ -211,13 +263,12 @@ 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: appSettings.https_plantLinkServer, uri: "https://oso-tech.appspot.com",
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()],
@@ -270,11 +321,10 @@ 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: appSettings.https_plantLinkServer, uri: "https://hardware-dot-oso-tech.appspot.com",
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],
@@ -283,6 +333,7 @@ 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
@@ -295,12 +346,12 @@ private refreshAuthToken() {
} }
data.action = "" data.action = ""
}else{ }else{
log.debug "PlantLink Server - ${resp.status} : ${resp.status.code}" log.debug "refresh failed ${resp.status} : ${resp.status.code}"
} }
} }
} }
catch(Exception e){ catch(Exception e){
log.debug "PlantLink Connector - OAuth Refresh Failed: " + e log.debug "caught exception refreshing auth token: " + e
} }
} }
@@ -313,7 +364,7 @@ def parse_api_response(resp, message) {
refreshAuthToken() refreshAuthToken()
return false return false
} else { } else {
debugEvent("Plantlink Error: ${resp.status} - ${resp.status.code}", true) debugEvent("Authentication error, invalid authentication method, lack of credentials, etc.", true)
return false return false
} }
} }

View File

@@ -161,7 +161,7 @@ private sendDeveloperReq() {
headers: [ headers: [
HOST: host HOST: host
], ],
body: [devicetype: "$token-0"]], "${selectedHue}")) body: [devicetype: "$token-0", username: "$token-0"]], "${selectedHue}"))
} }
private discoverHueBulbs() { private discoverHueBulbs() {