2 * Copyright 2015 SmartThings
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:
7 * http://www.apache.org/licenses/LICENSE-2.0
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.
13 * Yoics Service Manager
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",
30 appSetting "serverUrl"
34 page(name: "auth", title: "Sign in", content: "authPage", uninstall:true)
35 page(name: "page2", title: "Yoics Devices", install:true, content: "listAvailableCameras")
55 log.debug "authPage()"
57 if(!state.accessToken)
59 log.debug "about to create access token"
64 def description = "Required"
66 if(getAuthHashValueIsValid())
68 // TODO: Check if it's valid
71 description = "Already saved"
75 description = "Required"
79 def redirectUrl = buildUrl("", "foauth")
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
89 def buildUrl(String key, String endpoint="increment", Boolean absolute=true)
95 def url = "/api/smartapps/installations/${app.id}/${endpoint}${key}?access_token=${state.accessToken}"
103 url = serverUrl + url
110 def getServerName() {
111 return getServerUrl()
115 return appSettings.serverUrl
118 def listAvailableCameras() {
120 //def loginResult = forceLogin()
122 //if(loginResult.success)
124 state.cameraNames = [:]
126 def cameras = getDeviceList().inject([:]) { c, it ->
127 def dni = [app.id, it.uuid].join('.')
128 def cameraName = it.title ?: "Yoics"
130 state.cameraNames[dni] = cameraName
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])
140 section("Turn on which Lights when taking pictures")
142 input "switches", "capability.switch", multiple: true, required:false
148 log.error "login result false"
149 return [errorMessage:"There was an error logging in to Dropcam"]
156 log.debug "Installed with settings: ${settings}"
162 log.debug "Updated with settings: ${settings}"
169 removeChildDevices(getChildDevices())
174 if(!state.suppressDelete)
176 state.suppressDelete = [:]
179 log.debug "settings: $settings"
181 def devices = cameras.collect { dni ->
183 def name = state.cameraNames[dni] ?: "Yoics Device"
185 def d = getChildDevice(dni)
189 d = addChildDevice("smartthings", "Yoics Camera", dni, null, [name:"YoicsCamera", label:name])
191 /* WE'LL GET PROXY ON TAKE REQUEST
192 def setupProxyResult = setupProxy(dni)
193 if(setupProxyResult.success)
195 log.debug "Setting up the proxy worked...taking image capture now?"
200 //Let's not take photos on add
203 log.debug "created ${d.displayName} with id $dni"
207 log.debug "found ${d.displayName} with id $dni already exists"
213 log.debug "created ${devices.size()} dropcams"
215 /* //Original Code seems to delete the dropcam that is being added */
217 // Delete any that are no longer in settings
218 def delete = getChildDevices().findAll { !cameras?.contains(it.deviceNetworkId) }
219 removeChildDevices(delete)
222 private removeChildDevices(delete)
224 log.debug "deleting ${delete.size()} dropcams"
226 state.suppressDelete[it.deviceNetworkId] = true
227 deleteChildDevice(it.deviceNetworkId)
228 state.suppressDelete.remove(it.deviceNetworkId)
231 private List getDeviceList()
234 //https://apilb.yoics.net/web/api/getdevices.ashx?token=&filter=all&whose=me&state=%20all&type=xml
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" ]
244 log.debug "cam list via: $deviceListParams"
253 httpGet(deviceListParams) { resp ->
255 log.debug "getting device list..."
257 something = resp.status
258 more = "headers: " + resp.headers.collect { "${it.name}:${it.value}" }
260 if(resp.status == 200)
262 def jsonString = resp.data.str
263 def body = new groovy.json.JsonSlurper().parseText(jsonString)
265 //log.debug "get devices list response: ${jsonString}"
266 //log.debug "get device list response: ${body}"
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
277 log.error "camera list: unknown response"
282 log.debug "list: after getting cameras: " + [devices:devices, url:singleUrl, html:multipleHtml?.size(), something:something, more:more]
288 def removeChildFromSettings(child)
290 def device = child.device
292 def dni = device.deviceNetworkId
293 log.debug "removing child device $device with dni ${dni}"
295 if(!state?.suppressDelete?.get(dni))
297 def newSettings = settings.cameras?.findAll { it != dni } ?: []
298 app.updateSetting("cameras", newSettings)
302 private forceLogin() {
310 if(getAuthHashValueIsValid())
312 return [success:true]
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
320 def address = dni?.split(/\./)?.last()
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" ]
330 def result = [success:false]
332 httpGet(loginParams) { resp ->
333 if (resp.status == 200) //&& resp.headers.'Content-Type'.contains("application/json")
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)
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
344 def proxyMap = [proxy:proxy, requested: requested, expirationsec:expirationsec, url: url]
347 //log.debug "setting ${dni} proxy to ${proxyMap}"
348 //updateDeviceProxy(address, proxyMap)
349 result.success = true
353 // ERROR: any more information we can give?
354 result.reason = "Bad login"
359 // ERROR: any more information we can give?
360 result.reason = "Bad login"
371 private doLogin(user = "", pwd = "") { //change this name
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" ]
382 loginParams.query = [key: "SmartThingsApplication", usr: user, pwd: pwd, apilevel: 12, type:"json" ]
385 def result = [success:false]
387 httpGet(loginParams) { resp ->
388 if (resp.status == 200) //&& resp.headers.'Content-Type'.contains("application/json")
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)
394 log.debug "login response: ${jsonString}"
395 log.debug "login response: ${body}"
397 def authhash = body?.NewDataSet?.Table[0]?.authhash //.token
399 //this may return as well??
400 def token = body?.NewDataSet?.Table[0]?.token ?: null
403 log.debug "login setting authhash to ${authhash}"
404 updateAuthHash(authhash)
406 log.debug "login setting login token to ${token}"
407 updateLoginToken(token)
408 result.success = true
410 result.success = doLoginToken()
415 // ERROR: any more information we can give?
416 result.reason = "Bad login"
421 // ERROR: any more information we can give?
422 result.reason = "Bad login"
431 private doLoginToken() {
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" ]
441 def result = [success:false]
443 httpGet(loginParams) { resp ->
444 if (resp.status == 200)
446 log.debug "login 200 json headers: " + resp.headers.collect { "${it.name}:${it.value}" }
448 def jsonString = resp.data.str
449 def body = new groovy.json.JsonSlurper().parseText(jsonString)
451 def token = body?.NewDataSet?.Table[0]?.token
454 log.debug "login setting login to $token"
455 updateLoginToken(token)
456 result.success = true
460 // ERROR: any more information we can give?
461 result.reason = "Bad login"
466 // ERROR: any more information we can give?
467 result.reason = "Bad login"
476 def takePicture(String dni, Integer imgWidth=null)
479 //turn on any of the selected lights that are off
480 def offLights = switches.findAll{(it.currentValue("switch") == "off")}
482 offLights.collect{it.on()}
484 log.debug "parent.takePicture(${dni}, ${imgWidth})"
486 def uuid = dni?.split(/\./)?.last()
488 log.debug "taking picture for $uuid (${dni})"
491 def loginRequired = false
495 imageBytes = doTakePicture(uuid, imgWidth)
499 log.error "Exception $e trying to take a picture, attempting to login again"
505 def loginResult = doLoginToken()
506 if(loginResult.success)
509 imageBytes = doTakePicture(uuid, imgWidth)
513 log.error "tried to login to dropcam after failing to take a picture and failed"
517 //turn previously off lights to their original state
518 offLights.collect{it.off()}
522 private doTakePicture(String uuid, Integer imgWidth)
524 imgWidth = imgWidth ?: 1280
525 def loginRequired = false
527 def proxyParams = getDeviceProxy(uuid)
528 if(!proxyParams.success)
530 throw new Exception("Login Required")
534 uri: "${proxyParams.uri}",
535 path: "${proxyParams.path}",
536 headers: ['User-Agent': validUserAgent()]
541 httpGet(takeParams) { resp ->
543 if(resp.status == 403)
547 else if (resp.status == 200 && resp.headers.'Content-Type'.contains("image/jpeg"))
549 imageBytes = resp.data
553 log.error "unknown takePicture() response: ${resp.status} - ${resp.headers.'Content-Type'}"
559 throw new Exception("Login Required")
565 /////////////////////////
566 private Boolean getLoginTokenValueIsValid()
568 return getLoginTokenValue()
571 private updateLoginToken(String token) {
572 state.loginToken = token
575 private getLoginTokenValue() {
579 private Boolean getAuthHashValueIsValid()
581 return getAuthHashValue()
584 private updateAuthHash(String hash) {
585 state.authHash = hash
588 private getAuthHashValue() {
592 private updateUserName(String username) {
593 state.username = username
596 private getUserName() {
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}"
608 private updateDeviceProxy(dni, map){
609 if (!state.proxy) { state.proxy = [:] }
610 state.proxy[dni] = map
613 private getDeviceProxy(dni) {
614 def address = dni?.split(/\./)?.last()
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" ]
624 def result = [success:false]
626 httpGet(loginParams) { resp ->
627 if (resp.status == 200) //&& resp.headers.'Content-Type'.contains("application/json")
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)
633 if (body?.NewDataSet?.Table[0]?.error)
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
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
651 // ERROR: any more information we can give?
652 result.reason = "Bad login"
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"
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">
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">
687 <form name="login" action="${buildUrl("", "authorize")}" method="post">
690 <input type="text" name="user" style="height: 50px;">
695 <input type="password" name="password" style="height: 50px;">
697 <input type="submit" value="Submit">
704 render status: 200, contentType: 'text/html', data: html
709 def loginResult = doLogin(params.user, params.password)
712 if (loginResult.success) {
713 result = "Successful"
716 updateUserName(params.user)
723 <title>$inputQuery results</title>
724 <meta name="apple-mobile-web-app-capable" content="yes" />
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">
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">
745 Yoics Login ${result}!
751 render status: 200, contentType: 'text/html', data: html