Update thermostat-mode-director.groovy
[smartapps.git] / official / yoics-connect.groovy
1 /**
2  *  Copyright 2015 SmartThings
3  *
4  *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5  *  in compliance with the License. You may obtain a copy of the License at:
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
10  *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
11  *  for the specific language governing permissions and limitations under the License.
12  *
13  *  Yoics Service Manager
14  *
15  *  Author: SmartThings
16  *  Date: 2013-11-19
17  */
18
19 definition(
20     name: "Yoics (Connect)",
21     namespace: "smartthings",
22     author: "SmartThings",
23     description: "Connect and Control your Yoics Enabled Devices",
24     category: "SmartThings Internal",
25     iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
26     iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png",
27     oauth: true,
28     singleInstance: true
29 ) {
30   appSetting "serverUrl"
31 }
32
33 preferences {
34         page(name: "auth", title: "Sign in", content: "authPage", uninstall:true)
35         page(name: "page2", title: "Yoics Devices", install:true, content: "listAvailableCameras")
36 }
37
38
39 mappings {
40         path("/foauth") {
41                 action: [
42                         GET: "foauth"
43                 ]
44         }
45         path("/authorize") {
46                 action: [
47                         POST: "authorize"
48                 ]
49         }
50
51 }
52
53 def authPage()
54 {
55         log.debug "authPage()"
56
57         if(!state.accessToken)
58         {
59                 log.debug "about to create access token"
60                 createAccessToken()
61         }
62
63
64         def description = "Required"
65
66         if(getAuthHashValueIsValid())
67         {
68                 // TODO: Check if it's valid
69                 if(true)
70                 {
71                         description = "Already saved"
72                 }
73                 else
74                 {
75                         description = "Required"
76                 }
77         }
78
79         def redirectUrl = buildUrl("", "foauth")
80
81         return dynamicPage(name: "auth", title: "Yoics", nextPage:"page2") {
82                 section("Yoics Login"){
83                         href url:redirectUrl, style:"embedded", required:false, title:"Yoics", description:description
84                 }
85         }
86
87 }
88
89 def buildUrl(String key, String endpoint="increment", Boolean absolute=true)
90 {
91         if(key) {
92                 key = "/${key}"
93         }
94
95         def url = "/api/smartapps/installations/${app.id}/${endpoint}${key}?access_token=${state.accessToken}"
96
97         if (q) {
98                 url += "q=${q}"
99         }
100
101         if(absolute)
102         {
103                 url = serverUrl + url
104         }
105
106         return url
107 }
108
109 //Deprecated
110 def getServerName() {
111         return getServerUrl()
112 }
113
114 def getServerUrl() {
115   return appSettings.serverUrl
116 }
117
118 def listAvailableCameras() {
119
120         //def loginResult = forceLogin()
121
122         //if(loginResult.success)
123         //{
124                 state.cameraNames = [:]
125
126                 def cameras = getDeviceList().inject([:]) { c, it ->
127                         def dni = [app.id, it.uuid].join('.')
128                         def cameraName = it.title ?: "Yoics"
129
130                         state.cameraNames[dni] = cameraName
131                         c[dni] = cameraName
132
133                         return c
134                 }
135
136                 return dynamicPage(name: "page2", title: "Yoics Devices", install:true) {
137                         section("Select which Yoics Devices to connect"){
138                                 input(name: "cameras", title:"", type: "enum", required:false, multiple:true, metadata:[values:cameras])
139                         }
140                         section("Turn on which Lights when taking pictures")
141                         {
142                                 input "switches", "capability.switch", multiple: true, required:false
143                         }
144                 }
145         //}
146         /*else
147         {
148                 log.error "login result false"
149                 return [errorMessage:"There was an error logging in to Dropcam"]
150         }*/
151
152 }
153
154
155 def installed() {
156         log.debug "Installed with settings: ${settings}"
157
158         initialize()
159 }
160
161 def updated() {
162         log.debug "Updated with settings: ${settings}"
163
164         unsubscribe()
165         initialize()
166 }
167
168 def uninstalled() {
169         removeChildDevices(getChildDevices())
170 }
171
172 def initialize() {
173
174         if(!state.suppressDelete)
175         {
176                 state.suppressDelete = [:]
177         }
178
179         log.debug "settings: $settings"
180
181         def devices = cameras.collect { dni ->
182
183                 def name = state.cameraNames[dni] ?: "Yoics Device"
184
185                 def d = getChildDevice(dni)
186
187                 if(!d)
188                 {
189                         d = addChildDevice("smartthings", "Yoics Camera", dni, null, [name:"YoicsCamera", label:name])
190
191                         /* WE'LL GET PROXY ON TAKE REQUEST
192                         def setupProxyResult = setupProxy(dni)
193                         if(setupProxyResult.success)
194                         {
195                                 log.debug "Setting up the proxy worked...taking image capture now?"
196
197                         }
198                         */
199
200                         //Let's not take photos on add
201                         //d.take()
202
203                         log.debug "created ${d.displayName} with id $dni"
204                 }
205                 else
206                 {
207                         log.debug "found ${d.displayName} with id $dni already exists"
208                 }
209
210                 return d
211         }
212
213         log.debug "created ${devices.size()} dropcams"
214
215         /* //Original Code seems to delete the dropcam that is being added */
216
217         // Delete any that are no longer in settings
218         def delete = getChildDevices().findAll { !cameras?.contains(it.deviceNetworkId) }
219         removeChildDevices(delete)
220 }
221
222 private removeChildDevices(delete)
223 {
224         log.debug "deleting ${delete.size()} dropcams"
225         delete.each {
226                 state.suppressDelete[it.deviceNetworkId] = true
227                 deleteChildDevice(it.deviceNetworkId)
228                 state.suppressDelete.remove(it.deviceNetworkId)
229         }
230 }
231 private List getDeviceList()
232 {
233
234         //https://apilb.yoics.net/web/api/getdevices.ashx?token=&filter=all&whose=me&state=%20all&type=xml
235
236         def deviceListParams = [
237                 uri: "https://apilb.yoics.net",
238                 path: "/web/api/getdevices.ashx",
239                 headers: ['User-Agent': validUserAgent()],
240                 requestContentType: "application/json",
241                 query: [token: getLoginTokenValue(), filter: "all", whose: "me", state: "all", type:"json" ]
242         ]
243
244         log.debug "cam list via: $deviceListParams"
245
246         def multipleHtml
247         def singleUrl
248         def something
249         def more
250
251         def devices = []
252
253         httpGet(deviceListParams) { resp ->
254
255                 log.debug "getting device list..."
256
257                 something = resp.status
258                 more = "headers: " + resp.headers.collect { "${it.name}:${it.value}" }
259
260                 if(resp.status == 200)
261                 {
262                         def jsonString = resp.data.str
263                         def body = new groovy.json.JsonSlurper().parseText(jsonString)
264
265                         //log.debug "get devices list response: ${jsonString}"
266                         //log.debug "get device list response: ${body}"
267
268                         body.NewDataSet.Table.each { d ->
269                                 //log.debug "Addding ${d.devicealias} with address: ${d.deviceaddress}"
270                                 devices << [title: d.devicealias, uuid: d.deviceaddress]        //uuid should be another name
271                         }
272
273                 }
274                 else
275                 {
276                         // ERROR
277                         log.error "camera list: unknown response"
278                 }
279
280         }
281
282          log.debug "list: after getting cameras: " + [devices:devices, url:singleUrl, html:multipleHtml?.size(), something:something, more:more]
283
284         // ERROR?
285         return devices
286 }
287
288 def removeChildFromSettings(child)
289 {
290         def device = child.device
291
292         def dni = device.deviceNetworkId
293         log.debug "removing child device $device with dni ${dni}"
294
295         if(!state?.suppressDelete?.get(dni))
296         {
297                 def newSettings = settings.cameras?.findAll { it != dni } ?: []
298                 app.updateSetting("cameras", newSettings)
299         }
300 }
301
302 private forceLogin() {
303         updateAuthHash(null)
304         login()
305 }
306
307
308 private login() {
309
310         if(getAuthHashValueIsValid())
311         {
312                 return [success:true]
313         }
314         return doLogin()
315 }
316
317 /*private setupProxy(dni) {
318         //https://apilb.yoics.net/web/api/connect.ashx?token=&deviceaddress=00:00:48:02:2A:A2:08:0E&type=xml
319
320         def address = dni?.split(/\./)?.last()
321
322         def loginParams = [
323                 uri: "https://apilb.yoics.net",
324                 path: "/web/api/connect.ashx",
325                 headers: ['User-Agent': validUserAgent()],
326                 requestContentType: "application/json",
327                 query: [token: getLoginTokenValue(), deviceaddress:address, type:"json" ]
328         ]
329
330         def result = [success:false]
331
332         httpGet(loginParams) { resp ->
333                 if (resp.status == 200) //&& resp.headers.'Content-Type'.contains("application/json")
334                 {
335                         log.debug "login 200 json headers: " + resp.headers.collect { "${it.name}:${it.value}" }
336                         def jsonString = resp.data.str
337                         def body = new groovy.json.JsonSlurper().parseText(jsonString)
338
339                         def proxy = body?.NewDataSet?.Table[0]?.proxy
340                         def requested = body?.NewDataSet?.Table[0]?.requested
341                         def expirationsec = body?.NewDataSet?.Table[0]?.expirationsec
342                         def url = body?.NewDataSet?.Table[0]?.url
343
344                         def proxyMap = [proxy:proxy, requested: requested, expirationsec:expirationsec, url: url]
345
346                         if (proxy) {
347                                 //log.debug "setting ${dni} proxy to ${proxyMap}"
348                                 //updateDeviceProxy(address, proxyMap)
349                                 result.success = true
350                         }
351                         else
352                         {
353                                 // ERROR: any more information we can give?
354                                 result.reason = "Bad login"
355                         }
356                 }
357                 else
358                 {
359                         // ERROR: any more information we can give?
360                         result.reason = "Bad login"
361                 }
362
363
364         }
365
366         return result
367 }*/
368
369
370
371 private doLogin(user = "", pwd = "") { //change this name
372
373         def loginParams = [
374                 uri: "https://apilb.yoics.net",
375                 path: "/web/api/login.ashx",
376                 headers: ['User-Agent': validUserAgent()],
377                 requestContentType: "application/json",
378                 query: [key: "SmartThingsApplication", usr: username, pwd: password, apilevel: 12, type:"json" ]
379         ]
380
381         if (user) {
382                 loginParams.query = [key: "SmartThingsApplication", usr: user, pwd: pwd, apilevel: 12, type:"json" ]
383         }
384
385         def result = [success:false]
386
387         httpGet(loginParams) { resp ->
388                 if (resp.status == 200) //&& resp.headers.'Content-Type'.contains("application/json")
389                 {
390                         log.debug "login 200 json headers: " + resp.headers.collect { "${it.name}:${it.value}" }
391                         def jsonString = resp.data.str
392                         def body = new groovy.json.JsonSlurper().parseText(jsonString)
393
394                         log.debug "login response: ${jsonString}"
395                         log.debug "login response: ${body}"
396
397                         def authhash = body?.NewDataSet?.Table[0]?.authhash //.token
398
399                         //this may return as well??
400                         def token = body?.NewDataSet?.Table[0]?.token ?: null
401
402                         if (authhash) {
403                                 log.debug "login setting authhash to ${authhash}"
404                                 updateAuthHash(authhash)
405                                 if (token) {
406                                         log.debug "login setting login token to ${token}"
407                                         updateLoginToken(token)
408                                         result.success = true
409                                 } else {
410                                         result.success = doLoginToken()
411                                 }
412                         }
413                         else
414                         {
415                                 // ERROR: any more information we can give?
416                                 result.reason = "Bad login"
417                         }
418                 }
419                 else
420                 {
421                         // ERROR: any more information we can give?
422                         result.reason = "Bad login"
423                 }
424
425
426         }
427
428         return result
429 }
430
431 private doLoginToken() {
432
433         def loginParams = [
434                 uri: "https://apilb.yoics.net",
435                 path: "/web/api/login.ashx",
436                 headers: ['User-Agent': validUserAgent()],
437                 requestContentType: "application/json",
438                 query: [key: "SmartThingsApplication", usr: getUserName(), auth: getAuthHashValue(), apilevel: 12, type:"json" ]
439         ]
440
441         def result = [success:false]
442
443         httpGet(loginParams) { resp ->
444                 if (resp.status == 200)
445                 {
446                         log.debug "login 200 json headers: " + resp.headers.collect { "${it.name}:${it.value}" }
447
448                         def jsonString = resp.data.str
449                         def body = new groovy.json.JsonSlurper().parseText(jsonString)
450
451                         def token = body?.NewDataSet?.Table[0]?.token
452
453                         if (token) {
454                                 log.debug "login setting login to $token"
455                                 updateLoginToken(token)
456                                 result.success = true
457                         }
458                         else
459                         {
460                                 // ERROR: any more information we can give?
461                                 result.reason = "Bad login"
462                         }
463                 }
464                 else
465                 {
466                         // ERROR: any more information we can give?
467                         result.reason = "Bad login"
468                 }
469
470
471         }
472
473         return result
474 }
475
476 def takePicture(String dni, Integer imgWidth=null)
477 {
478
479         //turn on any of the selected lights that are off
480         def offLights = switches.findAll{(it.currentValue("switch") == "off")}
481         log.debug offLights
482         offLights.collect{it.on()}
483
484         log.debug "parent.takePicture(${dni}, ${imgWidth})"
485
486         def uuid = dni?.split(/\./)?.last()
487
488         log.debug "taking picture for $uuid (${dni})"
489
490         def imageBytes
491         def loginRequired = false
492
493         try
494         {
495                 imageBytes = doTakePicture(uuid, imgWidth)
496         }
497         catch(Exception e)
498         {
499                 log.error "Exception $e trying to take a picture, attempting to login again"
500                 loginRequired = true
501         }
502
503         if(loginRequired)
504         {
505                 def loginResult = doLoginToken()
506                 if(loginResult.success)
507                 {
508                         // try once more
509                         imageBytes = doTakePicture(uuid, imgWidth)
510                 }
511                 else
512                 {
513                         log.error "tried to login to dropcam after failing to take a picture and failed"
514                 }
515         }
516
517         //turn previously off lights to their original state
518         offLights.collect{it.off()}
519         return imageBytes
520 }
521
522 private doTakePicture(String uuid, Integer imgWidth)
523 {
524         imgWidth = imgWidth ?: 1280
525         def loginRequired = false
526
527         def proxyParams = getDeviceProxy(uuid)
528         if(!proxyParams.success)
529         {
530                 throw new Exception("Login Required")
531         }
532
533         def takeParams = [
534                 uri: "${proxyParams.uri}",
535                 path: "${proxyParams.path}",
536                 headers: ['User-Agent': validUserAgent()]
537         ]
538
539         def imageBytes
540
541         httpGet(takeParams) { resp ->
542
543                 if(resp.status == 403)
544                 {
545                         loginRequired = true
546                 }
547                 else if (resp.status == 200 && resp.headers.'Content-Type'.contains("image/jpeg"))
548                 {
549                         imageBytes = resp.data
550                 }
551                 else
552                 {
553                         log.error "unknown takePicture() response: ${resp.status} - ${resp.headers.'Content-Type'}"
554                 }
555         }
556
557         if(loginRequired)
558         {
559                 throw new Exception("Login Required")
560         }
561
562         return imageBytes
563 }
564
565 /////////////////////////
566 private Boolean getLoginTokenValueIsValid()
567 {
568         return getLoginTokenValue()
569 }
570
571 private updateLoginToken(String token) {
572         state.loginToken = token
573 }
574
575 private getLoginTokenValue() {
576         state.loginToken
577 }
578
579 private Boolean getAuthHashValueIsValid()
580 {
581         return getAuthHashValue()
582 }
583
584 private updateAuthHash(String hash) {
585         state.authHash = hash
586 }
587
588 private getAuthHashValue() {
589         state.authHash
590 }
591
592 private updateUserName(String username) {
593         state.username = username
594 }
595
596 private getUserName() {
597         state.username
598 }
599
600 /*private getDeviceProxy(dni){
601         //check if it exists or is not longer valid and create a new proxy here
602         log.debug "returning proxy ${state.proxy[dni].proxy}"
603         def proxy = [uri:state.proxy[dni].proxy, path:state.proxy[dni].url]
604         log.debug "returning proxy ${proxy}"
605         proxy
606 }*/
607
608 private updateDeviceProxy(dni, map){
609         if (!state.proxy) { state.proxy = [:] }
610         state.proxy[dni] = map
611 }
612
613 private getDeviceProxy(dni) {
614         def address = dni?.split(/\./)?.last()
615
616         def loginParams = [
617                 uri: "https://apilb.yoics.net",
618                 path: "/web/api/connect.ashx",
619                 headers: ['User-Agent': validUserAgent()],
620                 requestContentType: "application/json",
621                 query: [token: getLoginTokenValue(), deviceaddress:address, type:"json" ]
622         ]
623
624         def result = [success:false]
625
626         httpGet(loginParams) { resp ->
627                 if (resp.status == 200) //&& resp.headers.'Content-Type'.contains("application/json")
628                 {
629                         log.debug "login 200 json headers: " + resp.headers.collect { "${it.name}:${it.value}" }
630                         def jsonString = resp.data.str
631                         def body = new groovy.json.JsonSlurper().parseText(jsonString)
632
633                         if (body?.NewDataSet?.Table[0]?.error)
634                         {
635                                 log.error "Attempt to get Yoics Proxy failed"
636                                 // ERROR: any more information we can give?
637                                 result.reason = body?.NewDataSet?.Table[0]?.message
638                         }
639                         else
640                         {
641                                 result.uri = body?.NewDataSet?.Table[0]?.proxy
642                                 result.path = body?.NewDataSet?.Table[0]?.url
643                                 result.requested = body?.NewDataSet?.Table[0]?.requested
644                                 result.expirationsec = body?.NewDataSet?.Table[0]?.expirationsec
645                                 result.success = true
646                         }
647
648                 }
649                 else
650                 {
651                         // ERROR: any more information we can give?
652                         result.reason = "Bad login"
653                 }
654
655
656         }
657
658         return result
659
660 }
661
662 private validUserAgent() {
663         "curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8x zlib/1.2.5"
664 }
665
666 def foauth() {
667         def html = """<html>
668 <head>
669         <title>$inputQuery results</title>
670         <meta name="apple-mobile-web-app-capable" content="yes" />
671         <meta name="viewport" content="width=device-width, initial-scale=1.0">
672         <link rel="shortcut icon" href="/static/sT2cZkBCCKJduBLfQ6NfUjZg1AiMhFK9ESNxUjjlvsk.ico" type="image/x-icon">
673         <link rel="apple-touch-icon" href="/static/7UIUNICQhrzmPRYK3T7j5BhAsvUIbKE8OARNI702Dw9.png">
674         <link rel="apple-touch-icon" sizes="114x114" href="/static/HkpqhLsUc5flOzvxrpaoyybhcCP1iRd0ogxhWFJ9vKo.png">
675
676         <script src="/static/1vXORVkZK58St3QjdbzerXZDi9MfZQ8Q3wCyumiNiep.js" type="text/javascript" ></script>
677         <link href="/static/ZLo6WmGLBQwvykZ4sFgJS1W8IKyGj3TKdKZXyHcBB9l.css" type="text/css" rel="stylesheet" media="screen, projection" />
678         <link rel="stylesheet" href="/static/sd6ug4HGJyhdTwTONDZK6Yw8VsYbyDa4qUPgLokOkTn.css" type="text/css">
679
680 </head>
681 <body>
682
683         <h1>
684                 Yoics Login
685         </h1>
686
687 <form name="login" action="${buildUrl("", "authorize")}" method="post">
688 User:
689 <br>
690 <input type="text" name="user" style="height: 50px;">
691 <br>
692 <br>
693 Password:
694 <br>
695 <input type="password" name="password" style="height: 50px;">
696
697 <input type="submit" value="Submit">
698 </form>
699
700
701 </body>
702 </html>"""
703
704         render status: 200, contentType: 'text/html', data: html
705 }
706
707 def authorize() {
708
709         def loginResult = doLogin(params.user, params.password)
710
711         def result
712         if (loginResult.success) {
713         result = "Successful"
714
715         //save username
716                 updateUserName(params.user)
717     } else {
718         result = "Failed"
719     }
720
721         def html = """<html>
722 <head>
723         <title>$inputQuery results</title>
724         <meta name="apple-mobile-web-app-capable" content="yes" />
725
726 <meta name="viewport" content="width=device-width, initial-scale=1.0">
727 <link rel="shortcut icon" href="/static/sT2cZkBCCKJduBLfQ6NfUjZg1AiMhFK9ESNxUjjlvsk.ico" type="image/x-icon">
728 <link rel="apple-touch-icon" href="/static/7UIUNICQhrzmPRYK3T7j5BhAsvUIbKE8OARNI702Dw9.png">
729 <link rel="apple-touch-icon" sizes="114x114" href="/static/HkpqhLsUc5flOzvxrpaoyybhcCP1iRd0ogxhWFJ9vKo.png">
730
731
732
733 <script src="/static/1vXORVkZK58St3QjdbzerXZDi9MfZQ8Q3wCyumiNiep.js" type="text/javascript" ></script>
734 <link href="/static/ZLo6WmGLBQwvykZ4sFgJS1W8IKyGj3TKdKZXyHcBB9l.css" type="text/css" rel="stylesheet" media="screen, projection" />
735 <link rel="stylesheet" href="/static/sd6ug4HGJyhdTwTONDZK6Yw8VsYbyDa4qUPgLokOkTn.css" type="text/css">
736 <script>
737 function buildCmd(){
738
739 }
740 </script>
741 </head>
742 <body>
743
744         <h1>
745                 Yoics Login ${result}!
746         </h1>
747
748 </body>
749 </html>"""
750
751         render status: 200, contentType: 'text/html', data: html
752 }