Skip to content

Commit

Permalink
Implement the new Tesla FleetAPI
Browse files Browse the repository at this point in the history
Refactored the code a bit
  • Loading branch information
jonasman committed Oct 29, 2023
1 parent 4845ec3 commit b8ee547
Show file tree
Hide file tree
Showing 10 changed files with 292 additions and 89 deletions.
45 changes: 34 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# TeslaSwift
Swift library to access Tesla API based on [Tesla JSON API (Unofficial)](https://tesla-api.timdorr.com)
Swift library to access Tesla API based on [Tesla JSON API (Unofficial)](https://tesla-api.timdorr.com) and [Tesla Fleet API](https://developer.tesla.com/docs/fleet-api)

[![Swift](https://img.shields.io/badge/Swift-5.7-orange.svg?style=flat)](https://swift.org)
[![Swift](https://img.shields.io/badge/Swift-5.9-orange.svg?style=flat)](https://swift.org)
[![Build Status](https://travis-ci.org/jonasman/TeslaSwift.svg?branch=master)](https://travis-ci.org/jonasman/TeslaSwift)
[![TeslaSwift](https://img.shields.io/cocoapods/v/TeslaSwift.svg)](https://github.com/jonasman/TeslaSwift)

## Installation

Expand All @@ -12,12 +11,34 @@ Swift library to access Tesla API based on [Tesla JSON API (Unofficial)](https:/
You can use [Swift Package Manager](https://swift.org/package-manager/) and specify a dependency in `Package.swift` by adding this or adding the dependency to Xcode:

```swift
.Package(url: "https://github.com/jonasman/TeslaSwift.git", majorVersion: 8)
.Package(url: "https://github.com/jonasman/TeslaSwift.git", majorVersion: 9)
```

There are also extensions for Combine `TeslaSwiftCombine`
The Streaming extensions are: `TeslaSwiftStreaming`, Combine `TeslaSwiftStreamingCombine`

## Tesla API
There are 2 Tesla APIs available:
1. The old ownersAPI
2. The new FleetAPI

You can choose any of them. If you want to use FleetAPI, initialize the library with a region, clientId, clientSectret and redirectURI

## App registration for the FleetAPI
To use the new FleetAPI, you will need to register your app.

Follow the steps on the [official documentation](https://developer.tesla.com/docs/fleet-api#setup):
1. Create a private/public key and upload the public key to a website
2. Make a new app at [Tesla Developer](https://developer.tesla.com/dashboard)
3. Get a partner token (using this Library)
4. Register your app (using this Library)

This library helps you get a partner token and register your app with 2 APIs:
```swift
getPartnerToken(code: String)
registerApp(domain: String)
```

## Usage

Tesla's server is not compatible with ATS so you need to add the following to your app Info.plist
Expand All @@ -42,14 +63,14 @@ Add the extension modules if needed (with the previous line)
import TeslaSwiftCombine
```


Perform an authentication with your MyTesla credentials using the web oAuth2 flow with MFA support:

```swift
let api = TeslaSwift()
let (webloginViewController, result) = await api.authenticate()
guard let safeWebLoginViewController = webloginViewController else { return }
present(safeWebLoginViewController, animated: true, completion: nil)
let teslaAPI = ...
let api = TeslaSwift(teslaAPI: teslaAPI)
let (webloginViewController, result) = api.authenticateWeb()
guard let webloginViewController else { return }
present(webloginViewController, animated: true, completion: nil)
Task { @MainActor in
do {
_ = try await result()
Expand All @@ -67,7 +88,8 @@ import TeslaSwift
import SwiftUI

struct TeslaWebLogin: UIViewControllerRepresentable {
let api = TeslaSwift()
let teslaAPI = ...
let api = TeslaSwift(teslaAPI: teslaAPI)

func makeUIViewController(context: Context) -> TeslaWebLoginViewController {
let (webloginViewController, result) = api.authenticateWeb()
Expand Down Expand Up @@ -115,7 +137,8 @@ After authentication, store the AuthToken in a safe place.
The next time the app starts-up you can reuse the token:

```swift
let api = TeslaSwift()
let teslaAPI = ...
let api = TeslaSwift(teslaAPI: teslaAPI)
api.reuse(token: previousToken)

```
Expand Down
91 changes: 68 additions & 23 deletions Sources/TeslaSwift/Model/Authentication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@
import Foundation
import CryptoKit

private let oAuthClientID: String = "81527cff06843c8634fdc09e8ac0abefb46ac849f38fe1e431c2ef21067963841234334232123232323232"
private let oAuthWebClientID: String = "ownerapi"
private let oAuthClientSecret: String = "c7257eb71a564034f9419ee651c7d0e5f7aa6bfbd18bafb5c5c033b093bb2fa3"
private let oAuthScope: String = "openid email offline_access"
private let oAuthRedirectURI: String = "https://auth.tesla.com/void/callback"
private let oAuthCodeVerifier: String = "81527cff06843c8634fdc09e8ac0abefb46ac849f38fe1e431c2ef21067963841234334232123232323232"

open class AuthToken: Codable {

Expand Down Expand Up @@ -63,33 +59,41 @@ class AuthTokenRequestWeb: Encodable {
enum GrantType: String, Encodable {
case refreshToken = "refresh_token"
case authorizationCode = "authorization_code"
case clientCredentials = "client_credentials"
}

var grantType: GrantType
var clientID: String = oAuthWebClientID
var clientSecret: String = oAuthClientSecret
var clientID: String
var clientSecret: String

var codeVerifier: String?
var code: String?
var redirectURI: String?

var refreshToken: String?
var scope: String?

init(grantType: GrantType = .authorizationCode, code: String? = nil, refreshToken: String? = nil) {
if grantType == .authorizationCode {
self.codeVerifier = oAuthClientID
self.code = code
self.redirectURI = oAuthRedirectURI
} else if grantType == .refreshToken {
self.refreshToken = refreshToken
self.scope = oAuthScope
var audience: String?

init(teslaAPI: TeslaAPI, grantType: GrantType = .authorizationCode, code: String? = nil, refreshToken: String? = nil) {
switch grantType {
case .authorizationCode:
self.codeVerifier = oAuthCodeVerifier
self.redirectURI = teslaAPI.redirectURI
self.audience = teslaAPI.region?.rawValue
self.code = code
case .refreshToken:
self.refreshToken = refreshToken
self.scope = teslaAPI.scope
case .clientCredentials:
self.scope = teslaAPI.scope
self.audience = teslaAPI.region?.rawValue
}
self.clientID = teslaAPI.clientID
self.clientSecret = teslaAPI.clientSecret
self.grantType = grantType
}

// MARK: Codable protocol

enum CodingKeys: String, CodingKey {
typealias RawValue = String

Expand All @@ -107,16 +111,20 @@ class AuthTokenRequestWeb: Encodable {
class AuthCodeRequest: Encodable {

var responseType: String = "code"
var clientID = oAuthWebClientID
var clientSecret = oAuthClientSecret
var redirectURI = oAuthRedirectURI
var scope = oAuthScope
var clientID: String
var clientSecret: String
var redirectURI: String
var scope: String
let codeChallenge: String
var codeChallengeMethod = "S256"
var state = "teslaSwift"

init() {
self.codeChallenge = oAuthClientID.challenge
init(teslaAPI: TeslaAPI) {
self.clientID = teslaAPI.clientID
self.clientSecret = teslaAPI.clientSecret
self.redirectURI = teslaAPI.redirectURI
self.scope = teslaAPI.scope
self.codeChallenge = oAuthCodeVerifier.challenge
}

// MARK: Codable protocol
Expand Down Expand Up @@ -162,3 +170,40 @@ extension String {
return Data(hashed)
}
}

extension TeslaAPI {
var region: Region? {
switch self {
case .ownerAPI: return nil
case let .fleetAPI(region: region, clientID: _, clientSecret: _, redirectURI: _): return region
}
}

var clientID: String {
switch self {
case .ownerAPI: return "ownerapi"
case let .fleetAPI(region: _, clientID: clientID, clientSecret: _, redirectURI: _): return clientID
}
}

var clientSecret: String {
switch self {
case .ownerAPI: return "c7257eb71a564034f9419ee651c7d0e5f7aa6bfbd18bafb5c5c033b093bb2fa3"
case let .fleetAPI(region: _, clientID: _, clientSecret: clientSecret, redirectURI: _): return clientSecret
}
}

var redirectURI: String {
switch self {
case .ownerAPI: return "https://auth.tesla.com/void/callback"
case let .fleetAPI(region: _, clientID: _, clientSecret: _, redirectURI: redirectURI): return redirectURI
}
}

var scope: String {
switch self {
case .ownerAPI: return "openid email offline_access"
case .fleetAPI: return "openid user_data vehicle_device_data offline_access vehicle_cmds vehicle_charging_cmds energy_device_data energy_cmds"
}
}
}
41 changes: 41 additions & 0 deletions Sources/TeslaSwift/Model/Partner.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// Partner.swift
// TeslaSwiftDemoTests
//
// Created by João Nunes on 29/10/2023.
// Copyright © 2023 Joao Nunes. All rights reserved.
//

import Foundation

public struct PartnerBody: Codable {
let domain: String
}

public struct PartnerResponse: Codable {
let response: PartnerResponseBody
}

public struct PartnerResponseBody: Codable {
let domain: String
let name: String
let description: String
let clientId: String
let ca: String?
let createdAt: Date
let updatedAt: Date
let enterpriseTier: String
let publicKey: String

enum CodingKeys: String, CodingKey {
case domain
case name
case description
case clientId = "client_id"
case ca
case createdAt = "created_at"
case updatedAt = "updated_at"
case enterpriseTier = "enterprise_tier"
case publicKey = "public_key"
}
}
9 changes: 6 additions & 3 deletions Sources/TeslaSwift/TeslaEndpoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ enum Endpoint {
case oAuth2TokenCN
case oAuth2revoke(token: String)
case oAuth2revokeCN(token: String)
case partnerAccounts
case vehicles
case vehicleSummary(vehicleID: String)
case mobileAccess(vehicleID: String)
Expand Down Expand Up @@ -52,6 +53,8 @@ extension Endpoint {
return "/oauth2/v3/token"
case .oAuth2revoke, .oAuth2revokeCN:
return "/oauth2/v3/revoke"
case .partnerAccounts:
return "/api/1/partner_accounts"
// Vehicle Data and Commands
case .vehicles:
return "/api/1/vehicles"
Expand Down Expand Up @@ -102,7 +105,7 @@ extension Endpoint {

var method: String {
switch self {
case .revoke, .oAuth2Token, .oAuth2TokenCN, .wakeUp, .command:
case .revoke, .oAuth2Token, .oAuth2TokenCN, .wakeUp, .partnerAccounts, .command:
return "POST"
case .vehicles, .vehicleSummary, .mobileAccess, .allStates, .chargeState, .climateState, .driveState, .guiSettings, .vehicleState, .vehicleConfig, .nearbyChargingSites, .oAuth2Authorization, .oAuth2revoke, .oAuth2AuthorizationCN, .oAuth2revokeCN, .products, .getEnergySiteStatus, .getEnergySiteLiveStatus, .getEnergySiteInfo, .getEnergySiteHistory, .getBatteryStatus, .getBatteryData, .getBatteryPowerHistory:
return "GET"
Expand All @@ -122,14 +125,14 @@ extension Endpoint {
}
}

func baseURL() -> String {
func baseURL(teslaAPI: TeslaAPI) -> String {
switch self {
case .oAuth2Authorization, .oAuth2Token, .oAuth2revoke:
return "https://auth.tesla.com"
case .oAuth2AuthorizationCN, .oAuth2TokenCN, .oAuth2revokeCN:
return "https://auth.tesla.cn"
default:
return "https://owner-api.teslamotors.com"
return teslaAPI.url
}
}
}
Loading

0 comments on commit b8ee547

Please sign in to comment.