Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added simple homelink support #18

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 10 additions & 62 deletions commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,46 +14,6 @@ type CommandResponse struct {
} `json:"response"`
}

// Required elements to POST an Autopark/Summon request
// for the vehicle
type AutoParkRequest struct {
VehicleID int `json:"vehicle_id,omitempty"`
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
Action string `json:"action,omitempty"`
}

// Causes the vehicle to abort the Autopark request
func (v Vehicle) AutoparkAbort() error {
return v.autoPark("abort")
}

// Causes the vehicle to pull forward
func (v Vehicle) AutoparkForward() error {
return v.autoPark("start_forward")
}

// Causes the vehicle to go in reverse
func (v Vehicle) AutoparkReverse() error {
return v.autoPark("start_reverse")
}

// Performs the actual auto park/summon request for the vehicle
func (v Vehicle) autoPark(action string) error {
apiUrl := BaseURL + "/vehicles/" + strconv.FormatInt(v.ID, 10) + "/command/autopark_request"
driveState, _ := v.DriveState()
autoParkRequest := &AutoParkRequest{
VehicleID: v.VehicleID,
Lat: driveState.Latitude,
Lon: driveState.Longitude,
Action: action,
}
body, _ := json.Marshal(autoParkRequest)

_, err := sendCommand(apiUrl, body)
return err
}

// TBD based on Github issue #7
// Toggles defrost on and off, locations values are 'front' or 'rear'
// func (v Vehicle) Defrost(location string, state bool) error {
Expand All @@ -69,23 +29,6 @@ func (v Vehicle) autoPark(action string) error {
// return err
// }

// Opens and closes the configured Homelink garage door of the vehicle
// keep in mind this is a toggle and the garage door state is unknown
// a major limitation of Homelink
func (v Vehicle) TriggerHomelink() error {
apiUrl := BaseURL + "/vehicles/" + strconv.FormatInt(v.ID, 10) + "/command/trigger_homelink"
driveState, _ := v.DriveState()
autoParkRequest := &AutoParkRequest{
Lat: driveState.Latitude,
Lon: driveState.Longitude,
}
body, _ := json.Marshal(autoParkRequest)

_, err := sendCommand(apiUrl, body)
return err
return nil
}

// Wakes up the vehicle when it is powered off
func (v Vehicle) Wakeup() (*Vehicle, error) {
apiUrl := BaseURL + "/vehicles/" + strconv.FormatInt(v.ID, 10) + "/wake_up"
Expand Down Expand Up @@ -182,11 +125,16 @@ func (v Vehicle) LockDoors() error {

// Sets the temprature of the vehicle, where you may set the driver
// zone and the passenger zone to seperate temperatures
func (v Vehicle) SetTemprature(driver float64, passenger float64) error {
driveTemp := strconv.FormatFloat(driver, 'f', -1, 32)
passengerTemp := strconv.FormatFloat(passenger, 'f', -1, 32)
apiUrl := BaseURL + "/vehicles/" + strconv.FormatInt(v.ID, 10) + "/command/set_temps?driver_temp=" + driveTemp + "&passenger_temp=" + passengerTemp
_, err := ActiveClient.post(apiUrl, nil)
func (v Vehicle) SetTemperature(driver float64, passenger float64) error {
apiUrl := BaseURL + "/vehicles/" + strconv.FormatInt(v.ID, 10) + "/command/set_temps"
body, err := json.Marshal(map[string]interface{}{
"driver_temp": driver,
"passenger_temp": passenger,
})
if err != nil {
return err
}
_, err = ActiveClient.post(apiUrl, body)
return err
}

Expand Down
2 changes: 1 addition & 1 deletion states.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ type ClimateState struct {
DriverTempSetting float64 `json:"driver_temp_setting"`
PassengerTempSetting float64 `json:"passenger_temp_setting"`
IsAutoConditioningOn bool `json:"is_auto_conditioning_on"`
IsFrontDefrosterOn bool `json:"is_front_defroster_on"`
IsFrontDefrosterOn interface{} `json:"is_front_defroster_on"`
IsRearDefrosterOn bool `json:"is_rear_defroster_on"`
FanStatus interface{} `json:"fan_status"`
SeatHeaterLeft int `json:"seat_heater_left"`
Expand Down
5 changes: 5 additions & 0 deletions vehicles.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ type Vehicle struct {
NotificationsEnabled bool `json:"notifications_enabled"`
BackseatToken interface{} `json:"backseat_token"`
BackseatTokenUpdatedAt interface{} `json:"backseat_token_updated_at"`

client *Client
}

// The response that contains the vehicle details from the Tesla API
Expand Down Expand Up @@ -49,5 +51,8 @@ func (c *Client) Vehicles() (Vehicles, error) {
if err != nil {
return nil, err
}
for i := range vehiclesResponse.Response {
vehiclesResponse.Response[i].client = c
}
return vehiclesResponse.Response, nil
}
184 changes: 184 additions & 0 deletions websocket.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package tesla

import (
"encoding/base64"
"log"
"net/http"
"net/url"
"strconv"
"time"

"github.com/gorilla/websocket"
)

var (
// WebSocketServer is the host to connect to for the Tesla websocket stream.
WebSocketServer = "streaming.vn.teslamotors.com"
// WebSocketResource is the HTTP resource to connect to.
WebSocketResource = "/connect/"
)

type autoparkCommand struct {
MsgType string `json:"msg_type"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
}

type heartbeatCommand struct {
MsgType string `json:"msg_type"`
Timestamp int64 `json:"timestamp"`
}

// WebSocketStateListener receives updates from the websocket.
type WebSocketStateListener interface {
ConnectionUp(bool)
AutoparkReady(bool)
HomelinkNearby(bool)
}

// WebSocket encapsulates a controlling websocket to a vehicle.
type WebSocket struct {
Vehicle *Vehicle
Output <-chan map[string]interface{}

conn *websocket.Conn
listener WebSocketStateListener
}

// Close closes the underlying connection.
func (s *WebSocket) Close() error {
s.listener.ConnectionUp(false)
s.listener.AutoparkReady(false)
s.listener.HomelinkNearby(false)
return s.conn.Close()
}

func (s *WebSocket) Write(i interface{}) error {
log.Printf("Tesla: WriteJSON(%+v)", i)
return s.conn.WriteJSON(i)
}

// AutoparkReverse triggers autopark reverse via this connection.
func (s *WebSocket) AutoparkReverse() error {
driveState, err := s.Vehicle.DriveState()
if err != nil {
return err
}

cmd := autoparkCommand{
MsgType: "autopark:cmd_reverse",
Latitude: driveState.Latitude,
Longitude: driveState.Longitude,
}

return s.Write(cmd)
}

// AutoparkAbort aborts autopark via this connection.
func (s *WebSocket) AutoparkAbort() {
s.Write(map[string]interface{}{
"msg_type": "autopark:cmd_abort",
})
}

// AutoparkForward triggers autopark forward via this connection.
func (s *WebSocket) AutoparkForward() error {
driveState, err := s.Vehicle.DriveState()
if err != nil {
return err
}

cmd := autoparkCommand{
MsgType: "autopark:cmd_forward",
Latitude: driveState.Latitude,
Longitude: driveState.Longitude,
}

return s.Write(cmd)
}

// ActivateHomelink triggers homelink via this connection.
func (s *WebSocket) ActivateHomelink() error {
driveState, err := s.Vehicle.DriveState()
if err != nil {
return err
}

cmd := autoparkCommand{
MsgType: "homelink:cmd_trigger",
Latitude: driveState.Latitude,
Longitude: driveState.Longitude,
}

return s.Write(cmd)
}

// Returns a WebSocket connected to the vehicle.
func (v *Vehicle) WebSocket(listener WebSocketStateListener) (*WebSocket, error) {
sockURL := url.URL{Scheme: "wss", Host: WebSocketServer, Path: WebSocketResource + strconv.Itoa(v.VehicleID)}

data := []byte(v.client.Auth.Email + ":" + v.Tokens[0])
encodedToken := base64.StdEncoding.EncodeToString(data)
headers := http.Header{}
headers.Add("Authorization", "Basic "+encodedToken)

pipe := make(chan map[string]interface{})
sock := &WebSocket{
Vehicle: v,
Output: (<-chan map[string]interface{})(pipe),
listener: listener,
}

var err error
sock.conn, _, err = websocket.DefaultDialer.Dial(sockURL.String(), headers)
if err != nil {
return nil, err
}
sock.listener.ConnectionUp(true)

go func() {
for {
msg := map[string]interface{}{}
err := sock.conn.ReadJSON(&msg)
log.Printf("Tesla: ReadJSON: %+v, %v", msg, err)
if err != nil {
sock.listener.ConnectionUp(false)
sock.listener.AutoparkReady(false)
sock.listener.HomelinkNearby(false)
close(pipe)
return
}
switch msg["msg_type"] {
case "control:hello":
freq := msg["autopark"].(map[string]interface{})["heartbeat_frequency"].(float64)
go func() {
for _ = range time.Tick(time.Millisecond * time.Duration(freq)) {
if err = sock.Write(heartbeatCommand{
MsgType: "autopark:heartbeat_app",
Timestamp: time.Now().UnixNano() / int64(time.Second),
}); err != nil {
select {
case _, ok := <-pipe:
if ok {
close(pipe)
}
default:
close(pipe)
}
return
}
}
}()
case "autopark:status":
sock.listener.AutoparkReady(msg["autopark_state"] == "ready")

case "homelink:status":
sock.listener.HomelinkNearby(msg["homelink_nearby"].(bool))

}
pipe <- msg
}
}()

return sock, nil
}