Checking in all the SmartThings apps; both official and third-party.
[smartapps.git] / official / tesla-connect.groovy
diff --git a/official/tesla-connect.groovy b/official/tesla-connect.groovy
new file mode 100755 (executable)
index 0000000..5022c42
--- /dev/null
@@ -0,0 +1,420 @@
+/**
+ *  Copyright 2015 SmartThings
+ *
+ *  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.
+ *
+ *     Tesla Service Manager
+ *
+ *     Author: juano23@gmail.com
+ *     Date: 2013-08-15
+ */
+
+definition(
+    name: "Tesla (Connect)",
+    namespace: "smartthings",
+    author: "SmartThings",
+    description: "Integrate your Tesla car with SmartThings.",
+    category: "SmartThings Labs",
+    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/tesla-app%402x.png",
+    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tesla-app%403x.png",
+    singleInstance: true
+)
+
+preferences {
+       page(name: "loginToTesla", title: "Tesla")
+       page(name: "selectCars", title: "Tesla")
+}
+
+def loginToTesla() {
+       def showUninstall = username != null && password != null
+       return dynamicPage(name: "loginToTesla", title: "Connect your Tesla", nextPage:"selectCars", uninstall:showUninstall) {
+               section("Log in to your Tesla account:") {
+                       input "username", "text", title: "Username", required: true, autoCorrect:false
+                       input "password", "password", title: "Password", required: true, autoCorrect:false
+               }
+               section("To use Tesla, SmartThings encrypts and securely stores your Tesla credentials.") {}
+       }
+}
+
+def selectCars() {
+       def loginResult = forceLogin()
+
+       if(loginResult.success)
+       {
+               def options = carsDiscovered() ?: []
+
+               return dynamicPage(name: "selectCars", title: "Tesla", install:true, uninstall:true) {
+                       section("Select which Tesla to connect"){
+                               input(name: "selectedCars", type: "enum", required:false, multiple:true, options:options)
+                       }
+               }
+       }
+       else
+       {
+               log.error "login result false"
+        return dynamicPage(name: "selectCars", title: "Tesla", install:false, uninstall:true, nextPage:"") {
+                       section("") {
+                               paragraph "Please check your username and password"
+                       }
+               }
+       }
+}
+
+
+def installed() {
+       log.debug "Installed"
+       initialize()
+}
+
+def updated() {
+       log.debug "Updated"
+
+       unsubscribe()
+       initialize()
+}
+
+def uninstalled() {
+       removeChildDevices(getChildDevices())
+}
+
+def initialize() {
+
+       if (selectCars) {
+               addDevice()
+       }
+
+       // Delete any that are no longer in settings
+       def delete = getChildDevices().findAll { !selectedCars }
+       log.info delete
+    //removeChildDevices(delete)
+}
+
+//CHILD DEVICE METHODS
+def addDevice() {
+    def devices = getcarList()
+    log.trace "Adding childs $devices - $selectedCars"
+       selectedCars.each { dni ->
+               def d = getChildDevice(dni)
+               if(!d) {
+                       def newCar = devices.find { (it.dni) == dni }
+                       d = addChildDevice("smartthings", "Tesla", dni, null, [name:"Tesla", label:"Tesla"])
+                       log.trace "created ${d.name} with id $dni"
+               } else {
+                       log.trace "found ${d.name} with id $key already exists"
+               }
+       }
+}
+
+private removeChildDevices(delete)
+{
+       log.debug "deleting ${delete.size()} Teslas"
+       delete.each {
+               state.suppressDelete[it.deviceNetworkId] = true
+               deleteChildDevice(it.deviceNetworkId)
+               state.suppressDelete.remove(it.deviceNetworkId)
+       }
+}
+
+def getcarList() {
+       def devices = []
+
+       def carListParams = [
+               uri: "https://portal.vn.teslamotors.com/",
+        path: "/vehicles",
+               headers: [Cookie: getCookieValue(), 'User-Agent': validUserAgent()]
+       ]
+    
+       httpGet(carListParams) { resp ->
+               log.debug "Getting car list"
+               if(resp.status == 200) {
+            def vehicleId = resp.data.id.value[0].toString()
+            def vehicleVIN = resp.data.vin[0]
+            def dni = vehicleVIN + ":" + vehicleId
+                       def name = "Tesla [${vehicleId}]"
+            // CHECK HERE IF MOBILE IS ENABLE
+            // path: "/vehicles/${vehicleId}/mobile_enabled",
+            // if (enable)
+            devices += ["name" : "${name}", "dni" : "${dni}"]
+            // else return [errorMessage:"Mobile communication isn't enable on all of your vehicles."]
+               } else if(resp.status == 302) {
+               // Token expired or incorrect
+                       singleUrl = resp.headers.Location.value
+               } else {
+                       // ERROR
+                       log.error "car list: unknown response"
+               }        
+       }
+    return devices
+}
+
+Map carsDiscovered() {
+       def devices =  getcarList()
+    log.trace "Map $devices"    
+       def map = [:]
+       if (devices instanceof java.util.Map) {
+               devices.each {
+                       def value = "${it?.name}"
+                       def key = it?.dni
+                       map["${key}"] = value
+               }
+       } else { //backwards compatable
+               devices.each {
+                       def value = "${it?.name}"
+                       def key = it?.dni
+                       map["${key}"] = value
+               }
+       }
+       map
+}
+
+def removeChildFromSettings(child) {
+       def device = child.device
+       def dni = device.deviceNetworkId
+       log.debug "removing child device $device with dni ${dni}"
+       if(!state?.suppressDelete?.get(dni))
+       {
+               def newSettings = settings.cars?.findAll { it != dni } ?: []
+               app.updateSetting("cars", newSettings)
+       }
+}
+
+private forceLogin() {
+       updateCookie(null)
+       login()
+}
+
+
+private login() {
+       if(getCookieValueIsValid()) {
+               return [success:true]
+       }
+       return doLogin()
+}
+
+private doLogin() {
+       def loginParams = [
+               uri: "https://portal.vn.teslamotors.com",
+        path: "/login",
+               contentType: "application/x-www-form-urlencoded",
+               body: "user_session%5Bemail%5D=${username}&user_session%5Bpassword%5D=${password}"
+       ]
+
+       def result = [success:false]
+       
+    try {
+       httpPost(loginParams) { resp ->
+            if (resp.status == 302) {
+                log.debug "login 302 json headers: " + resp.headers.collect { "${it.name}:${it.value}" }
+                def cookie = resp?.headers?.'Set-Cookie'?.split(";")?.getAt(0)
+                if (cookie) {
+                    log.debug "login setting cookie to $cookie"
+                    updateCookie(cookie)                
+                    result.success = true
+                } else {
+                    // ERROR: any more information we can give?
+                    result.reason = "Bad login"
+                }
+            } else {
+                // ERROR: any more information we can give?
+                result.reason = "Bad login"
+            }
+       }        
+       } catch (groovyx.net.http.HttpResponseException e) {
+                       result.reason = "Bad login"
+       }
+       return result
+}
+
+private command(String dni, String command, String value = '') {
+       def id = getVehicleId(dni)
+    def commandPath
+       switch (command) {
+               case "flash":
+               commandPath = "/vehicles/${id}/command/flash_lights"
+            break;
+               case "honk":
+               commandPath = "/vehicles/${id}/command/honk_horn"  
+            break;
+               case "doorlock":
+               commandPath = "/vehicles/${id}/command/door_lock"  
+            break;            
+               case "doorunlock":
+               commandPath = "/vehicles/${id}/command/door_unlock"  
+            break;  
+               case "climaon":
+               commandPath = "/vehicles/${id}/command/auto_conditioning_start"  
+            break;            
+               case "climaoff":
+               commandPath = "/vehicles/${id}/command/auto_conditioning_stop"  
+            break;             
+               case "roof":
+               commandPath = "/vehicles/${id}/command/sun_roof_control?state=${value}" 
+            break;    
+               case "temp":
+               commandPath = "/vehicles/${id}/command/set_temps?driver_temp=${value}&passenger_temp=${value}"
+            break;              
+               default:
+                       break; 
+    }   
+    
+       def commandParams = [
+               uri: "https://portal.vn.teslamotors.com",
+               path: commandPath,
+               headers: [Cookie: getCookieValue(), 'User-Agent': validUserAgent()]
+       ]
+
+       def loginRequired = false
+
+       httpGet(commandParams) { resp ->
+
+               if(resp.status == 403) {
+                       loginRequired = true
+               } else if (resp.status == 200) {
+                       def data = resp.data
+            sendNotification(data.toString())
+               } else {
+                       log.error "unknown response: ${resp.status} - ${resp.headers.'Content-Type'}"
+               }
+       }
+       if(loginRequired) { throw new Exception("Login Required") }
+}
+
+private honk(String dni) {
+       def id = getVehicleId(dni)
+       def honkParams = [
+               uri: "https://portal.vn.teslamotors.com",
+               path: "/vehicles/${id}/command/honk_horn",
+               headers: [Cookie: getCookieValue(), 'User-Agent': validUserAgent()]
+       ]
+
+       def loginRequired = false
+
+       httpGet(honkParams) { resp ->
+
+               if(resp.status == 403) {
+                       loginRequired = true
+               } else if (resp.status == 200) {
+                       def data = resp.data
+               } else {
+                       log.error "unknown response: ${resp.status} - ${resp.headers.'Content-Type'}"
+               }
+       }
+
+       if(loginRequired) {
+               throw new Exception("Login Required")
+       }
+}
+
+private poll(String dni) {
+       def id = getVehicleId(dni)
+       def pollParams1 = [
+               uri: "https://portal.vn.teslamotors.com",
+               path: "/vehicles/${id}/command/climate_state",
+               headers: [Cookie: getCookieValue(), 'User-Agent': validUserAgent()]
+       ]
+
+       def childDevice = getChildDevice(dni)
+    
+       def loginRequired = false
+
+       httpGet(pollParams1) { resp ->
+
+               if(resp.status == 403) {
+                       loginRequired = true
+               } else if (resp.status == 200) {
+                       def data = resp.data
+            childDevice?.sendEvent(name: 'temperature', value: cToF(data.inside_temp).toString())
+            if (data.is_auto_conditioning_on)            
+               childDevice?.sendEvent(name: 'clima', value: 'on')
+            else
+                childDevice?.sendEvent(name: 'clima', value: 'off')
+            childDevice?.sendEvent(name: 'thermostatSetpoint', value: cToF(data.driver_temp_setting).toString())            
+               } else {
+                       log.error "unknown response: ${resp.status} - ${resp.headers.'Content-Type'}"
+               }
+       }
+
+       def pollParams2 = [
+               uri: "https://portal.vn.teslamotors.com",
+               path: "/vehicles/${id}/command/vehicle_state",
+               headers: [Cookie: getCookieValue(), 'User-Agent': validUserAgent()]
+       ]
+
+       httpGet(pollParams2) { resp ->
+               if(resp.status == 403) {
+                       loginRequired = true
+               } else if (resp.status == 200) {
+                       def data = resp.data
+            if (data.sun_roof_percent_open == 0)
+               childDevice?.sendEvent(name: 'roof', value: 'close')
+                       else if (data.sun_roof_percent_open > 0 && data.sun_roof_percent_open < 70)
+               childDevice?.sendEvent(name: 'roof', value: 'vent')
+                       else if (data.sun_roof_percent_open >= 70 && data.sun_roof_percent_open <= 80)                
+               childDevice?.sendEvent(name: 'roof', value: 'comfort')
+            else if (data.sun_roof_percent_open > 80 && data.sun_roof_percent_open <= 100)
+               childDevice?.sendEvent(name: 'roof', value: 'open')           
+            if (data.locked)            
+               childDevice?.sendEvent(name: 'door', value: 'lock')
+            else
+                childDevice?.sendEvent(name: 'door', value: 'unlock')
+               } else {
+                       log.error "unknown response: ${resp.status} - ${resp.headers.'Content-Type'}"
+               }
+       }
+
+       def pollParams3 = [
+               uri: "https://portal.vn.teslamotors.com",
+               path: "/vehicles/${id}/command/charge_state",
+               headers: [Cookie: getCookieValue(), 'User-Agent': validUserAgent()]
+       ]
+
+       httpGet(pollParams3) { resp ->
+               if(resp.status == 403) {
+                       loginRequired = true
+               } else if (resp.status == 200) {
+                       def data = resp.data
+            childDevice?.sendEvent(name: 'connected', value: data.charging_state.toString())
+            childDevice?.sendEvent(name: 'miles', value: data.battery_range.toString())
+            childDevice?.sendEvent(name: 'battery', value: data.battery_level.toString())
+               } else {
+                       log.error "unknown response: ${resp.status} - ${resp.headers.'Content-Type'}"
+               }
+       }
+
+       if(loginRequired) {
+               throw new Exception("Login Required")
+       }
+}
+
+private getVehicleId(String dni) {
+    return dni.split(":").last()
+}
+
+private Boolean getCookieValueIsValid()
+{
+       // TODO: make a call with the cookie to verify that it works
+       return getCookieValue()
+}
+
+private updateCookie(String cookie) {
+       state.cookie = cookie
+}
+
+private getCookieValue() {
+       state.cookie
+}
+
+def cToF(temp) {
+    return temp * 1.8 + 32
+}
+
+private validUserAgent() {
+       "curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8x zlib/1.2.5"
+}
\ No newline at end of file