diff --git a/commands.go b/commands.go index 5b300fd..ba2f612 100644 --- a/commands.go +++ b/commands.go @@ -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 { @@ -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" @@ -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 } diff --git a/states.go b/states.go index f8cb243..85d8787 100644 --- a/states.go +++ b/states.go @@ -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"` diff --git a/vehicles.go b/vehicles.go index 30c337e..1ea40b8 100644 --- a/vehicles.go +++ b/vehicles.go @@ -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 @@ -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 } diff --git a/websocket.go b/websocket.go new file mode 100644 index 0000000..b7e00e6 --- /dev/null +++ b/websocket.go @@ -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 +}