Files

420 lines
12 KiB
Groovy

/**
* 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"
}