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 * Tesla Service Manager
15 * Author: juano23@gmail.com
20 name: "Tesla (Connect)",
21 namespace: "smartthings",
22 author: "SmartThings",
23 description: "Integrate your Tesla car with SmartThings.",
24 category: "SmartThings Labs",
25 iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/tesla-app%402x.png",
26 iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tesla-app%403x.png",
31 page(name: "loginToTesla", title: "Tesla")
32 page(name: "selectCars", title: "Tesla")
36 def showUninstall = username != null && password != null
37 return dynamicPage(name: "loginToTesla", title: "Connect your Tesla", nextPage:"selectCars", uninstall:showUninstall) {
38 section("Log in to your Tesla account:") {
39 input "username", "text", title: "Username", required: true, autoCorrect:false
40 input "password", "password", title: "Password", required: true, autoCorrect:false
42 section("To use Tesla, SmartThings encrypts and securely stores your Tesla credentials.") {}
47 def loginResult = forceLogin()
49 if(loginResult.success)
51 def options = carsDiscovered() ?: []
53 return dynamicPage(name: "selectCars", title: "Tesla", install:true, uninstall:true) {
54 section("Select which Tesla to connect"){
55 input(name: "selectedCars", type: "enum", required:false, multiple:true, options:options)
61 log.error "login result false"
62 return dynamicPage(name: "selectCars", title: "Tesla", install:false, uninstall:true, nextPage:"") {
64 paragraph "Please check your username and password"
84 removeChildDevices(getChildDevices())
93 // Delete any that are no longer in settings
94 def delete = getChildDevices().findAll { !selectedCars }
96 //removeChildDevices(delete)
99 //CHILD DEVICE METHODS
101 def devices = getcarList()
102 log.trace "Adding childs $devices - $selectedCars"
103 selectedCars.each { dni ->
104 def d = getChildDevice(dni)
106 def newCar = devices.find { (it.dni) == dni }
107 d = addChildDevice("smartthings", "Tesla", dni, null, [name:"Tesla", label:"Tesla"])
108 log.trace "created ${d.name} with id $dni"
110 log.trace "found ${d.name} with id $key already exists"
115 private removeChildDevices(delete)
117 log.debug "deleting ${delete.size()} Teslas"
119 state.suppressDelete[it.deviceNetworkId] = true
120 deleteChildDevice(it.deviceNetworkId)
121 state.suppressDelete.remove(it.deviceNetworkId)
128 def carListParams = [
129 uri: "https://portal.vn.teslamotors.com/",
131 headers: [Cookie: getCookieValue(), 'User-Agent': validUserAgent()]
134 httpGet(carListParams) { resp ->
135 log.debug "Getting car list"
136 if(resp.status == 200) {
137 def vehicleId = resp.data.id.value[0].toString()
138 def vehicleVIN = resp.data.vin[0]
139 def dni = vehicleVIN + ":" + vehicleId
140 def name = "Tesla [${vehicleId}]"
141 // CHECK HERE IF MOBILE IS ENABLE
142 // path: "/vehicles/${vehicleId}/mobile_enabled",
144 devices += ["name" : "${name}", "dni" : "${dni}"]
145 // else return [errorMessage:"Mobile communication isn't enable on all of your vehicles."]
146 } else if(resp.status == 302) {
147 // Token expired or incorrect
148 singleUrl = resp.headers.Location.value
151 log.error "car list: unknown response"
157 Map carsDiscovered() {
158 def devices = getcarList()
159 log.trace "Map $devices"
161 if (devices instanceof java.util.Map) {
163 def value = "${it?.name}"
165 map["${key}"] = value
167 } else { //backwards compatable
169 def value = "${it?.name}"
171 map["${key}"] = value
177 def removeChildFromSettings(child) {
178 def device = child.device
179 def dni = device.deviceNetworkId
180 log.debug "removing child device $device with dni ${dni}"
181 if(!state?.suppressDelete?.get(dni))
183 def newSettings = settings.cars?.findAll { it != dni } ?: []
184 app.updateSetting("cars", newSettings)
188 private forceLogin() {
195 if(getCookieValueIsValid()) {
196 return [success:true]
203 uri: "https://portal.vn.teslamotors.com",
205 contentType: "application/x-www-form-urlencoded",
206 body: "user_session%5Bemail%5D=${username}&user_session%5Bpassword%5D=${password}"
209 def result = [success:false]
212 httpPost(loginParams) { resp ->
213 if (resp.status == 302) {
214 log.debug "login 302 json headers: " + resp.headers.collect { "${it.name}:${it.value}" }
215 def cookie = resp?.headers?.'Set-Cookie'?.split(";")?.getAt(0)
217 log.debug "login setting cookie to $cookie"
219 result.success = true
221 // ERROR: any more information we can give?
222 result.reason = "Bad login"
225 // ERROR: any more information we can give?
226 result.reason = "Bad login"
229 } catch (groovyx.net.http.HttpResponseException e) {
230 result.reason = "Bad login"
235 private command(String dni, String command, String value = '') {
236 def id = getVehicleId(dni)
240 commandPath = "/vehicles/${id}/command/flash_lights"
243 commandPath = "/vehicles/${id}/command/honk_horn"
246 commandPath = "/vehicles/${id}/command/door_lock"
249 commandPath = "/vehicles/${id}/command/door_unlock"
252 commandPath = "/vehicles/${id}/command/auto_conditioning_start"
255 commandPath = "/vehicles/${id}/command/auto_conditioning_stop"
258 commandPath = "/vehicles/${id}/command/sun_roof_control?state=${value}"
261 commandPath = "/vehicles/${id}/command/set_temps?driver_temp=${value}&passenger_temp=${value}"
267 def commandParams = [
268 uri: "https://portal.vn.teslamotors.com",
270 headers: [Cookie: getCookieValue(), 'User-Agent': validUserAgent()]
273 def loginRequired = false
275 httpGet(commandParams) { resp ->
277 if(resp.status == 403) {
279 } else if (resp.status == 200) {
281 sendNotification(data.toString())
283 log.error "unknown response: ${resp.status} - ${resp.headers.'Content-Type'}"
286 if(loginRequired) { throw new Exception("Login Required") }
289 private honk(String dni) {
290 def id = getVehicleId(dni)
292 uri: "https://portal.vn.teslamotors.com",
293 path: "/vehicles/${id}/command/honk_horn",
294 headers: [Cookie: getCookieValue(), 'User-Agent': validUserAgent()]
297 def loginRequired = false
299 httpGet(honkParams) { resp ->
301 if(resp.status == 403) {
303 } else if (resp.status == 200) {
306 log.error "unknown response: ${resp.status} - ${resp.headers.'Content-Type'}"
311 throw new Exception("Login Required")
315 private poll(String dni) {
316 def id = getVehicleId(dni)
318 uri: "https://portal.vn.teslamotors.com",
319 path: "/vehicles/${id}/command/climate_state",
320 headers: [Cookie: getCookieValue(), 'User-Agent': validUserAgent()]
323 def childDevice = getChildDevice(dni)
325 def loginRequired = false
327 httpGet(pollParams1) { resp ->
329 if(resp.status == 403) {
331 } else if (resp.status == 200) {
333 childDevice?.sendEvent(name: 'temperature', value: cToF(data.inside_temp).toString())
334 if (data.is_auto_conditioning_on)
335 childDevice?.sendEvent(name: 'clima', value: 'on')
337 childDevice?.sendEvent(name: 'clima', value: 'off')
338 childDevice?.sendEvent(name: 'thermostatSetpoint', value: cToF(data.driver_temp_setting).toString())
340 log.error "unknown response: ${resp.status} - ${resp.headers.'Content-Type'}"
345 uri: "https://portal.vn.teslamotors.com",
346 path: "/vehicles/${id}/command/vehicle_state",
347 headers: [Cookie: getCookieValue(), 'User-Agent': validUserAgent()]
350 httpGet(pollParams2) { resp ->
351 if(resp.status == 403) {
353 } else if (resp.status == 200) {
355 if (data.sun_roof_percent_open == 0)
356 childDevice?.sendEvent(name: 'roof', value: 'close')
357 else if (data.sun_roof_percent_open > 0 && data.sun_roof_percent_open < 70)
358 childDevice?.sendEvent(name: 'roof', value: 'vent')
359 else if (data.sun_roof_percent_open >= 70 && data.sun_roof_percent_open <= 80)
360 childDevice?.sendEvent(name: 'roof', value: 'comfort')
361 else if (data.sun_roof_percent_open > 80 && data.sun_roof_percent_open <= 100)
362 childDevice?.sendEvent(name: 'roof', value: 'open')
364 childDevice?.sendEvent(name: 'door', value: 'lock')
366 childDevice?.sendEvent(name: 'door', value: 'unlock')
368 log.error "unknown response: ${resp.status} - ${resp.headers.'Content-Type'}"
373 uri: "https://portal.vn.teslamotors.com",
374 path: "/vehicles/${id}/command/charge_state",
375 headers: [Cookie: getCookieValue(), 'User-Agent': validUserAgent()]
378 httpGet(pollParams3) { resp ->
379 if(resp.status == 403) {
381 } else if (resp.status == 200) {
383 childDevice?.sendEvent(name: 'connected', value: data.charging_state.toString())
384 childDevice?.sendEvent(name: 'miles', value: data.battery_range.toString())
385 childDevice?.sendEvent(name: 'battery', value: data.battery_level.toString())
387 log.error "unknown response: ${resp.status} - ${resp.headers.'Content-Type'}"
392 throw new Exception("Login Required")
396 private getVehicleId(String dni) {
397 return dni.split(":").last()
400 private Boolean getCookieValueIsValid()
402 // TODO: make a call with the cookie to verify that it works
403 return getCookieValue()
406 private updateCookie(String cookie) {
407 state.cookie = cookie
410 private getCookieValue() {
415 return temp * 1.8 + 32
418 private validUserAgent() {
419 "curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8x zlib/1.2.5"