Compare commits

...

1 Commits

Author SHA1 Message Date
Baogong Jiang
30c9b4828e MSA-2824: to control tplink plug 2018-06-01 16:13:19 -07:00
3 changed files with 1159 additions and 0 deletions

View File

@@ -0,0 +1,499 @@
/*
TP-Link Plug and Switch Device Handler, 2018, Version 2
Copyright 2018 Dave Gutheinz
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.
Discalimer: This Service Manager and the associated Device
Handlers are in no way sanctioned or supported by TP-Link.
All development is based upon open-source data on the
TP-Link devices; primarily various users on GitHub.com.
===== History =============================================
2018-01-31 Update to Version 2
a. Common file content for all bulb implementations,
using separate files by model only.
b. User file-internal selection of Energy Monitor
function enabling.
2018-02-17 Updated Energy Monitor Functions
a. Allowed for full month collection in previous month
b. Cleaned-up algorithm to use Groovy date.
2018-02-19 Completed Energy Monitor tuning
a. Fixed March 1, 2 issue where data would not be
captured
b. Update remaining code.
2018-04-22 Update setCurrentDate to eliminate error for some users.
// ===== Hub or Cloud Installation =========================*/
def installType = "Cloud"
//def installType = "Hub"
// ===========================================================
metadata {
definition (name: "(${installType}) TP-Link EnergyMonitor Plug",
namespace: "davegut",
author: "Dave Gutheinz",
deviceType: "EnergyMonitor Plug",
energyMonitor: "EnergyMonitor",
installType: "${installType}") {
capability "Switch"
capability "refresh"
capability "polling"
capability "Sensor"
capability "Actuator"
capability "Power Meter"
command "getPower"
capability "Energy Meter"
command "getEnergyStats"
attribute "monthTotalE", "string"
attribute "monthAvgE", "string"
attribute "weekTotalE", "string"
attribute "weekAvgE", "string"
}
tiles(scale: 2) {
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#00a0dc",
nextState:"waiting"
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff",
nextState:"waiting"
attributeState "waiting", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#15EE10",
nextState:"waiting"
attributeState "commsError", label:'Comms Error', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#e86d13",
nextState:"waiting"
}
tileAttribute ("deviceError", key: "SECONDARY_CONTROL") {
attributeState "deviceError", label: '${currentValue}'
}
}
standardTile("refresh", "capability.refresh", width: 2, height: 1, decoration: "flat") {
state "default", label:"Refresh", action:"refresh.refresh"
}
valueTile("currentPower", "device.power", decoration: "flat", height: 1, width: 2) {
state "power", label: 'Current Power \n\r ${currentValue} W'
}
valueTile("energyToday", "device.energy", decoration: "flat", height: 1, width: 2) {
state "energy", label: 'Usage Today\n\r${currentValue} WattHr'
}
valueTile("monthTotal", "device.monthTotalE", decoration: "flat", height: 1, width: 2) {
state "monthTotalE", label: '30 Day Total\n\r ${currentValue} KWH'
}
valueTile("monthAverage", "device.monthAvgE", decoration: "flat", height: 1, width: 2) {
state "monthAvgE", label: '30 Day Avg\n\r ${currentValue} KWH'
}
valueTile("weekTotal", "device.weekTotalE", decoration: "flat", height: 1, width: 2) {
state "weekTotalE", label: '7 Day Total\n\r ${currentValue} KWH'
}
valueTile("weekAverage", "device.weekAvgE", decoration: "flat", height: 1, width: 2) {
state "weekAvgE", label: '7 Day Avg\n\r ${currentValue} KWH'
}
valueTile("4x1Blank", "default", decoration: "flat", height: 1, width: 4) {
state "default", label: ''
}
main("switch")
details("switch", "refresh" ,"4x1Blank",
"currentPower", "weekTotal", "monthTotal",
"energyToday", "weekAverage", "monthAverage")
}
def rates = [:]
rates << ["5" : "Refresh every 5 minutes"]
rates << ["10" : "Refresh every 10 minutes"]
rates << ["15" : "Refresh every 15 minutes"]
rates << ["30" : "Refresh every 30 minutes"]
preferences {
if (installType == "Hub") {
input("deviceIP", "text", title: "Device IP", required: true, displayDuringSetup: true)
input("gatewayIP", "text", title: "Gateway IP", required: true, displayDuringSetup: true)
}
input name: "refreshRate", type: "enum", title: "Refresh Rate", options: rates, description: "Select Refresh Rate", required: false
}
}
// ===== Update when installed or setting changed =====
def installed() {
update()
}
def updated() {
runIn(2, update)
}
def update() {
state.deviceType = metadata.definition.deviceType
state.installType = metadata.definition.installType
state.emon = metadata.definition.energyMonitor
state.emeterText = "emeter"
state.getTimeText = "time"
unschedule()
switch(refreshRate) {
case "5":
runEvery5Minutes(refresh)
log.info "Refresh Scheduled for every 5 minutes"
break
case "10":
runEvery10Minutes(refresh)
log.info "Refresh Scheduled for every 10 minutes"
break
case "15":
runEvery15Minutes(refresh)
log.info "Refresh Scheduled for every 15 minutes"
break
default:
runEvery30Minutes(refresh)
log.info "Refresh Scheduled for every 30 minutes"
}
schedule("0 05 0 * * ?", setCurrentDate)
schedule("0 10 0 * * ?", getEnergyStats)
setCurrentDate()
runIn(2, refresh)
runIn(7, getEnergyStats)
}
void uninstalled() {
if (state.installType == "Cloud") {
def alias = device.label
log.debug "Removing device ${alias} with DNI = ${device.deviceNetworkId}"
parent.removeChildDevice(alias, device.deviceNetworkId)
}
}
// ===== Basic Plug Control/Status =====
def on() {
sendCmdtoServer('{"system":{"set_relay_state":{"state": 1}}}', "deviceCommand", "commandResponse")
runIn(2, refresh)
}
def off() {
sendCmdtoServer('{"system":{"set_relay_state":{"state": 0}}}', "deviceCommand", "commandResponse")
runIn(2, refresh)
}
def poll() {
sendCmdtoServer('{"system":{"get_sysinfo":{}}}', "deviceCommand", "commandResponse")
}
def refresh(){
sendCmdtoServer('{"system":{"get_sysinfo":{}}}', "deviceCommand", "commandResponse")
runIn(2, getPower)
}
def commandResponse(cmdResponse){
if (cmdResponse.system.set_relay_state == null) {
def status = cmdResponse.system.get_sysinfo.relay_state
if (status == 1) {
status = "on"
} else {
status = "off"
}
log.info "${device.name} ${device.label}: Power: ${status}"
sendEvent(name: "switch", value: status)
} else {
return
}
}
// ===== Get Current Energy Data =====
def getPower(){
sendCmdtoServer("""{"${state.emeterText}":{"get_realtime":{}}}""", "deviceCommand", "energyMeterResponse")
runIn(5, getConsumption)
}
def energyMeterResponse(cmdResponse) {
def realtime = cmdResponse["emeter"]["get_realtime"]
if (realtime.power == null) {
state.powerScale = "power_mw"
state.energyScale = "energy_wh"
} else {
state.powerScale = "power"
state.energyScale = "energy"
}
def powerConsumption = realtime."${state.powerScale}"
if (state.powerScale == "power_mw") {
powerConsumption = Math.round(powerConsumption/10) / 100
} else {
powerConsumption = Math.round(100*powerConsumption) / 100
}
sendEvent(name: "power", value: powerConsumption)
log.info "$device.name $device.label: Updated CurrentPower to $powerConsumption"
}
// ===== Get Today's Consumption =====
def getConsumption(){
sendCmdtoServer("""{"${state.emeterText}":{"get_daystat":{"month": ${state.monthToday}, "year": ${state.yearToday}}}}""", "emeterCmd", "useTodayResponse")
}
def useTodayResponse(cmdResponse) {
def wattHrToday
def wattHrData
def dayList = cmdResponse["emeter"]["get_daystat"].day_list
for (int i = 0; i < dayList.size(); i++) {
wattHrData = dayList[i]
if(wattHrData.day == state.dayToday) {
wattHrToday = wattHrData."${state.energyScale}"
}
}
if (state.powerScale == "power") {
wattHrToday = Math.round(1000*wattHrToday)
}
sendEvent(name: "energy", value: wattHrToday)
log.info "$device.name $device.label: Updated Usage Today to ${wattHrToday}"
}
// ===== Get Weekly and Monthly Stats =====
def getEnergyStats() {
state.monTotEnergy = 0
state.monTotDays = 0
state.wkTotEnergy = 0
state.wkTotDays = 0
sendCmdtoServer("""{"${state.emeterText}":{"get_daystat":{"month": ${state.monthToday}, "year": ${state.yearToday}}}}""", "emeterCmd", "engrStatsResponse")
runIn(4, getPrevMonth)
}
def getPrevMonth() {
def prevMonth = state.monthStart
if (state.monthToday == state.monthStart) {
// If all of the data is in this month, do not request previous month.
// This will occur when the current month is 31 days.
return
} else if (state.monthToday - 2 == state.monthStart) {
// If the start month is 2 less than current, we must handle
// the data to get a third month - January.
state.handleFeb = "yes"
prevMonth = prevMonth + 1
runIn(4, getJan)
}
// sendCmdtoServer("""{"${state.emeterText}":{"get_daystat":{"month": ${prevMonth}, "year": ${state.yearStart}}}}""", "emeterCmd", "engrStatsResponse")
// ===== SIMULATOR COMMANDS ================================================
sendCmdtoServer("""{"${state.emeterText}":{"get_daystat":{"month": ${prevMonth}, "year": ${state.yearStart}}}}""", "emeterCmd", "UseJanWatts")
// ===== SIMULATOR COMMANDS ================================================
}
def getJan() {
// Gets January data on March 1 and 2. Only access if current month = 3
// and start month = 1
sendCmdtoServer("""{"${state.emeterText}":{"get_daystat":{"month": ${state.monthStart}, "year": ${state.yearStart}}}}""", "emeterCmd", "engrStatsResponse")
}
def engrStatsResponse(cmdResponse) {
/*
This method parses up to two energy status messages from the device,
adding the energy for the previous 30 days and week, ignoring the
current day. It then calculates the 30 and 7 day average formatted
in kiloWattHours to two decimal places.
*/
def dayList = cmdResponse[state.emeterText]["get_daystat"].day_list
if (!dayList[0]) {
log.info "$device.name $device.label: Month has no energy data."
return
}
def monTotEnergy = state.monTotEnergy
def wkTotEnergy = state.wkTotEnergy
def monTotDays = state.monTotDays
def wkTotDays = state.wkTotDays
def startDay = state.dayStart
def dataMonth = dayList[0].month
if (dataMonth == state.monthToday) {
for (int i = 0; i < dayList.size(); i++) {
def energyData = dayList[i]
monTotEnergy += energyData."${state.energyScale}"
monTotDays += 1
if (state.dayToday < 8 || energyData.day >= state.weekStart) {
wkTotEnergy += energyData."${state.energyScale}"
wkTotDays += 1
}
if(energyData.day == state.dayToday) {
monTotEnergy -= energyData."${state.energyScale}"
wkTotEnergy -= energyData."${state.energyScale}"
monTotDays -= 1
wkTotDays -= 1
}
}
} else if (state.handleFeb == "yes" && dataMonth == 2) {
startDay = 1
for (int i = 0; i < dayList.size(); i++) {
def energyData = dayList[i]
if (energyData.day >= startDay) {
monTotEnergy += energyData."${state.energyScale}"
monTotDays += 1
}
if (energyData.day >= state.weekStart && state.dayToday < 8) {
wkTotEnergy += energyData."${state.energyScale}"
wkTotDays += 1
}
}
} else if (state.handleFeb == "yes" && dataMonth == 1) {
for (int i = 0; i < dayList.size(); i++) {
def energyData = dayList[i]
if (energyData.day >= startDay) {
monTotEnergy += energyData."${state.energyScale}"
monTotDays += 1
}
state.handleFeb = ""
}
} else {
for (int i = 0; i < dayList.size(); i++) {
def energyData = dayList[i]
if (energyData.day >= startDay) {
monTotEnergy += energyData."${state.energyScale}"
monTotDays += 1
}
if (energyData.day >= state.weekStart && state.dayToday < 8) {
wkTotEnergy += energyData."${state.energyScale}"
wkTotDays += 1
}
}
}
state.monTotDays = monTotDays
state.monTotEnergy = monTotEnergy
state.wkTotEnergy = wkTotEnergy
state.wkTotDays = wkTotDays
log.info "$device.name $device.label: Update 7 and 30 day energy consumption statistics"
if (monTotDays == 0) {
// Aviod divide by zero on 1st of month
monTotDays = 1
wkTotDays = 1
}
def monAvgEnergy =monTotEnergy/monTotDays
def wkAvgEnergy = wkTotEnergy/wkTotDays
if (state.powerScale == "power_mw") {
monAvgEnergy = Math.round(monAvgEnergy/10)/100
wkAvgEnergy = Math.round(wkAvgEnergy/10)/100
monTotEnergy = Math.round(monTotEnergy/10)/100
wkTotEnergy = Math.round(wkTotEnergy/10)/100
} else {
monAvgEnergy = Math.round(100*monAvgEnergy)/100
wkAvgEnergy = Math.round(100*wkAvgEnergy)/100
monTotEnergy = Math.round(100*monTotEnergy)/100
wkTotEnergy = Math.round(100*wkTotEnergy)/100
}
sendEvent(name: "monthTotalE", value: monTotEnergy)
sendEvent(name: "monthAvgE", value: monAvgEnergy)
sendEvent(name: "weekTotalE", value: wkTotEnergy)
sendEvent(name: "weekAvgE", value: wkAvgEnergy)
}
// ===== Obtain Week and Month Data =====
def setCurrentDate() {
sendCmdtoServer('{"time":{"get_time":null}}', "deviceCommand", "currentDateResponse")
}
def currentDateResponse(cmdResponse) {
def currDate = cmdResponse["time"]["get_time"]
state.dayToday = currDate.mday.toInteger()
state.monthToday = currDate.month.toInteger()
state.yearToday = currDate.year.toInteger()
def dateToday = Date.parse("yyyy-MM-dd", "${currDate.year}-${currDate.month}-${currDate.mday}")
def monStartDate = dateToday - 30
def wkStartDate = dateToday - 7
state.dayStart = monStartDate[Calendar.DAY_OF_MONTH].toInteger()
state.monthStart = monStartDate[Calendar.MONTH].toInteger() + 1
state.yearStart = monStartDate[Calendar.YEAR].toInteger()
state.weekStart = wkStartDate[Calendar.DAY_OF_MONTH].toInteger()
}
// ----- SEND COMMAND TO CLOUD VIA SM -----
private sendCmdtoServer(command, hubCommand, action) {
if (state.installType == "Hub") {
sendCmdtoHub(command, hubCommand, action)
} else {
sendCmdtoCloud(command, hubCommand, action)
}
}
private sendCmdtoCloud(command, hubCommand, action){
def appServerUrl = getDataValue("appServerUrl")
def deviceId = getDataValue("deviceId")
def cmdResponse = parent.sendDeviceCmd(appServerUrl, deviceId, command)
String cmdResp = cmdResponse.toString()
if (cmdResp.substring(0,5) == "ERROR"){
def errMsg = cmdResp.substring(7,cmdResp.length())
log.error "${device.name} ${device.label}: ${errMsg}"
sendEvent(name: "switch", value: "commsError", descriptionText: errMsg)
sendEvent(name: "deviceError", value: errMsg)
action = ""
} else {
sendEvent(name: "deviceError", value: "OK")
}
actionDirector(action, cmdResponse)
}
private sendCmdtoHub(command, hubCommand, action){
def headers = [:]
headers.put("HOST", "$gatewayIP:8082") // Same as on Hub.
headers.put("tplink-iot-ip", deviceIP)
headers.put("tplink-command", command)
headers.put("action", action)
headers.put("command", hubCommand)
sendHubCommand(new physicalgraph.device.HubAction([
headers: headers],
device.deviceNetworkId,
[callback: hubResponseParse]
))
}
def hubResponseParse(response) {
def action = response.headers["action"]
def cmdResponse = parseJson(response.headers["cmd-response"])
if (cmdResponse == "TcpTimeout") {
log.error "$device.name $device.label: Communications Error"
sendEvent(name: "switch", value: "offline", descriptionText: "ERROR at hubResponseParse TCP Timeout")
sendEvent(name: "deviceError", value: "TCP Timeout in Hub")
} else {
actionDirector(action, cmdResponse)
sendEvent(name: "deviceError", value: "OK")
}
}
def actionDirector(action, cmdResponse) {
switch(action) {
case "commandResponse":
commandResponse(cmdResponse)
break
case "energyMeterResponse":
energyMeterResponse(cmdResponse)
break
case "useTodayResponse":
useTodayResponse(cmdResponse)
break
case "currentDateResponse":
currentDateResponse(cmdResponse)
break
case "engrStatsResponse":
engrStatsResponse(cmdResponse)
break
default:
log.debug "at default"
}
}
// ----- CHILD / PARENT INTERCHANGE TASKS -----
def syncAppServerUrl(newAppServerUrl) {
updateDataValue("appServerUrl", newAppServerUrl)
log.info "Updated appServerUrl for ${device.name} ${device.label}"
}

View File

@@ -0,0 +1,261 @@
/*
TP-Link Plug and Switch Device Handler, 2018, Version 2
Copyright 2018 Dave Gutheinz
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.
Discalimer: This Service Manager and the associated Device
Handlers are in no way sanctioned or supported by TP-Link.
All development is based upon open-source data on the
TP-Link devices; primarily various users on GitHub.com.
===== History =============================================
2018-01-31 Update to Version 2
a. Common file content for all bulb implementations,
using separate files by model only.
b. User file-internal selection of Energy Monitor
function enabling.
===== Plug/Switch Type. DO NOT EDIT ====================*/
def deviceType = "Plug-Switch" // Plug/Switch
// def deviceType = "Dimming Switch" // HS220 Only
// ===== Hub or Cloud Installation =========================*/
def installType = "Cloud"
//def installType = "Hub"
// ===========================================================
metadata {
definition (name: "(${installType}) TP-Link ${deviceType}",
namespace: "davegut",
author: "Dave Gutheinz",
deviceType: "${deviceType}",
energyMonitor: "Standard",
installType: "${installType}") {
capability "Switch"
capability "refresh"
capability "polling"
capability "Sensor"
capability "Actuator"
if (deviceType == "Dimming Switch") {
capability "Switch Level"
}
}
tiles(scale: 2) {
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#00a0dc",
nextState:"waiting"
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff",
nextState:"waiting"
attributeState "waiting", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#15EE10",
nextState:"waiting"
attributeState "commsError", label:'Comms Error', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#e86d13",
nextState:"waiting"
}
if (deviceType == "Dimming Switch") {
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
attributeState "level", label: "Brightness: ${currentValue}", action:"switch level.setLevel", range: "(1..100)"
}
}
tileAttribute ("deviceError", key: "SECONDARY_CONTROL") {
attributeState "deviceError", label: '${currentValue}'
}
}
standardTile("refresh", "capability.refresh", width: 2, height: 1, decoration: "flat") {
state "default", label:"Refresh", action:"refresh.refresh"
}
main("switch")
details("switch", "refresh")
}
def rates = [:]
rates << ["1" : "Refresh every minutes (Not Recommended)"]
rates << ["5" : "Refresh every 5 minutes"]
rates << ["10" : "Refresh every 10 minutes"]
rates << ["15" : "Refresh every 15 minutes"]
rates << ["30" : "Refresh every 30 minutes (Recommended)"]
preferences {
if (installType == "Hub") {
input("deviceIP", "text", title: "Device IP", required: true, displayDuringSetup: true)
input("gatewayIP", "text", title: "Gateway IP", required: true, displayDuringSetup: true)
}
input name: "refreshRate", type: "enum", title: "Refresh Rate", options: rates, description: "Select Refresh Rate", required: false
}
}
// ===== Update when installed or setting changed =====
def installed() {
update()
}
def updated() {
runIn(2, update)
}
def update() {
state.deviceType = metadata.definition.deviceType
state.installType = metadata.definition.installType
unschedule()
switch(refreshRate) {
case "1":
runEvery1Minute(refresh)
log.info "Refresh Scheduled for every minute"
break
case "5":
runEvery5Minutes(refresh)
log.info "Refresh Scheduled for every 5 minutes"
break
case "10":
runEvery10Minutes(refresh)
log.info "Refresh Scheduled for every 10 minutes"
break
case "15":
runEvery15Minutes(refresh)
log.info "Refresh Scheduled for every 15 minutes"
break
default:
runEvery30Minutes(refresh)
log.info "Refresh Scheduled for every 30 minutes"
}
runIn(5, refresh)
}
void uninstalled() {
if (state.installType == "Cloud") {
def alias = device.label
log.debug "Removing device ${alias} with DNI = ${device.deviceNetworkId}"
parent.removeChildDevice(alias, device.deviceNetworkId)
}
}
// ===== Basic Plug Control/Status =====
def on() {
sendCmdtoServer('{"system":{"set_relay_state":{"state": 1}}}', "deviceCommand", "commandResponse")
}
def off() {
sendCmdtoServer('{"system":{"set_relay_state":{"state": 0}}}', "deviceCommand", "commandResponse")
}
def setLevel(percentage) {
percentage = percentage as int
if (percentage == 0) {
percentage = 1
}
sendCmdtoServer("""{"smartlife.iot.dimmer":{"set_brightness":{"brightness":${percentage}}}}""", "deviceCommand", "commandResponse")
}
def poll() {
sendCmdtoServer('{"system":{"get_sysinfo":{}}}', "deviceCommand", "refreshResponse")
}
def refresh(){
sendCmdtoServer('{"system":{"get_sysinfo":{}}}', "deviceCommand", "refreshResponse")
}
def commandResponse(cmdResponse) {
refresh()
}
def refreshResponse(cmdResponse){
def onOff = cmdResponse.system.get_sysinfo.relay_state
if (onOff == 1) {
onOff = "on"
} else {
onOff = "off"
}
sendEvent(name: "switch", value: onOff)
def level = "0"
if (state.deviceType == "Dimming Switch") {
level = cmdResponse.system.get_sysinfo.brightness
sendEvent(name: "level", value: level)
}
log.info "${device.name} ${device.label}: Power: ${onOff} / Dimmer Level: ${level}%"
}
// ----- SEND COMMAND TO CLOUD VIA SM -----
private sendCmdtoServer(command, hubCommand, action) {
if (state.installType == "Hub") {
sendCmdtoHub(command, hubCommand, action)
} else {
sendCmdtoCloud(command, hubCommand, action)
}
}
private sendCmdtoCloud(command, hubCommand, action){
def appServerUrl = getDataValue("appServerUrl")
def deviceId = getDataValue("deviceId")
def cmdResponse = parent.sendDeviceCmd(appServerUrl, deviceId, command)
String cmdResp = cmdResponse.toString()
if (cmdResp.substring(0,5) == "ERROR"){
def errMsg = cmdResp.substring(7,cmdResp.length())
log.error "${device.name} ${device.label}: ${errMsg}"
sendEvent(name: "switch", value: "commsError", descriptionText: errMsg)
sendEvent(name: "deviceError", value: errMsg)
action = ""
} else {
sendEvent(name: "deviceError", value: "OK")
}
actionDirector(action, cmdResponse)
}
private sendCmdtoHub(command, hubCommand, action){
def headers = [:]
headers.put("HOST", "$gatewayIP:8082") // Same as on Hub.
headers.put("tplink-iot-ip", deviceIP)
headers.put("tplink-command", command)
headers.put("action", action)
headers.put("command", hubCommand)
sendHubCommand(new physicalgraph.device.HubAction([
headers: headers],
device.deviceNetworkId,
[callback: hubResponseParse]
))
}
def hubResponseParse(response) {
def action = response.headers["action"]
def cmdResponse = parseJson(response.headers["cmd-response"])
if (cmdResponse == "TcpTimeout") {
log.error "$device.name $device.label: Communications Error"
sendEvent(name: "switch", value: "offline", descriptionText: "ERROR at hubResponseParse TCP Timeout")
sendEvent(name: "deviceError", value: "TCP Timeout in Hub")
} else {
actionDirector(action, cmdResponse)
sendEvent(name: "deviceError", value: "OK")
}
}
def actionDirector(action, cmdResponse) {
switch(action) {
case "commandResponse":
commandResponse(cmdResponse)
break
case "refreshResponse":
refreshResponse(cmdResponse)
break
default:
log.debug "at default"
}
}
// ----- CHILD / PARENT INTERCHANGE TASKS -----
def syncAppServerUrl(newAppServerUrl) {
updateDataValue("appServerUrl", newAppServerUrl)
log.info "Updated appServerUrl for ${device.name} ${device.label}"
}

View File

@@ -0,0 +1,399 @@
/*
TP-Link Connect Service Manager, 2018 Version 2
Copyright 2018 Dave Gutheinz
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.
##### Discalimer: This Service Manager and the associated Device
Handlers are in no way sanctioned or supported by TP-Link. All
development is based upon open-source data on the TP-Link devices;
primarily various users on GitHub.com.
##### Notes #####
1. This Service Manager is designed to install and manage TP-Link
bulbs, plugs, and switches using their respective device handlers.
2. Please direct comments to the SmartThings community thread
'Cloud TP-Link Device SmartThings Integration'.
##### History #####
2018-01-31 Updated for new release of Device Handlers
*/
definition(
name: "TP-Link Cloud Connect",
namespace: "davegut",
author: "Dave Gutheinz",
description: "A Service Manager for the TP-Link devices connecting through the TP-Link Cloud",
category: "SmartThings Labs",
iconUrl: "http://ecx.images-amazon.com/images/I/51S8gO0bvZL._SL210_QL95_.png",
iconX2Url: "http://ecx.images-amazon.com/images/I/51S8gO0bvZL._SL210_QL95_.png",
iconX3Url: "http://ecx.images-amazon.com/images/I/51S8gO0bvZL._SL210_QL95_.png",
singleInstance: true
)
preferences {
page(name: "cloudLogin", title: "TP-Link Cloud Login", nextPage:"", content:"cloudLogin", uninstall: true)
page(name: "selectDevices", title: "Select TP-Link Devices", nextPage:"", content:"selectDevices", uninstall: true, install: true)
}
def setInitialStates() {
if (!state.TpLinkToken) {state.TpLinkToken = null}
if (!state.devices) {state.devices = [:]}
if (!state.currentError) {state.currentError = null}
if (!state.errorCount) {state.errorCount = 0}
}
// ----- LOGIN PAGE -----
def cloudLogin() {
setInitialStates()
def cloudLoginText = "If possible, open the IDE and select Live Logging. THEN, " +
"enter your Username and Password for TP-Link (same as Kasa app) and the "+
"action you want to complete. Your current token:\n\r\n\r${state.TpLinkToken}" +
"\n\r\n\rAvailable actions:\n\r" +
" Initial Install: Obtains token and adds devices.\n\r" +
" Add Devices: Only add devices.\n\r" +
" Update Token: Updates the token.\n\r"
def errorMsg = ""
if (state.currentError != null){
errorMsg = "Error communicating with cloud:\n\r\n\r${state.currentError}" +
"\n\r\n\rPlease resolve the error and try again.\n\r\n\r"
}
return dynamicPage(
name: "cloudLogin",
title: "TP-Link Device Service Manager",
nextPage: "selectDevices",
uninstall: true) {
section(errorMsg)
section(cloudLoginText) {
input(
"userName", "string",
title:"Your TP-Link Email Address",
required:true,
displayDuringSetup: true
)
input(
"userPassword", "password",
title:"TP-Link account password",
required: true,
displayDuringSetup: true
)
input(
"updateToken", "enum",
title: "What do you want to do?",
required: true,
multiple: false,
options: ["Initial Install", "Add Devices", "Update Token"]
)
}
}
}
// ----- SELECT DEVICES PAGE -----
def selectDevices() {
if (updateToken != "Add Devices") {
getToken()
}
if (state.currentError != null || updateToken == "Update Token") {
return cloudLogin()
}
getDevices()
def devices = state.devices
if (state.currentError != null) {
return cloudLogin()
}
def errorMsg = ""
if (devices == [:]) {
errorMsg = "There were no devices from TP-Link. This usually means "+
"that all devices are in 'Local Control Only'. Correct then " +
"rerun.\n\r\n\r"
}
def newDevices = [:]
devices.each {
def isChild = getChildDevice(it.value.deviceMac)
if (!isChild) {
newDevices["${it.value.deviceMac}"] = "${it.value.alias} model ${it.value.deviceModel}"
}
}
if (newDevices == [:]) {
errorMsg = "No new devices to add. Are you sure they are in Remote " +
"Control Mode?\n\r\n\r"
}
settings.selectedDevices = null
def TPLinkDevicesMsg = "TP-Link Token is ${state.TpLinkToken}\n\r" +
"Devices that have not been previously installed and are not in 'Local " +
"WiFi control only' will appear below. TAP below to see the list of " +
"TP-Link devices available select the ones you want to connect to " +
"SmartThings.\n\r\n\rPress DONE when you have selected the devices you " +
"wish to add, thenpress DONE again to install the devices. Press < " +
"to return to the previous page."
return dynamicPage(
name: "selectDevices",
title: "Select Your TP-Link Devices",
install: true,
uninstall: true) {
section(errorMsg)
section(TPLinkDevicesMsg) {
input "selectedDevices", "enum",
required:false,
multiple:true,
title: "Select Devices (${newDevices.size() ?: 0} found)",
options: newDevices
}
}
}
def getDevices() {
def currentDevices = getDeviceData()
state.devices = [:]
def devices = state.devices
currentDevices.each {
def device = [:]
device["deviceMac"] = it.deviceMac
device["alias"] = it.alias
device["deviceModel"] = it.deviceModel
device["deviceId"] = it.deviceId
device["appServerUrl"] = it.appServerUrl
devices << ["${it.deviceMac}": device]
def isChild = getChildDevice(it.deviceMac)
if (isChild) {
isChild.syncAppServerUrl(it.appServerUrl)
}
log.info "Device ${it.alias} added to devices array"
}
}
def addDevices() {
def tpLinkModel = [:]
// Plug-Switch Devices (no energy monitor capability)
tpLinkModel << ["HS100" : "(Cloud) TP-Link Plug-Switch"] // HS100
tpLinkModel << ["HS105" : "(Cloud) TP-Link Plug-Switch"] // HS105
tpLinkModel << ["HS200" : "(Cloud) TP-Link Plug-Switch"] // HS200
tpLinkModel << ["HS210" : "(Cloud) TP-Link Plug-Switch"] // HS210
tpLinkModel << ["KP100" : "(Cloud) TP-Link Plug-Switch"] // KP100
// Dimming Plug Devices
tpLinkModel << ["HS220" : "(Cloud) TP-Link Dimming Switch"] // HS220
// Energy Monitor Plugs
tpLinkModel << ["HS110" : "(Cloud) TP-Link EnergyMonitor Plug"] // HS110
tpLinkModel << ["HS115" : "(Cloud) TP-Link EnergyMonitor Plug"] // HS110
// Soft White Bulbs
tpLinkModel << ["KB100" : "(Cloud) TP-Link SoftWhite Bulb"] // KB100
tpLinkModel << ["LB100" : "(Cloud) TP-Link SoftWhite Bulb"] // LB100
tpLinkModel << ["LB110" : "(Cloud) TP-Link SoftWhite Bulb"] // LB110
tpLinkModel << ["LB200" : "(Cloud) TP-Link SoftWhite Bulb"] // LB200
// Tunable White Bulbs
tpLinkModel << ["LB120" : "(Cloud) TP-Link TunableWhite Bulb"] // LB120
// Color Bulbs
tpLinkModel << ["KB130" : "(Cloud) TP-Link Color Bulb"] // KB130
tpLinkModel << ["LB130" : "(Cloud) TP-Link Color Bulb"] // LB130
tpLinkModel << ["LB230" : "(Cloud) TP-Link Color Bulb"] // LB230
def hub = location.hubs[0]
def hubId = hub.id
selectedDevices.each { dni ->
def isChild = getChildDevice(dni)
if (!isChild) {
def device = state.devices.find { it.value.deviceMac == dni }
def deviceModel = device.value.deviceModel.substring(0,5)
addChildDevice(
"davegut",
tpLinkModel["${deviceModel}"],
device.value.deviceMac,
hubId, [
"label": device.value.alias,
"name": device.value.deviceModel,
"data": [
"deviceId" : device.value.deviceId,
"appServerUrl": device.value.appServerUrl,
]
]
)
log.info "Installed TP-Link $deviceModel with alias ${device.value.alias}"
}
}
}
// ----- GET A NEW TOKEN FROM CLOUD -----
def getToken() {
def hub = location.hubs[0]
def cmdBody = [
method: "login",
params: [
appType: "Kasa_Android",
cloudUserName: "${userName}",
cloudPassword: "${userPassword}",
terminalUUID: "${hub.id}"
]
]
def getTokenParams = [
uri: "https://wap.tplinkcloud.com",
requestContentType: 'application/json',
contentType: 'application/json',
headers: ['Accept':'application/json; version=1, */*; q=0.01'],
body : new groovy.json.JsonBuilder(cmdBody).toString()
]
httpPostJson(getTokenParams) {resp ->
if (resp.status == 200 && resp.data.error_code == 0) {
state.TpLinkToken = resp.data.result.token
log.info "TpLinkToken updated to ${state.TpLinkToken}"
sendEvent(name: "TokenUpdate", value: "tokenUpdate Successful.")
if (state.currentError != null) {
state.currentError = null
}
} else if (resp.status != 200) {
state.currentError = resp.statusLine
sendEvent(name: "currentError", value: resp.data)
log.error "Error in getToken: ${state.currentError}"
sendEvent(name: "TokenUpdate", value: state.currentError)
} else if (resp.data.error_code != 0) {
state.currentError = resp.data
sendEvent(name: "currentError", value: resp.data)
log.error "Error in getToken: ${state.currentError}"
sendEvent(name: "TokenUpdate", value: state.currentError)
}
}
}
// ----- GET DEVICE DATA FROM THE CLOUD -----
def getDeviceData() {
def currentDevices = ""
def cmdBody = [method: "getDeviceList"]
def getDevicesParams = [
uri: "https://wap.tplinkcloud.com?token=${state.TpLinkToken}",
requestContentType: 'application/json',
contentType: 'application/json',
headers: ['Accept':'application/json; version=1, */*; q=0.01'],
body : new groovy.json.JsonBuilder(cmdBody).toString()
]
httpPostJson(getDevicesParams) {resp ->
if (resp.status == 200 && resp.data.error_code == 0) {
currentDevices = resp.data.result.deviceList
if (state.currentError != null) {
state.currentError = null
}
return currentDevices
} else if (resp.status != 200) {
state.currentError = resp.statusLine
sendEvent(name: "currentError", value: resp.data)
log.error "Error in getDeviceData: ${state.currentError}"
} else if (resp.data.error_code != 0) {
state.currentError = resp.data
sendEvent(name: "currentError", value: resp.data)
log.error "Error in getDeviceData: ${state.currentError}"
}
}
}
// ----- SEND DEVICE COMMAND TO CLOUD FOR DH -----
def sendDeviceCmd(appServerUrl, deviceId, command) {
def cmdResponse = ""
def cmdBody = [
method: "passthrough",
params: [
deviceId: deviceId,
requestData: "${command}"
]
]
def sendCmdParams = [
uri: "${appServerUrl}/?token=${state.TpLinkToken}",
requestContentType: 'application/json',
contentType: 'application/json',
headers: ['Accept':'application/json; version=1, */*; q=0.01'],
body : new groovy.json.JsonBuilder(cmdBody).toString()
]
httpPostJson(sendCmdParams) {resp ->
if (resp.status == 200 && resp.data.error_code == 0) {
def jsonSlurper = new groovy.json.JsonSlurper()
cmdResponse = jsonSlurper.parseText(resp.data.result.responseData)
if (state.errorCount != 0) {
state.errorCount = 0
}
if (state.currentError != null) {
state.currentError = null
sendEvent(name: "currentError", value: null)
log.debug "state.errorCount = ${state.errorCount} // state.currentError = ${state.currentError}"
}
// log.debug "state.errorCount = ${state.errorCount} // state.currentError = ${state.currentError}"
} else if (resp.status != 200) {
state.currentError = resp.statusLine
cmdResponse = "ERROR: ${resp.statusLine}"
sendEvent(name: "currentError", value: resp.data)
log.error "Error in sendDeviceCmd: ${state.currentError}"
} else if (resp.data.error_code != 0) {
state.currentError = resp.data
cmdResponse = "ERROR: ${resp.data.msg}"
sendEvent(name: "currentError", value: resp.data)
log.error "Error in sendDeviceCmd: ${state.currentError}"
}
}
return cmdResponse
}
// ----- INSTALL, UPDATE, INITIALIZE -----
def installed() {
initialize()
}
def updated() {
unsubscribe()
initialize()
}
def initialize() {
unsubscribe()
unschedule()
runEvery5Minutes(checkError)
schedule("0 30 2 ? * WED", getToken)
if (selectedDevices) {
addDevices()
}
}
// ----- PERIODIC CLOUD MX TASKS -----
def checkError() {
if (state.currentError == null || state.currentError == "none") {
log.info "TP-Link Connect did not have any set errors."
return
}
def errMsg = state.currentError.msg
log.info "Attempting to solve error: ${errMsg}"
state.errorCount = state.errorCount +1
if (errMsg == "Token expired" && state.errorCount < 6) {
sendEvent (name: "ErrHandling", value: "Handle comms error attempt ${state.errorCount}")
getDevices()
if (state.currentError == null) {
log.info "getDevices successful. apiServerUrl updated and token is good."
return
}
log.error "${errMsg} error while attempting getDevices. Will attempt getToken"
getToken()
if (state.currentError == null) {
log.info "getToken successful. Token has been updated."
getDevices()
return
}
} else {
log.error "checkError: No auto-correctable errors or exceeded Token request count."
}
log.error "checkError residual: ${state.currentError}"
}
// ----- CHILD CALLED TASKS -----
def removeChildDevice(alias, deviceNetworkId) {
try {
deleteChildDevice(it.deviceNetworkId)
sendEvent(name: "DeviceDelete", value: "${alias} deleted")
} catch (Exception e) {
sendEvent(name: "DeviceDelete", value: "Failed to delete ${alias}")
}
}