mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-08 05:31:56 +00:00
420 lines
12 KiB
Groovy
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"
|
|
} |