Update laundry-monitor.groovy
[smartapps.git] / official / tcp-bulbs-connect.groovy
1 /**
2  *  TCP Bulbs (Connect)
3  *
4  *  Copyright 2014 Todd Wackford
5  *
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:
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
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.
14  *
15  */
16
17 import java.security.MessageDigest;
18
19 private apiUrl() { "https://tcp.greenwavereality.com/gwr/gop.php?" }
20
21 definition(
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",
29         singleInstance: true
30 )
31
32
33 preferences {
34         def msg = """Tap 'Next' after you have entered in your TCP Mobile remote credentials.
35
36 Once your credentials are accepted, SmartThings will scan your TCP installation for Bulbs."""
37
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
42                         paragraph msg
43                 }
44         }
45
46         page(name: "chooseBulbs", title: "Choose Bulbs to Control With SmartThings", content: "initialize")
47 }
48
49 def installed() {
50         debugOut "Installed with settings: ${settings}"
51
52         unschedule()
53         unsubscribe()
54
55         setupBulbs()
56
57         def cron = "0 11 23 * * ?"
58         log.debug "schedule('$cron', syncronizeDevices)"
59         schedule(cron, syncronizeDevices)
60 }
61
62 def updated() {
63         debugOut "Updated with settings: ${settings}"
64
65         unschedule()
66
67         setupBulbs()
68
69         def cron = "0 11 23 * * ?"
70         log.debug "schedule('$cron', syncronizeDevices)"
71         schedule(cron, syncronizeDevices)
72 }
73
74 def uninstalled()
75 {
76         unschedule() //in case we have hanging runIn()'s
77 }
78
79 private removeChildDevices(delete)
80 {
81         debugOut "deleting ${delete.size()} bulbs"
82         debugOut "deleting ${delete}"
83         delete.each {
84                 deleteChildDevice(it.device.deviceNetworkId)
85         }
86 }
87
88 def uninstallFromChildDevice(childDevice)
89 {
90         def errorMsg = "uninstallFromChildDevice was called and "
91         if (!settings.selectedBulbs) {
92                 debugOut errorMsg += "had empty list passed in"
93                 return
94         }
95
96         def dni = childDevice.device.deviceNetworkId
97
98         if ( !dni ) {
99                 debugOut errorMsg += "could not find dni of device"
100                 return
101         }
102
103         def newDeviceList = settings.selectedBulbs - dni
104         app.updateSetting("selectedBulbs", newDeviceList)
105
106         debugOut errorMsg += "completed succesfully"
107 }
108
109
110 def setupBulbs() {
111         debugOut "In setupBulbs"
112
113         def bulbs = state.devices
114         def deviceFile = "TCP Bulb"
115
116         selectedBulbs.each { did ->
117                 //see if this is a selected bulb and install it if not already
118                 def d = getChildDevice(did)
119
120                 if(!d) {
121                         def newBulb = bulbs.find { (it.did) == did }
122                         d = addChildDevice("wackford", deviceFile, did, null, [name: "${newBulb?.name}", label: "${newBulb?.name}", completedSetup: true])
123
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")
127                                 d.save()
128                         }*/
129
130                 } else {
131                         debugOut "We already added this device"
132                 }
133         }
134
135         // Delete any that are no longer in settings
136         def delete = getChildDevices().findAll { !selectedBulbs?.contains(it.deviceNetworkId) }
137         removeChildDevices(delete)
138
139         //we want to ensure syncronization between rooms and bulbs
140         //syncronizeDevices()
141 }
142
143 def initialize() {
144
145         atomicState.token = ""
146
147         getToken()
148
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) {
151                         section("") {}
152                 }
153         } else {
154                 "we're good to go"
155                 debugOut "We have Token."
156         }
157
158         //getGatewayData() //we really don't need anything from the gateway
159
160         deviceDiscovery()
161
162         def options = devicesDiscovered() ?: []
163
164         def msg = """Tap 'Done' after you have selected the desired devices."""
165
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
169                         paragraph msg
170                 }
171         }
172 }
173
174 def deviceDiscovery() {
175         def data = "<gip><version>1</version><token>${atomicState.token}</token></gip>"
176
177         def Params = [
178                 cmd: "RoomGetCarousel",
179                 data: "${data}",
180                 fmt: "json"
181         ]
182
183         def cmd = toQueryString(Params)
184
185         def rooms = ""
186
187         apiPost(cmd) { response ->
188                 rooms = response.data.gip.room
189         }
190
191         debugOut "rooms data = ${rooms}"
192
193         def devices = []
194         def bulbIndex = 1
195         def lastRoomName = null
196         def deviceList = []
197
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"]
205                         devices.each({
206                                 debugOut "Bulb Device Data: did:${it?.did} room:${roomName} BulbName:${it?.name}"
207                                 deviceList += ["name" : "${roomName} ${it?.name}", "did" : "${it?.did}", "type" : "bulb"]
208                         })
209                 } else {
210                         debugOut "Bulb Device Data: did:${it?.did} room:${roomName} BulbName:${it?.name}"
211                         deviceList += ["name" : "${roomName} ${it?.name}", "did" : "${it?.did}", "type" : "bulb"]
212                 }
213         } else {
214                 rooms.each({
215                         devices  = it.device
216                         def roomName = it.name
217                         if ( devices[1] != null ) {
218                                 def roomId = it?.rid
219                                 debugOut "Room Device Data: did:${roomId} roomName:${roomName}"
220                                 //deviceList += ["name" : "${roomName}", "did" : "${roomId}", "type" : "room"]
221                                 devices.each({
222                                         debugOut "Bulb Device Data: did:${it?.did} room:${roomName} BulbName:${it?.name}"
223                                         deviceList += ["name" : "${roomName} ${it?.name}", "did" : "${it?.did}", "type" : "bulb"]
224                                 })
225                         } else {
226                                 debugOut "Bulb Device Data: did:${devices?.did} room:${roomName} BulbName:${devices?.name}"
227                                 deviceList += ["name" : "${roomName} ${devices?.name}", "did" : "${devices?.did}", "type" : "bulb"]
228                         }
229                 })
230         }
231         devices = ["devices" : deviceList]
232         state.devices = devices.devices
233 }
234
235 Map devicesDiscovered() {
236         def devices =  state.devices
237         def map = [:]
238         if (devices instanceof java.util.Map) {
239                 devices.each {
240                         def value = "${it?.name}"
241                         def key = it?.did
242                         map["${key}"] = value
243                 }
244         } else { //backwards compatable
245                 devices.each {
246                         def value = "${it?.name}"
247                         def key = it?.did
248                         map["${key}"] = value
249                 }
250         }
251         map
252 }
253
254 def getGatewayData() {
255         debugOut "In getGatewayData"
256
257         def data = "<gip><version>1</version><token>${atomicState.token}</token></gip>"
258
259         def qParams = [
260                 cmd: "GatewayGetInfo",
261                 data: "${data}",
262                 fmt: "json"
263         ]
264
265         def cmd = toQueryString(qParams)
266
267         apiPost(cmd) { response ->
268                 debugOut "the gateway reponse is ${response.data.gip.gateway}"
269         }
270
271 }
272
273 def getToken() {
274
275         atomicState.token = ""
276
277         if (password) {
278                 def hashedPassword = generateMD5(password)
279
280                 def data = "<gip><version>1</version><email>${username}</email><password>${hashedPassword}</password></gip>"
281
282                 def qParams = [
283                         cmd : "GWRLogin",
284                         data: "${data}",
285                         fmt : "json"
286                 ]
287
288                 def cmd = toQueryString(qParams)
289
290                 apiPost(cmd) { response ->
291                         def status = response.data.gip.rc
292
293                         //sendNotificationEvent("Get token status ${status}")
294
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"
299                         } else {
300                                 atomicState.token = response.data.gip.token
301                         }
302                 }
303         } else {
304                 log.warn "Unable to log into TCP Gateway. Error = Password is null"
305                 atomicState.token = "error"
306         }
307 }
308
309 def apiPost(String data, Closure callback) {
310         //debugOut "In apiPost with data: ${data}"
311         def params = [
312                 uri: apiUrl(),
313                 body: data
314         ]
315
316         httpPost(params) {
317                 response ->
318                         def rc = response.data.gip.rc
319
320                         if ( rc == "200" ) {
321                                 debugOut ("Return Code = ${rc} = Command Succeeded.")
322                                 callback.call(response)
323
324                         } else if ( rc == "401" ) {
325                                 debugOut "Return Code = ${rc} = Error: User not logged in!" //Error code from gateway
326                                 log.debug "Refreshing Token"
327                                 getToken()
328                                 //callback.call(response) //stubbed out so getToken works (we had race issue)
329
330                         } else {
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)
334                         }
335         }
336 }
337
338
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()"
342
343         debugOut "deviceCapability: ${deviceCapability}"
344         debugOut "usePercentage: ${usePercentage}"
345
346         def calcPower = usePercentage * 1000
347         def reportPower = calcPower.round(1) as String
348
349         debugOut "report power = ${reportPower}"
350
351         return reportPower
352 }
353
354 def generateSha256(String s) {
355
356         MessageDigest digest = MessageDigest.getInstance("SHA-256")
357         digest.update(s.bytes)
358         new BigInteger(1, digest.digest()).toString(16).padLeft(40, '0')
359 }
360
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')
365 }
366
367 String toQueryString(Map m) {
368         return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
369 }
370
371 def checkDevicesOnline(bulbs) {
372         debugOut "In checkDevicesOnline()"
373
374         def onlineBulbs = []
375         def thisBulb = []
376
377         bulbs.each {
378                 def dni = it?.did
379                 thisBulb = it
380
381                 def data = "<gip><version>1</version><token>${atomicState.token}</token><did>${dni}</did></gip>"
382
383                 def qParams = [
384                         cmd: "DeviceGetInfo",
385                         data: "${data}",
386                         fmt: "json"
387                 ]
388
389                 def cmd = toQueryString(qParams)
390
391                 def bulbData = []
392
393                 apiPost(cmd) { response ->
394                         bulbData = response.data.gip
395                 }
396
397                 if ( bulbData?.offline == "1" ) {
398                         debugOut "${it?.name} is offline with offline value of ${bulbData?.offline}"
399
400                 } else {
401                         debugOut "${it?.name} is online with offline value of ${bulbData?.offline}"
402                         onlineBulbs += thisBulb
403                 }
404         }
405         return onlineBulbs
406 }
407
408 def syncronizeDevices() {
409         debugOut "In syncronizeDevices"
410
411         def update = getChildDevices().findAll { selectedBulbs?.contains(it.deviceNetworkId) }
412
413         update.each {
414                 def dni = getChildDevice( it.deviceNetworkId )
415                 debugOut "dni = ${dni}"
416
417                 if (isRoom(dni)) {
418                         pollRoom(dni)
419                 } else {
420                         poll(dni)
421                 }
422         }
423 }
424
425 boolean isRoom(dni) {
426         def device = state.devices.find() {(( it.type == 'room') && (it.did == "${dni}"))}
427 }
428
429 boolean isBulb(dni) {
430         def device = state.devices.find() {(( it.type == 'bulb') && (it.did == "${dni}"))}
431 }
432
433 def debugEvent(message, displayEvent) {
434
435         def results = [
436                 name: "appdebug",
437                 descriptionText: message,
438                 displayed: displayEvent
439         ]
440         log.debug "Generating AppDebug Event: ${results}"
441         sendEvent (results)
442
443 }
444
445 def debugOut(msg) {
446         //log.debug msg
447         //sendNotificationEvent(msg) //Uncomment this for troubleshooting only
448 }
449
450
451 /**************************************************************************
452  Child Device Call In Methods
453  **************************************************************************/
454 def on(childDevice) {
455         debugOut "On request from child device"
456
457         def dni = childDevice.device.deviceNetworkId
458         def data = ""
459         def cmd = ""
460
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"
464         } else {
465                 data = "<gip><version>1</version><token>$atomicState.token</token><did>${dni}</did><type>power</type><value>1</value></gip>"
466                 cmd = "DeviceSendCommand"
467         }
468
469         def qParams = [
470                 cmd: cmd,
471                 data: "${data}",
472                 fmt: "json"
473         ]
474
475         cmd = toQueryString(qParams)
476
477         apiPost(cmd) { response ->
478                 debugOut "ON result: ${response.data}"
479         }
480
481         //we want to ensure syncronization between rooms and bulbs
482         //runIn(2, "syncronizeDevices")
483 }
484
485 def off(childDevice) {
486         debugOut "Off request from child device"
487
488         def dni = childDevice.device.deviceNetworkId
489         def data = ""
490         def cmd = ""
491
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"
495         } else {
496                 data = "<gip><version>1</version><token>$atomicState.token</token><did>${dni}</did><type>power</type><value>0</value></gip>"
497                 cmd = "DeviceSendCommand"
498         }
499
500         def qParams = [
501                 cmd: cmd,
502                 data: "${data}",
503                 fmt: "json"
504         ]
505
506         cmd = toQueryString(qParams)
507
508         apiPost(cmd) { response ->
509                 debugOut "${response.data}"
510         }
511
512         //we want to ensure syncronization between rooms and bulbs
513         //runIn(2, "syncronizeDevices")
514 }
515
516 def setLevel(childDevice, value) {
517         debugOut "setLevel request from child device"
518
519         def dni = childDevice.device.deviceNetworkId
520         def data = ""
521         def cmd = ""
522
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"
526         } else {
527                 data = "<gip><version>1</version><token>${atomicState.token}</token><did>${dni}</did><type>level</type><value>${value}</value></gip>"
528                 cmd = "DeviceSendCommand"
529         }
530
531         def qParams = [
532                 cmd: cmd,
533                 data: "${data}",
534                 fmt: "json"
535         ]
536
537         cmd = toQueryString(qParams)
538
539         apiPost(cmd) { response ->
540                 debugOut "${response.data}"
541         }
542
543         //we want to ensure syncronization between rooms and bulbs
544         //runIn(2, "syncronizeDevices")
545 }
546
547 // Really not called from child, but called from poll() if it is a room
548 def pollRoom(dni) {
549         debugOut "In pollRoom"
550         def data = ""
551         def cmd = ""
552         def roomDeviceData = []
553
554         data = "<gip><version>1</version><token>${atomicState.token}</token><rid>${dni}</rid><fields>name,power,control,status,state</fields></gip>"
555         cmd = "RoomGetDevices"
556
557         def qParams = [
558                 cmd: cmd,
559                 data: "${data}",
560                 fmt: "json"
561         ]
562
563         cmd = toQueryString(qParams)
564
565         apiPost(cmd) { response ->
566                 roomDeviceData = response.data.gip
567         }
568
569         debugOut "Room Data: ${roomDeviceData}"
570
571         def totalPower = 0
572         def totalLevel = 0
573         def cnt = 0
574         def onCnt = 0 //used to tally on/off states
575
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()
581                         cnt += 1
582                 }
583         })
584
585         def avgLevel = totalLevel/cnt
586         def usingPower = totalPower * (avgLevel / 100) as float
587         def room = getChildDevice( dni )
588
589         //the device is a room but we use same type file
590         sendEvent( dni, [name: "setBulbPower",value:"${totalPower}"] ) //used in child device calcs
591
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)] )
596
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] )
600         }
601
602         debugOut "Room Using Power: ${usingPower.round(1)}"
603 }
604
605 def poll(childDevice) {
606         debugOut "In poll() with ${childDevice}"
607
608
609         def dni = childDevice.device.deviceNetworkId
610
611         def bulbData = []
612         def data = ""
613         def cmd = ""
614
615         if ( isRoom(dni) ) { // this is a room, not a bulb
616                 pollRoom(dni)
617                 return
618         }
619
620         data = "<gip><version>1</version><token>${atomicState.token}</token><did>${dni}</did></gip>"
621         cmd = "DeviceGetInfo"
622
623         def qParams = [
624                 cmd: cmd,
625                 data: "${data}",
626                 fmt: "json"
627         ]
628
629         cmd = toQueryString(qParams)
630
631         apiPost(cmd) { response ->
632                 bulbData = response.data.gip
633         }
634
635         debugOut "This Bulbs Data Return = ${bulbData}"
636
637         def bulb = getChildDevice( dni )
638
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}"] )
642
643         if (( bulbData.state == "1" ) && ( bulb?.currentValue("switch") != "on" ))
644                 sendEvent( dni, [name: "switch",value:"on"] )
645
646         if (( bulbData.state == "0" ) && ( bulb?.currentValue("switch") != "off" ))
647                 sendEvent( dni, [name: "switch",value:"off"] )
648
649         //if ( bulbData.level != bulb?.currentValue("level")) {
650         //      sendEvent( dni, [name: "level",value: "${bulbData.level}"] )
651         //    sendEvent( dni, [name: "setLevel",value: "${bulbData.level}"] )
652         //}
653
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)] )
659         }
660
661         if (( bulbData.state == "0" ) && ( bulbData.other.bulbpower ))
662                 sendEvent( dni, [name: "power", value: 0.0] )
663 }