4 * Copyright 2014 Todd Wackford
6 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 * in compliance with the License. You may obtain a copy of the License at:
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13 * for the specific language governing permissions and limitations under the License.
17 import java.security.MessageDigest;
19 private apiUrl() { "https://tcp.greenwavereality.com/gwr/gop.php?" }
22 name: "Tcp Bulbs (Connect)",
23 namespace: "wackford",
24 author: "SmartThings",
25 description: "Connect your TCP bulbs to SmartThings using Cloud to Cloud integration. You must create a remote login acct on TCP Mobile App.",
26 category: "SmartThings Labs",
27 iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/tcp.png",
28 iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tcp@2x.png",
34 def msg = """Tap 'Next' after you have entered in your TCP Mobile remote credentials.
36 Once your credentials are accepted, SmartThings will scan your TCP installation for Bulbs."""
38 page(name: "selectDevices", title: "Connect Your TCP Lights to SmartThings", install: false, uninstall: true, nextPage: "chooseBulbs") {
39 section("TCP Connected Remote Credentials") {
40 input "username", "text", title: "Enter TCP Remote Email/UserName", required: true
41 input "password", "password", title: "Enter TCP Remote Password", required: true
46 page(name: "chooseBulbs", title: "Choose Bulbs to Control With SmartThings", content: "initialize")
50 debugOut "Installed with settings: ${settings}"
57 def cron = "0 11 23 * * ?"
58 log.debug "schedule('$cron', syncronizeDevices)"
59 schedule(cron, syncronizeDevices)
63 debugOut "Updated with settings: ${settings}"
69 def cron = "0 11 23 * * ?"
70 log.debug "schedule('$cron', syncronizeDevices)"
71 schedule(cron, syncronizeDevices)
76 unschedule() //in case we have hanging runIn()'s
79 private removeChildDevices(delete)
81 debugOut "deleting ${delete.size()} bulbs"
82 debugOut "deleting ${delete}"
84 deleteChildDevice(it.device.deviceNetworkId)
88 def uninstallFromChildDevice(childDevice)
90 def errorMsg = "uninstallFromChildDevice was called and "
91 if (!settings.selectedBulbs) {
92 debugOut errorMsg += "had empty list passed in"
96 def dni = childDevice.device.deviceNetworkId
99 debugOut errorMsg += "could not find dni of device"
103 def newDeviceList = settings.selectedBulbs - dni
104 app.updateSetting("selectedBulbs", newDeviceList)
106 debugOut errorMsg += "completed succesfully"
111 debugOut "In setupBulbs"
113 def bulbs = state.devices
114 def deviceFile = "TCP Bulb"
116 selectedBulbs.each { did ->
117 //see if this is a selected bulb and install it if not already
118 def d = getChildDevice(did)
121 def newBulb = bulbs.find { (it.did) == did }
122 d = addChildDevice("wackford", deviceFile, did, null, [name: "${newBulb?.name}", label: "${newBulb?.name}", completedSetup: true])
124 /*if ( isRoom(did) ) { //change to the multi light group icon for a room device
125 d.setIcon("switch", "on", "st.lights.multi-light-bulb-on")
126 d.setIcon("switch", "off", "st.lights.multi-light-bulb-off")
131 debugOut "We already added this device"
135 // Delete any that are no longer in settings
136 def delete = getChildDevices().findAll { !selectedBulbs?.contains(it.deviceNetworkId) }
137 removeChildDevices(delete)
139 //we want to ensure syncronization between rooms and bulbs
140 //syncronizeDevices()
145 atomicState.token = ""
149 if ( atomicState.token == "error" ) {
150 return dynamicPage(name:"chooseBulbs", title:"TCP Login Failed!\r\nTap 'Done' to try again", nextPage:"", install:false, uninstall: false) {
155 debugOut "We have Token."
158 //getGatewayData() //we really don't need anything from the gateway
162 def options = devicesDiscovered() ?: []
164 def msg = """Tap 'Done' after you have selected the desired devices."""
166 return dynamicPage(name:"chooseBulbs", title:"TCP and SmartThings Connected!", nextPage:"", install:true, uninstall: true) {
167 section("Tap Below to View Device List") {
168 input "selectedBulbs", "enum", required:false, title:"Select Bulb/Fixture", multiple:true, options:options
174 def deviceDiscovery() {
175 def data = "<gip><version>1</version><token>${atomicState.token}</token></gip>"
178 cmd: "RoomGetCarousel",
183 def cmd = toQueryString(Params)
187 apiPost(cmd) { response ->
188 rooms = response.data.gip.room
191 debugOut "rooms data = ${rooms}"
195 def lastRoomName = null
198 if ( rooms[1] == null ) {
199 def roomId = rooms.rid
200 def roomName = rooms.name
201 devices = rooms.device
202 if ( devices[1] != null ) {
203 debugOut "Room Device Data: did:${roomId} roomName:${roomName}"
204 //deviceList += ["name" : "${roomName}", "did" : "${roomId}", "type" : "room"]
206 debugOut "Bulb Device Data: did:${it?.did} room:${roomName} BulbName:${it?.name}"
207 deviceList += ["name" : "${roomName} ${it?.name}", "did" : "${it?.did}", "type" : "bulb"]
210 debugOut "Bulb Device Data: did:${it?.did} room:${roomName} BulbName:${it?.name}"
211 deviceList += ["name" : "${roomName} ${it?.name}", "did" : "${it?.did}", "type" : "bulb"]
216 def roomName = it.name
217 if ( devices[1] != null ) {
219 debugOut "Room Device Data: did:${roomId} roomName:${roomName}"
220 //deviceList += ["name" : "${roomName}", "did" : "${roomId}", "type" : "room"]
222 debugOut "Bulb Device Data: did:${it?.did} room:${roomName} BulbName:${it?.name}"
223 deviceList += ["name" : "${roomName} ${it?.name}", "did" : "${it?.did}", "type" : "bulb"]
226 debugOut "Bulb Device Data: did:${devices?.did} room:${roomName} BulbName:${devices?.name}"
227 deviceList += ["name" : "${roomName} ${devices?.name}", "did" : "${devices?.did}", "type" : "bulb"]
231 devices = ["devices" : deviceList]
232 state.devices = devices.devices
235 Map devicesDiscovered() {
236 def devices = state.devices
238 if (devices instanceof java.util.Map) {
240 def value = "${it?.name}"
242 map["${key}"] = value
244 } else { //backwards compatable
246 def value = "${it?.name}"
248 map["${key}"] = value
254 def getGatewayData() {
255 debugOut "In getGatewayData"
257 def data = "<gip><version>1</version><token>${atomicState.token}</token></gip>"
260 cmd: "GatewayGetInfo",
265 def cmd = toQueryString(qParams)
267 apiPost(cmd) { response ->
268 debugOut "the gateway reponse is ${response.data.gip.gateway}"
275 atomicState.token = ""
278 def hashedPassword = generateMD5(password)
280 def data = "<gip><version>1</version><email>${username}</email><password>${hashedPassword}</password></gip>"
288 def cmd = toQueryString(qParams)
290 apiPost(cmd) { response ->
291 def status = response.data.gip.rc
293 //sendNotificationEvent("Get token status ${status}")
295 if (status != "200") {//success code = 200
296 def errorText = response.data.gip.error
297 debugOut "Error logging into TCP Gateway. Error = ${errorText}"
298 atomicState.token = "error"
300 atomicState.token = response.data.gip.token
304 log.warn "Unable to log into TCP Gateway. Error = Password is null"
305 atomicState.token = "error"
309 def apiPost(String data, Closure callback) {
310 //debugOut "In apiPost with data: ${data}"
318 def rc = response.data.gip.rc
321 debugOut ("Return Code = ${rc} = Command Succeeded.")
322 callback.call(response)
324 } else if ( rc == "401" ) {
325 debugOut "Return Code = ${rc} = Error: User not logged in!" //Error code from gateway
326 log.debug "Refreshing Token"
328 //callback.call(response) //stubbed out so getToken works (we had race issue)
331 log.error "Return Code = ${rc} = Error!" //Error code from gateway
332 sendNotificationEvent("TCP Lighting is having Communication Errors. Error code = ${rc}. Check that TCP Gateway is online")
333 callback.call(response)
339 //this is not working. TCP power reporting is broken. Leave it here for future fix
340 def calculateCurrentPowerUse(deviceCapability, usePercentage) {
341 debugOut "In calculateCurrentPowerUse()"
343 debugOut "deviceCapability: ${deviceCapability}"
344 debugOut "usePercentage: ${usePercentage}"
346 def calcPower = usePercentage * 1000
347 def reportPower = calcPower.round(1) as String
349 debugOut "report power = ${reportPower}"
354 def generateSha256(String s) {
356 MessageDigest digest = MessageDigest.getInstance("SHA-256")
357 digest.update(s.bytes)
358 new BigInteger(1, digest.digest()).toString(16).padLeft(40, '0')
361 def generateMD5(String s) {
362 MessageDigest digest = MessageDigest.getInstance("MD5")
363 digest.update(s.bytes);
364 new BigInteger(1, digest.digest()).toString(16).padLeft(32, '0')
367 String toQueryString(Map m) {
368 return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
371 def checkDevicesOnline(bulbs) {
372 debugOut "In checkDevicesOnline()"
381 def data = "<gip><version>1</version><token>${atomicState.token}</token><did>${dni}</did></gip>"
384 cmd: "DeviceGetInfo",
389 def cmd = toQueryString(qParams)
393 apiPost(cmd) { response ->
394 bulbData = response.data.gip
397 if ( bulbData?.offline == "1" ) {
398 debugOut "${it?.name} is offline with offline value of ${bulbData?.offline}"
401 debugOut "${it?.name} is online with offline value of ${bulbData?.offline}"
402 onlineBulbs += thisBulb
408 def syncronizeDevices() {
409 debugOut "In syncronizeDevices"
411 def update = getChildDevices().findAll { selectedBulbs?.contains(it.deviceNetworkId) }
414 def dni = getChildDevice( it.deviceNetworkId )
415 debugOut "dni = ${dni}"
425 boolean isRoom(dni) {
426 def device = state.devices.find() {(( it.type == 'room') && (it.did == "${dni}"))}
429 boolean isBulb(dni) {
430 def device = state.devices.find() {(( it.type == 'bulb') && (it.did == "${dni}"))}
433 def debugEvent(message, displayEvent) {
437 descriptionText: message,
438 displayed: displayEvent
440 log.debug "Generating AppDebug Event: ${results}"
447 //sendNotificationEvent(msg) //Uncomment this for troubleshooting only
451 /**************************************************************************
452 Child Device Call In Methods
453 **************************************************************************/
454 def on(childDevice) {
455 debugOut "On request from child device"
457 def dni = childDevice.device.deviceNetworkId
461 if ( isRoom(dni) ) { // this is a room, not a bulb
462 data = "<gip><version>1</version><token>$atomicState.token</token><rid>${dni}</rid><type>power</type><value>1</value></gip>"
463 cmd = "RoomSendCommand"
465 data = "<gip><version>1</version><token>$atomicState.token</token><did>${dni}</did><type>power</type><value>1</value></gip>"
466 cmd = "DeviceSendCommand"
475 cmd = toQueryString(qParams)
477 apiPost(cmd) { response ->
478 debugOut "ON result: ${response.data}"
481 //we want to ensure syncronization between rooms and bulbs
482 //runIn(2, "syncronizeDevices")
485 def off(childDevice) {
486 debugOut "Off request from child device"
488 def dni = childDevice.device.deviceNetworkId
492 if ( isRoom(dni) ) { // this is a room, not a bulb
493 data = "<gip><version>1</version><token>$atomicState.token</token><rid>${dni}</rid><type>power</type><value>0</value></gip>"
494 cmd = "RoomSendCommand"
496 data = "<gip><version>1</version><token>$atomicState.token</token><did>${dni}</did><type>power</type><value>0</value></gip>"
497 cmd = "DeviceSendCommand"
506 cmd = toQueryString(qParams)
508 apiPost(cmd) { response ->
509 debugOut "${response.data}"
512 //we want to ensure syncronization between rooms and bulbs
513 //runIn(2, "syncronizeDevices")
516 def setLevel(childDevice, value) {
517 debugOut "setLevel request from child device"
519 def dni = childDevice.device.deviceNetworkId
523 if ( isRoom(dni) ) { // this is a room, not a bulb
524 data = "<gip><version>1</version><token>${atomicState.token}</token><rid>${dni}</rid><type>level</type><value>${value}</value></gip>"
525 cmd = "RoomSendCommand"
527 data = "<gip><version>1</version><token>${atomicState.token}</token><did>${dni}</did><type>level</type><value>${value}</value></gip>"
528 cmd = "DeviceSendCommand"
537 cmd = toQueryString(qParams)
539 apiPost(cmd) { response ->
540 debugOut "${response.data}"
543 //we want to ensure syncronization between rooms and bulbs
544 //runIn(2, "syncronizeDevices")
547 // Really not called from child, but called from poll() if it is a room
549 debugOut "In pollRoom"
552 def roomDeviceData = []
554 data = "<gip><version>1</version><token>${atomicState.token}</token><rid>${dni}</rid><fields>name,power,control,status,state</fields></gip>"
555 cmd = "RoomGetDevices"
563 cmd = toQueryString(qParams)
565 apiPost(cmd) { response ->
566 roomDeviceData = response.data.gip
569 debugOut "Room Data: ${roomDeviceData}"
574 def onCnt = 0 //used to tally on/off states
576 roomDeviceData.device.each({
577 if ( getChildDevice(it.did) ) {
578 totalPower += it.other.bulbpower.toInteger()
579 totalLevel += it.level.toInteger()
580 onCnt += it.state.toInteger()
585 def avgLevel = totalLevel/cnt
586 def usingPower = totalPower * (avgLevel / 100) as float
587 def room = getChildDevice( dni )
589 //the device is a room but we use same type file
590 sendEvent( dni, [name: "setBulbPower",value:"${totalPower}"] ) //used in child device calcs
592 //if all devices in room are on, room is on
593 if ( cnt == onCnt ) { // all devices are on
594 sendEvent( dni, [name: "switch",value:"on"] )
595 sendEvent( dni, [name: "power",value:usingPower.round(1)] )
597 } else { //if any device in room is off, room is off
598 sendEvent( dni, [name: "switch",value:"off"] )
599 sendEvent( dni, [name: "power",value:0.0] )
602 debugOut "Room Using Power: ${usingPower.round(1)}"
605 def poll(childDevice) {
606 debugOut "In poll() with ${childDevice}"
609 def dni = childDevice.device.deviceNetworkId
615 if ( isRoom(dni) ) { // this is a room, not a bulb
620 data = "<gip><version>1</version><token>${atomicState.token}</token><did>${dni}</did></gip>"
621 cmd = "DeviceGetInfo"
629 cmd = toQueryString(qParams)
631 apiPost(cmd) { response ->
632 bulbData = response.data.gip
635 debugOut "This Bulbs Data Return = ${bulbData}"
637 def bulb = getChildDevice( dni )
639 //set the devices power max setting to do calcs within the device type
640 if ( bulbData.other.bulbpower )
641 sendEvent( dni, [name: "setBulbPower",value:"${bulbData.other.bulbpower}"] )
643 if (( bulbData.state == "1" ) && ( bulb?.currentValue("switch") != "on" ))
644 sendEvent( dni, [name: "switch",value:"on"] )
646 if (( bulbData.state == "0" ) && ( bulb?.currentValue("switch") != "off" ))
647 sendEvent( dni, [name: "switch",value:"off"] )
649 //if ( bulbData.level != bulb?.currentValue("level")) {
650 // sendEvent( dni, [name: "level",value: "${bulbData.level}"] )
651 // sendEvent( dni, [name: "setLevel",value: "${bulbData.level}"] )
654 if (( bulbData.state == "1" ) && ( bulbData.other.bulbpower )) {
655 def levelSetting = bulbData.level as float
656 def bulbPowerMax = bulbData.other.bulbpower as float
657 def calculatedPower = bulbPowerMax * (levelSetting / 100)
658 sendEvent( dni, [name: "power", value: calculatedPower.round(1)] )
661 if (( bulbData.state == "0" ) && ( bulbData.other.bulbpower ))
662 sendEvent( dni, [name: "power", value: 0.0] )