/** * TCP Bulbs (Connect) * * Copyright 2014 Todd Wackford * * 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. * */ import java.security.MessageDigest; private apiUrl() { "https://tcp.greenwavereality.com/gwr/gop.php?" } definition( name: "Tcp Bulbs (Connect)", namespace: "wackford", author: "SmartThings", description: "Connect your TCP bulbs to SmartThings using Cloud to Cloud integration. You must create a remote login acct on TCP Mobile App.", category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/tcp.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tcp@2x.png", singleInstance: true ) preferences { def msg = """Tap 'Next' after you have entered in your TCP Mobile remote credentials. Once your credentials are accepted, SmartThings will scan your TCP installation for Bulbs.""" page(name: "selectDevices", title: "Connect Your TCP Lights to SmartThings", install: false, uninstall: true, nextPage: "chooseBulbs") { section("TCP Connected Remote Credentials") { input "username", "text", title: "Enter TCP Remote Email/UserName", required: true input "password", "password", title: "Enter TCP Remote Password", required: true paragraph msg } } page(name: "chooseBulbs", title: "Choose Bulbs to Control With SmartThings", content: "initialize") } def installed() { debugOut "Installed with settings: ${settings}" unschedule() unsubscribe() setupBulbs() def cron = "0 11 23 * * ?" log.debug "schedule('$cron', syncronizeDevices)" schedule(cron, syncronizeDevices) } def updated() { debugOut "Updated with settings: ${settings}" unschedule() setupBulbs() def cron = "0 11 23 * * ?" log.debug "schedule('$cron', syncronizeDevices)" schedule(cron, syncronizeDevices) } def uninstalled() { unschedule() //in case we have hanging runIn()'s } private removeChildDevices(delete) { debugOut "deleting ${delete.size()} bulbs" debugOut "deleting ${delete}" delete.each { deleteChildDevice(it.device.deviceNetworkId) } } def uninstallFromChildDevice(childDevice) { def errorMsg = "uninstallFromChildDevice was called and " if (!settings.selectedBulbs) { debugOut errorMsg += "had empty list passed in" return } def dni = childDevice.device.deviceNetworkId if ( !dni ) { debugOut errorMsg += "could not find dni of device" return } def newDeviceList = settings.selectedBulbs - dni app.updateSetting("selectedBulbs", newDeviceList) debugOut errorMsg += "completed succesfully" } def setupBulbs() { debugOut "In setupBulbs" def bulbs = state.devices def deviceFile = "TCP Bulb" selectedBulbs.each { did -> //see if this is a selected bulb and install it if not already def d = getChildDevice(did) if(!d) { def newBulb = bulbs.find { (it.did) == did } d = addChildDevice("wackford", deviceFile, did, null, [name: "${newBulb?.name}", label: "${newBulb?.name}", completedSetup: true]) /*if ( isRoom(did) ) { //change to the multi light group icon for a room device d.setIcon("switch", "on", "st.lights.multi-light-bulb-on") d.setIcon("switch", "off", "st.lights.multi-light-bulb-off") d.save() }*/ } else { debugOut "We already added this device" } } // Delete any that are no longer in settings def delete = getChildDevices().findAll { !selectedBulbs?.contains(it.deviceNetworkId) } removeChildDevices(delete) //we want to ensure syncronization between rooms and bulbs //syncronizeDevices() } def initialize() { atomicState.token = "" getToken() if ( atomicState.token == "error" ) { return dynamicPage(name:"chooseBulbs", title:"TCP Login Failed!\r\nTap 'Done' to try again", nextPage:"", install:false, uninstall: false) { section("") {} } } else { "we're good to go" debugOut "We have Token." } //getGatewayData() //we really don't need anything from the gateway deviceDiscovery() def options = devicesDiscovered() ?: [] def msg = """Tap 'Done' after you have selected the desired devices.""" return dynamicPage(name:"chooseBulbs", title:"TCP and SmartThings Connected!", nextPage:"", install:true, uninstall: true) { section("Tap Below to View Device List") { input "selectedBulbs", "enum", required:false, title:"Select Bulb/Fixture", multiple:true, options:options paragraph msg } } } def deviceDiscovery() { def data = "1${atomicState.token}" def Params = [ cmd: "RoomGetCarousel", data: "${data}", fmt: "json" ] def cmd = toQueryString(Params) def rooms = "" apiPost(cmd) { response -> rooms = response.data.gip.room } debugOut "rooms data = ${rooms}" def devices = [] def bulbIndex = 1 def lastRoomName = null def deviceList = [] if ( rooms[1] == null ) { def roomId = rooms.rid def roomName = rooms.name devices = rooms.device if ( devices[1] != null ) { debugOut "Room Device Data: did:${roomId} roomName:${roomName}" //deviceList += ["name" : "${roomName}", "did" : "${roomId}", "type" : "room"] devices.each({ debugOut "Bulb Device Data: did:${it?.did} room:${roomName} BulbName:${it?.name}" deviceList += ["name" : "${roomName} ${it?.name}", "did" : "${it?.did}", "type" : "bulb"] }) } else { debugOut "Bulb Device Data: did:${it?.did} room:${roomName} BulbName:${it?.name}" deviceList += ["name" : "${roomName} ${it?.name}", "did" : "${it?.did}", "type" : "bulb"] } } else { rooms.each({ devices = it.device def roomName = it.name if ( devices[1] != null ) { def roomId = it?.rid debugOut "Room Device Data: did:${roomId} roomName:${roomName}" //deviceList += ["name" : "${roomName}", "did" : "${roomId}", "type" : "room"] devices.each({ debugOut "Bulb Device Data: did:${it?.did} room:${roomName} BulbName:${it?.name}" deviceList += ["name" : "${roomName} ${it?.name}", "did" : "${it?.did}", "type" : "bulb"] }) } else { debugOut "Bulb Device Data: did:${devices?.did} room:${roomName} BulbName:${devices?.name}" deviceList += ["name" : "${roomName} ${devices?.name}", "did" : "${devices?.did}", "type" : "bulb"] } }) } devices = ["devices" : deviceList] state.devices = devices.devices } Map devicesDiscovered() { def devices = state.devices def map = [:] if (devices instanceof java.util.Map) { devices.each { def value = "${it?.name}" def key = it?.did map["${key}"] = value } } else { //backwards compatable devices.each { def value = "${it?.name}" def key = it?.did map["${key}"] = value } } map } def getGatewayData() { debugOut "In getGatewayData" def data = "1${atomicState.token}" def qParams = [ cmd: "GatewayGetInfo", data: "${data}", fmt: "json" ] def cmd = toQueryString(qParams) apiPost(cmd) { response -> debugOut "the gateway reponse is ${response.data.gip.gateway}" } } def getToken() { atomicState.token = "" if (password) { def hashedPassword = generateMD5(password) def data = "1${username}${hashedPassword}" def qParams = [ cmd : "GWRLogin", data: "${data}", fmt : "json" ] def cmd = toQueryString(qParams) apiPost(cmd) { response -> def status = response.data.gip.rc //sendNotificationEvent("Get token status ${status}") if (status != "200") {//success code = 200 def errorText = response.data.gip.error debugOut "Error logging into TCP Gateway. Error = ${errorText}" atomicState.token = "error" } else { atomicState.token = response.data.gip.token } } } else { log.warn "Unable to log into TCP Gateway. Error = Password is null" atomicState.token = "error" } } def apiPost(String data, Closure callback) { //debugOut "In apiPost with data: ${data}" def params = [ uri: apiUrl(), body: data ] httpPost(params) { response -> def rc = response.data.gip.rc if ( rc == "200" ) { debugOut ("Return Code = ${rc} = Command Succeeded.") callback.call(response) } else if ( rc == "401" ) { debugOut "Return Code = ${rc} = Error: User not logged in!" //Error code from gateway log.debug "Refreshing Token" getToken() //callback.call(response) //stubbed out so getToken works (we had race issue) } else { log.error "Return Code = ${rc} = Error!" //Error code from gateway sendNotificationEvent("TCP Lighting is having Communication Errors. Error code = ${rc}. Check that TCP Gateway is online") callback.call(response) } } } //this is not working. TCP power reporting is broken. Leave it here for future fix def calculateCurrentPowerUse(deviceCapability, usePercentage) { debugOut "In calculateCurrentPowerUse()" debugOut "deviceCapability: ${deviceCapability}" debugOut "usePercentage: ${usePercentage}" def calcPower = usePercentage * 1000 def reportPower = calcPower.round(1) as String debugOut "report power = ${reportPower}" return reportPower } def generateSha256(String s) { MessageDigest digest = MessageDigest.getInstance("SHA-256") digest.update(s.bytes) new BigInteger(1, digest.digest()).toString(16).padLeft(40, '0') } def generateMD5(String s) { MessageDigest digest = MessageDigest.getInstance("MD5") digest.update(s.bytes); new BigInteger(1, digest.digest()).toString(16).padLeft(32, '0') } String toQueryString(Map m) { return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") } def checkDevicesOnline(bulbs) { debugOut "In checkDevicesOnline()" def onlineBulbs = [] def thisBulb = [] bulbs.each { def dni = it?.did thisBulb = it def data = "1${atomicState.token}${dni}" def qParams = [ cmd: "DeviceGetInfo", data: "${data}", fmt: "json" ] def cmd = toQueryString(qParams) def bulbData = [] apiPost(cmd) { response -> bulbData = response.data.gip } if ( bulbData?.offline == "1" ) { debugOut "${it?.name} is offline with offline value of ${bulbData?.offline}" } else { debugOut "${it?.name} is online with offline value of ${bulbData?.offline}" onlineBulbs += thisBulb } } return onlineBulbs } def syncronizeDevices() { debugOut "In syncronizeDevices" def update = getChildDevices().findAll { selectedBulbs?.contains(it.deviceNetworkId) } update.each { def dni = getChildDevice( it.deviceNetworkId ) debugOut "dni = ${dni}" if (isRoom(dni)) { pollRoom(dni) } else { poll(dni) } } } boolean isRoom(dni) { def device = state.devices.find() {(( it.type == 'room') && (it.did == "${dni}"))} } boolean isBulb(dni) { def device = state.devices.find() {(( it.type == 'bulb') && (it.did == "${dni}"))} } def debugEvent(message, displayEvent) { def results = [ name: "appdebug", descriptionText: message, displayed: displayEvent ] log.debug "Generating AppDebug Event: ${results}" sendEvent (results) } def debugOut(msg) { //log.debug msg //sendNotificationEvent(msg) //Uncomment this for troubleshooting only } /************************************************************************** Child Device Call In Methods **************************************************************************/ def on(childDevice) { debugOut "On request from child device" def dni = childDevice.device.deviceNetworkId def data = "" def cmd = "" if ( isRoom(dni) ) { // this is a room, not a bulb data = "1$atomicState.token${dni}power1" cmd = "RoomSendCommand" } else { data = "1$atomicState.token${dni}power1" cmd = "DeviceSendCommand" } def qParams = [ cmd: cmd, data: "${data}", fmt: "json" ] cmd = toQueryString(qParams) apiPost(cmd) { response -> debugOut "ON result: ${response.data}" } //we want to ensure syncronization between rooms and bulbs //runIn(2, "syncronizeDevices") } def off(childDevice) { debugOut "Off request from child device" def dni = childDevice.device.deviceNetworkId def data = "" def cmd = "" if ( isRoom(dni) ) { // this is a room, not a bulb data = "1$atomicState.token${dni}power0" cmd = "RoomSendCommand" } else { data = "1$atomicState.token${dni}power0" cmd = "DeviceSendCommand" } def qParams = [ cmd: cmd, data: "${data}", fmt: "json" ] cmd = toQueryString(qParams) apiPost(cmd) { response -> debugOut "${response.data}" } //we want to ensure syncronization between rooms and bulbs //runIn(2, "syncronizeDevices") } def setLevel(childDevice, value) { debugOut "setLevel request from child device" def dni = childDevice.device.deviceNetworkId def data = "" def cmd = "" if ( isRoom(dni) ) { // this is a room, not a bulb data = "1${atomicState.token}${dni}level${value}" cmd = "RoomSendCommand" } else { data = "1${atomicState.token}${dni}level${value}" cmd = "DeviceSendCommand" } def qParams = [ cmd: cmd, data: "${data}", fmt: "json" ] cmd = toQueryString(qParams) apiPost(cmd) { response -> debugOut "${response.data}" } //we want to ensure syncronization between rooms and bulbs //runIn(2, "syncronizeDevices") } // Really not called from child, but called from poll() if it is a room def pollRoom(dni) { debugOut "In pollRoom" def data = "" def cmd = "" def roomDeviceData = [] data = "1${atomicState.token}${dni}name,power,control,status,state" cmd = "RoomGetDevices" def qParams = [ cmd: cmd, data: "${data}", fmt: "json" ] cmd = toQueryString(qParams) apiPost(cmd) { response -> roomDeviceData = response.data.gip } debugOut "Room Data: ${roomDeviceData}" def totalPower = 0 def totalLevel = 0 def cnt = 0 def onCnt = 0 //used to tally on/off states roomDeviceData.device.each({ if ( getChildDevice(it.did) ) { totalPower += it.other.bulbpower.toInteger() totalLevel += it.level.toInteger() onCnt += it.state.toInteger() cnt += 1 } }) def avgLevel = totalLevel/cnt def usingPower = totalPower * (avgLevel / 100) as float def room = getChildDevice( dni ) //the device is a room but we use same type file sendEvent( dni, [name: "setBulbPower",value:"${totalPower}"] ) //used in child device calcs //if all devices in room are on, room is on if ( cnt == onCnt ) { // all devices are on sendEvent( dni, [name: "switch",value:"on"] ) sendEvent( dni, [name: "power",value:usingPower.round(1)] ) } else { //if any device in room is off, room is off sendEvent( dni, [name: "switch",value:"off"] ) sendEvent( dni, [name: "power",value:0.0] ) } debugOut "Room Using Power: ${usingPower.round(1)}" } def poll(childDevice) { debugOut "In poll() with ${childDevice}" def dni = childDevice.device.deviceNetworkId def bulbData = [] def data = "" def cmd = "" if ( isRoom(dni) ) { // this is a room, not a bulb pollRoom(dni) return } data = "1${atomicState.token}${dni}" cmd = "DeviceGetInfo" def qParams = [ cmd: cmd, data: "${data}", fmt: "json" ] cmd = toQueryString(qParams) apiPost(cmd) { response -> bulbData = response.data.gip } debugOut "This Bulbs Data Return = ${bulbData}" def bulb = getChildDevice( dni ) //set the devices power max setting to do calcs within the device type if ( bulbData.other.bulbpower ) sendEvent( dni, [name: "setBulbPower",value:"${bulbData.other.bulbpower}"] ) if (( bulbData.state == "1" ) && ( bulb?.currentValue("switch") != "on" )) sendEvent( dni, [name: "switch",value:"on"] ) if (( bulbData.state == "0" ) && ( bulb?.currentValue("switch") != "off" )) sendEvent( dni, [name: "switch",value:"off"] ) //if ( bulbData.level != bulb?.currentValue("level")) { // sendEvent( dni, [name: "level",value: "${bulbData.level}"] ) // sendEvent( dni, [name: "setLevel",value: "${bulbData.level}"] ) //} if (( bulbData.state == "1" ) && ( bulbData.other.bulbpower )) { def levelSetting = bulbData.level as float def bulbPowerMax = bulbData.other.bulbpower as float def calculatedPower = bulbPowerMax * (levelSetting / 100) sendEvent( dni, [name: "power", value: calculatedPower.round(1)] ) } if (( bulbData.state == "0" ) && ( bulbData.other.bulbpower )) sendEvent( dni, [name: "power", value: 0.0] ) }