--- /dev/null
+/**
+ * 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