├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── TeslaAPI │ ├── Models │ ├── ChargeState.swift │ ├── Error.swift │ ├── Token.swift │ └── Vehicle.swift │ ├── Protocols │ └── RequestProtocol.swift │ ├── Requests │ ├── AuthenticateRequest.swift │ ├── ChargeStateRequest.swift │ ├── ListVehiclesRequest.swift │ ├── LockRequest.swift │ ├── MobileEnabledForVehicleRequest.swift │ └── OpenChargePortRequest.swift │ ├── TeslaAPI.swift │ └── WebRequest.swift └── Tests ├── Credentials.swift ├── ModelMocks.swift ├── Request Tests ├── ChargeStateRequestTests.swift ├── ListVehiclesRequestTests.swift ├── LockRequestTests.swift ├── MobileEnabledForRequestTests.swift └── OpenChargePortRequestTests.swift └── TeslaAPITests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | .DS_Store 6 | 7 | ## Build generated 8 | build/ 9 | DerivedData/ 10 | 11 | ## Various settings 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata/ 21 | 22 | ## Other 23 | *.moved-aside 24 | *.xccheckout 25 | *.xcscmblueprint 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | .build/ 43 | 44 | # CocoaPods 45 | # 46 | # We recommend against adding the Pods directory to your .gitignore. However 47 | # you should judge for yourself, the pros and cons are mentioned at: 48 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 49 | # 50 | # Pods/ 51 | 52 | # Carthage 53 | # 54 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 55 | # Carthage/Checkouts 56 | 57 | Carthage/Build 58 | 59 | # fastlane 60 | # 61 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 62 | # screenshots whenever they are needed. 63 | # For more information about the recommended setup visit: 64 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 65 | 66 | fastlane/report.xml 67 | fastlane/Preview.html 68 | fastlane/screenshots 69 | fastlane/test_output 70 | fastlane/README.md 71 | 72 | Tests/Credentials.swift 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 César Pinto Castillo 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "TeslaAPI", 8 | platforms: [ 9 | .macOS(.v10_12), .iOS(.v11), .watchOS(.v3), .tvOS(.v10) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, and make them visible to other packages. 13 | .library( 14 | name: "TeslaAPI", 15 | targets: ["TeslaAPI"]), 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 24 | .target( 25 | name: "TeslaAPI", 26 | dependencies: []), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tesla API supports 📱 🖥 ⌚ 📺 2 | 3 | [![Tesla-API](https://img.shields.io/badge/platform-iOS%20%7C%20macOS%20%7C%20watchOS%20%7C%20tvOS-lightgrey.svg?style=flat)](https://github.com/JagCesar/Tesla-API) 4 | [![License](https://img.shields.io/badge/license-MIT-AA8DF8.svg?style=flat)](https://github.com/JagCesar/Tesla-API/blob/master/LICENSE) 5 | 6 | This is a Swift package that works with 7 | - iOS 8 | - macOS 9 | - watchOS 10 | - tvOS 11 | 12 | It handles the communication with the vehicle API by Tesla and offers an easy to use interface. The implementation is based on the [Unofficial Tesla Model S API](https://docs.timdorr.apiary.io). 13 | 14 | ## Purpose 15 | 16 | I want to be a part of the Tesla community and since I have a lot of experience with iOS engineering I decided that this was the best way to contribute. 17 | 18 | This framework is and will always be open source. This way you can be sure there isn't anything weird going on under the hood (pun intended). And for further safety I will never precompile the framework and attach it to the release tags. 19 | 20 | I hope that open sourcing this will also inspire people to write awesome Tesla apps and help Tesla on their mission. 21 | 22 | ## Dependencies 23 | 24 | I want to avoid adding 3rd party dependencies to this project. Having 3rd party dependencies opens up the opportunity for someone to inject evil code into this project and makes it more difficult to start using this project. The goal is for anyone who knows Swift and Foundation to hit the ground running. 25 | 26 | ## How to get started 27 | 28 | The best way to use this code in your project is to add is as a Swift Package dependency. [Check out this tutorial by Apple to get started.](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app) 29 | 30 | ## How to use 31 | 32 | First thing you have to do is import `TeslaAPI` in each file where you want to use it. You do this by writing `import TeslaAPI` at the top of the file. 33 | 34 | To sign in and receive an authentication token you write: 35 | 36 | ``` 37 | AuthenticateRequest( 38 | username: username, 39 | password: password).execute { result in 40 | switch result { 41 | case .success(let token): 42 | // Handle success of login here. 43 | case .failure(let error): 44 | // Handle error here. The error object might give you a hint what went wrong. 45 | } 46 | } 47 | ``` 48 | 49 | The object `token` given in the `.success` case contains everything you need to make further requests as this user. Please note that this framework does not handle saving of this token. You have to persist this token in a way that you think makes sense. I suggest storing it in the keychain. 50 | 51 | ## Are there any other requests? 52 | 53 | I'll continuously add support for more endpoints, and I encourage you to submit PR's and help me. If you want to see which endpoints are currently available [you'll find them all here](https://github.com/JagCesar/Tesla-API/tree/master/Source/Requests). 54 | 55 | ## License 56 | 57 | This project is licensed under the terms of the MIT license. See the [LICENSE](LICENSE) file. 58 | 59 | > This project is in no way affiliated with Tesla Inc. This project is open source under the MIT license, which means you have full access to the source code and can modify it to fit your own needs. 60 | -------------------------------------------------------------------------------- /Sources/TeslaAPI/Models/ChargeState.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public class ChargeState { 4 | public enum ChargingState: String { 5 | case disconnected = "Disconnected" 6 | case charging = "Charging" 7 | case complete = "Complete" 8 | } 9 | 10 | public let batteryHeaterOn: Bool? 11 | public let batteryLevel: Int 12 | public let batteryRange: Double 13 | public let chargeCurrentRequest: Int 14 | public let chargeCurrentRequestMax: Int 15 | public let chargeEnableRequest: Bool 16 | public let chargeEnergyAdded: Double 17 | public let chargeLimitSoc: Int 18 | public let chargeLimitSocMax: Int 19 | public let chargeLimitSocMin: Int 20 | public let chargeLimitSocStd: Int 21 | public let chargeMilesAddedIdeal: Double 22 | public let chargeMilesAddedRated: Double 23 | public let chargePortDoorOpen: Bool? 24 | public let chargePortLatch: String 25 | public let chargeRate: Double 26 | public let chargeToMaxRange: Int 27 | public let chargerActualCurrent: Int 28 | public let chargerPhases: Int? 29 | public let chargerPilotCurrent: Int? 30 | public let chargerPower: Int 31 | public let chargerVoltage: Int 32 | public let chargingState: ChargingState 33 | public let estBatteryRange: Double 34 | public let euVehicle: Bool? 35 | public let fastChargerPresent: Bool 36 | public let fastChargerType: String? 37 | public let idealBatteryRange: Double 38 | public let managedChargingActive: Bool 39 | public let managedChargingStartTime: Date? 40 | public let managedChargingUserCanceled: Bool 41 | public let maxRangeChargeCounter: Int 42 | public let motorizedChargePort: Bool? 43 | public let notEnoughPowerToHeat: Bool? 44 | public let scheduledChargingPending: Bool 45 | public let scheduledChargingStartTime: Date? 46 | public let timeToFullCharge: Double 47 | public let timestamp: Date 48 | public let tripCharging: Bool? 49 | public let usableBatteryLevel: Int 50 | public let userChargeEnableRequest: Bool? 51 | 52 | init(dictionary: [String: Any]) { 53 | batteryHeaterOn = dictionary["battery_heater_on"] as? Bool 54 | batteryLevel = dictionary["battery_level"] as! Int 55 | batteryRange = dictionary["battery_range"] as! Double 56 | chargeCurrentRequest = dictionary["charge_current_request"] as! Int 57 | chargeCurrentRequestMax = dictionary["charge_current_request_max"] as! Int 58 | chargeEnableRequest = dictionary["charge_enable_request"] as! Bool 59 | chargeEnergyAdded = dictionary["charge_energy_added"] as! Double 60 | chargeLimitSoc = dictionary["charge_limit_soc"] as! Int 61 | chargeLimitSocMax = dictionary["charge_limit_soc_max"] as! Int 62 | chargeLimitSocMin = dictionary["charge_limit_soc_min"] as! Int 63 | chargeLimitSocStd = dictionary["charge_limit_soc_std"] as! Int 64 | chargeMilesAddedIdeal = dictionary["charge_miles_added_ideal"] as! Double 65 | chargeMilesAddedRated = dictionary["charge_miles_added_rated"] as! Double 66 | chargePortDoorOpen = dictionary["charge_port_door_open"] as? Bool 67 | chargePortLatch = dictionary["charge_port_latch"] as! String 68 | chargeRate = dictionary["charge_rate"] as! Double 69 | chargeToMaxRange = dictionary["charge_to_max_range"] as! Int 70 | chargerActualCurrent = dictionary["charger_actual_current"] as! Int 71 | chargerPhases = dictionary["charger_phases"] as? Int 72 | chargerPilotCurrent = dictionary["charger_pilot_current"] as? Int 73 | chargerPower = dictionary["charger_power"] as! Int 74 | chargerVoltage = dictionary["charger_voltage"] as! Int 75 | chargingState = ChargingState(rawValue: dictionary["charging_state"] as! String)! 76 | estBatteryRange = dictionary["est_battery_range"] as! Double 77 | euVehicle = dictionary["eu_vehicle"] as? Bool 78 | fastChargerPresent = dictionary["fast_charger_present"] as! Bool 79 | fastChargerType = dictionary["fast_charger_type"] as? String 80 | idealBatteryRange = dictionary["ideal_battery_range"] as! Double 81 | managedChargingActive = dictionary["managed_charging_active"] as! Bool 82 | if let timestamp = dictionary["managed_charging_start_time"] as? Double { 83 | managedChargingStartTime = Date(timeIntervalSince1970: timestamp) 84 | } else { 85 | managedChargingStartTime = nil 86 | } 87 | managedChargingUserCanceled = dictionary["managed_charging_user_canceled"] as! Bool 88 | maxRangeChargeCounter = dictionary["max_range_charge_counter"] as! Int 89 | motorizedChargePort = dictionary["motorized_charge_port"] as? Bool 90 | notEnoughPowerToHeat = dictionary["not_enough_power_to_heat"] as? Bool 91 | scheduledChargingPending = dictionary["scheduled_charging_pending"] as! Bool 92 | if let timestamp = dictionary["scheduled_charging_start_time"] as? Double { 93 | scheduledChargingStartTime = Date(timeIntervalSince1970: timestamp) 94 | } else { 95 | scheduledChargingStartTime = nil 96 | } 97 | timeToFullCharge = dictionary["time_to_full_charge"] as! Double 98 | timestamp = Date(timeIntervalSince1970: dictionary["timestamp"] as! Double) 99 | tripCharging = dictionary["trip_charging"] as? Bool 100 | usableBatteryLevel = dictionary["usable_battery_level"] as! Int 101 | userChargeEnableRequest = dictionary["user_charge_enable_request"] as? Bool 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Sources/TeslaAPI/Models/Error.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct APIError: Error { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /Sources/TeslaAPI/Models/Token.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public class Token { 4 | public let accessToken: String 5 | public let type: String 6 | public let expires: Date 7 | public let created: Date 8 | public let refreshToken: String 9 | 10 | init(dictionary: [String: AnyObject]) throws { 11 | guard let accessToken = dictionary["access_token"] as? String, 12 | let type = dictionary["token_type"] as? String, 13 | let expiresInteger = dictionary["expires_in"] as? Double, 14 | let createdInteger = dictionary["created_at"] as? Double, 15 | let refreshToken = dictionary["refresh_token"] as? String else { 16 | throw APIError() 17 | } 18 | self.accessToken = accessToken 19 | self.type = type 20 | self.expires = Date(timeIntervalSince1970: expiresInteger + createdInteger) 21 | self.created = Date(timeIntervalSince1970: createdInteger) 22 | self.refreshToken = refreshToken 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/TeslaAPI/Models/Vehicle.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public class Vehicle { 4 | public enum State: String { 5 | case online 6 | } 7 | public let color: String? 8 | public let displayName: String? 9 | public let identifier: String 10 | public let optionCodes: [String] 11 | public let vehicleIdentifier: Int 12 | public let vin: String 13 | public let tokens: [String] 14 | public let state: State 15 | 16 | init?(dictionary: [String: AnyObject]) { 17 | guard let identifierNumber = dictionary["id"] as? NSNumber, 18 | let optionCodes = dictionary["option_codes"] as? String, 19 | let vehicleIdentifier = dictionary["vehicle_id"] as? Int, 20 | let vin = dictionary["vin"] as? String, 21 | let tokens = dictionary["tokens"] as? [String], 22 | let stateString = dictionary["state"] as? String, 23 | let state = State(rawValue: stateString) else { 24 | return nil 25 | } 26 | self.color = dictionary["color"] as? String 27 | self.displayName = dictionary["display_name"] as? String 28 | self.identifier = identifierNumber.stringValue 29 | self.optionCodes = optionCodes.components(separatedBy: ",") 30 | self.vehicleIdentifier = vehicleIdentifier 31 | self.vin = vin 32 | self.tokens = tokens 33 | self.state = state 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/TeslaAPI/Protocols/RequestProtocol.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum Result { 4 | case success(T) 5 | case failure(Error) 6 | } 7 | 8 | protocol RequestProtocol { 9 | associatedtype CompletionType 10 | var path: String { get } 11 | var method: WebRequest.RequestMethod { get } 12 | func execute(completion: @escaping (_ result: Result) -> Void) 13 | } 14 | -------------------------------------------------------------------------------- /Sources/TeslaAPI/Requests/AuthenticateRequest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct AuthenticateRequest: RequestProtocol { 4 | typealias CompletionType = Token 5 | let path = "/oauth/token" 6 | let method = WebRequest.RequestMethod.post 7 | private let clientIdentifier = "81527cff06843c8634fdc09e8ac0abefb46ac849f38fe1e431c2ef2106796384" 8 | private let clientSecret = "c7257eb71a564034f9419ee651c7d0e5f7aa6bfbd18bafb5c5c033b093bb2fa3" 9 | private let grantType = "password" 10 | private let username: String 11 | private let password: String 12 | 13 | public init(username: String, password: String) { 14 | self.username = username 15 | self.password = password 16 | } 17 | 18 | public func execute(completion: @escaping (Result) -> Void) { 19 | let params = [ 20 | "email": username, 21 | "password": password, 22 | "grant_type": grantType, 23 | "client_id": clientIdentifier, 24 | "client_secret": clientSecret 25 | ] 26 | WebRequest.request( 27 | path: path, 28 | method: method, 29 | params: params) { response, error in 30 | DispatchQueue.main.async { 31 | if let error = error { 32 | completion(Result.failure(error)) 33 | } else { 34 | guard let responseDictionary = response as? [String: AnyObject] else { 35 | completion(Result.failure(APIError())) 36 | return 37 | } 38 | do { 39 | let result = try Result.success(Token(dictionary: responseDictionary)) 40 | completion(result) 41 | } catch { 42 | completion(Result.failure(error)) 43 | } 44 | } 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/TeslaAPI/Requests/ChargeStateRequest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct ChargeStateRequest: RequestProtocol { 4 | typealias CompletionType = ChargeState 5 | var path: String { 6 | return "/api/1/vehicles/\(vehicleIdentifier)/data_request/charge_state" 7 | } 8 | let method = WebRequest.RequestMethod.get 9 | let accessToken: String 10 | let vehicleIdentifier: String 11 | 12 | public init(accessToken: String, vehicleIdentifier: String) { 13 | self.accessToken = accessToken 14 | self.vehicleIdentifier = vehicleIdentifier 15 | } 16 | 17 | public func execute(completion: @escaping (Result) -> Void) { 18 | WebRequest.request( 19 | path: path, 20 | method: method, 21 | accessToken: accessToken) { response, error in 22 | DispatchQueue.main.async { 23 | if let error = error { 24 | completion(Result.failure(error)) 25 | } else if let response = response as? [String: [String: Any]], 26 | let dictionary = response["response"] { 27 | completion(Result.success(ChargeState(dictionary: dictionary))) 28 | } else { 29 | completion(Result.failure(APIError())) 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/TeslaAPI/Requests/ListVehiclesRequest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct ListVehiclesRequest: RequestProtocol { 4 | typealias CompletionType = [Vehicle] 5 | let accessToken: String 6 | let path = "/api/1/vehicles" 7 | let method = WebRequest.RequestMethod.get 8 | 9 | public init(accessToken: String) { 10 | self.accessToken = accessToken 11 | } 12 | 13 | public func execute(completion: @escaping (Result<[Vehicle]>) -> Void) { 14 | WebRequest.request( 15 | path: path, 16 | method: method, 17 | accessToken: accessToken) { response, error in 18 | DispatchQueue.main.async { 19 | if let error = error { 20 | completion(Result.failure(error)) 21 | } else { 22 | guard let responseArray = response?["response"] as? [[String: AnyObject]] else { 23 | completion(Result.failure(APIError())) 24 | return 25 | } 26 | let vehicles = responseArray.compactMap { return Vehicle(dictionary: $0) } 27 | completion(Result.success(vehicles)) 28 | } 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/TeslaAPI/Requests/LockRequest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct LockRequest: RequestProtocol { 4 | public enum LockState { 5 | case lock 6 | case unlock 7 | } 8 | typealias CompletionType = Bool 9 | var path: String { 10 | switch state { 11 | case .lock: 12 | return "/api/1/vehicles/\(vehicleIdentifier)/command/door_lock" 13 | case .unlock: 14 | return "/api/1/vehicles/\(vehicleIdentifier)/command/door_unlock" 15 | } 16 | } 17 | let method = WebRequest.RequestMethod.post 18 | let accessToken: String 19 | let vehicleIdentifier: String 20 | let state: LockState 21 | 22 | public init(accessToken: String, vehicleIdentifier: String, state: LockState) { 23 | self.accessToken = accessToken 24 | self.vehicleIdentifier = vehicleIdentifier 25 | self.state = state 26 | } 27 | 28 | public func execute(completion: @escaping (Result) -> Void) { 29 | WebRequest.request( 30 | path: path, 31 | method: method, 32 | accessToken: accessToken) { response, error in 33 | DispatchQueue.main.async { 34 | if let error = error { 35 | completion(Result.failure(error)) 36 | } else if let response = response as? [String: [String: Any]], 37 | let resultBool = response["response"]?["result"] as? Bool { 38 | completion(Result.success(resultBool)) 39 | } else { 40 | completion(Result.failure(APIError())) 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/TeslaAPI/Requests/MobileEnabledForVehicleRequest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct MobileEnabledForVehicleRequest: RequestProtocol { 4 | typealias CompletionType = Bool 5 | var path: String { 6 | return "/api/1/vehicles/\(vehicleIdentifier)/mobile_enabled" 7 | } 8 | let vehicleIdentifier: String 9 | let method = WebRequest.RequestMethod.get 10 | let accessToken: String 11 | 12 | init(vehicle: Vehicle, accessToken: String) { 13 | self.vehicleIdentifier = vehicle.identifier 14 | self.accessToken = accessToken 15 | } 16 | 17 | func execute(completion: @escaping (Result) -> Void) { 18 | WebRequest.request( 19 | path: path, 20 | method: method, 21 | accessToken: accessToken) { response, error in 22 | DispatchQueue.main.async { 23 | if let error = error { 24 | completion(Result.failure(error)) 25 | } else if let response = response as? [String: Bool], 26 | let responseBool = response["response"] { 27 | completion(Result.success(responseBool)) 28 | } else { 29 | completion(Result.failure(APIError())) 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/TeslaAPI/Requests/OpenChargePortRequest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct OpenChargePortRequest: RequestProtocol { 4 | typealias CompletionType = Bool 5 | var path: String { 6 | return "/api/1/vehicles/\(vehicleIdentifier)/command/charge_port_door_open" 7 | } 8 | let method = WebRequest.RequestMethod.post 9 | let accessToken: String 10 | let vehicleIdentifier: String 11 | 12 | public init(accessToken: String, vehicleIdentifier: String) { 13 | self.accessToken = accessToken 14 | self.vehicleIdentifier = vehicleIdentifier 15 | } 16 | 17 | public func execute(completion: @escaping (Result) -> Void) { 18 | WebRequest.request( 19 | path: path, 20 | method: method, 21 | accessToken: accessToken) { response, error in 22 | DispatchQueue.main.async { 23 | if let error = error { 24 | completion(Result.failure(error)) 25 | } else if let response = response as? [String: [String: Any]], 26 | let resultBool = response["response"]?["result"] as? Bool { 27 | completion(Result.success(resultBool)) 28 | } else { 29 | completion(Result.failure(APIError())) 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/TeslaAPI/TeslaAPI.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct TeslaAPI { 4 | public static var host = "owner-api.teslamotors.com" 5 | 6 | private init() { 7 | 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/TeslaAPI/WebRequest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class WebRequest { 4 | enum RequestMethod: String { 5 | case get = "GET" 6 | case post = "POST" 7 | case put = "PUT" 8 | } 9 | 10 | static private func clientURLRequest( 11 | path: String, 12 | params: [String: String] = [:], 13 | accessToken: String? = nil) -> URLRequest { 14 | 15 | var components = URLComponents() 16 | components.scheme = "https" 17 | components.host = TeslaAPI.host 18 | components.path = path 19 | 20 | var request = URLRequest(url: components.url!) 21 | 22 | let bodyString = params.compactMap { args -> String? in 23 | guard let escapedKey = args.key.percentageEncoded else { return nil } 24 | guard let escapedValue = args.value.percentageEncoded else { return nil } 25 | return escapedKey + "=" + escapedValue 26 | }.joined(separator: "&") 27 | 28 | request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") 29 | request.httpBody = bodyString.data(using: .utf8) 30 | 31 | if let accessToken = accessToken { 32 | request.addValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization") 33 | } 34 | 35 | return request 36 | } 37 | 38 | static public func request( 39 | path: String, 40 | method: RequestMethod, 41 | params: [String: String] = [:], 42 | accessToken: String? = nil, 43 | completion: @escaping (_ response: AnyObject?, _ error: Error?) -> Void) { 44 | dataTask( 45 | request: clientURLRequest( 46 | path: path, 47 | params: params, 48 | accessToken: accessToken), 49 | method: method, 50 | completion: completion) 51 | } 52 | 53 | static private func dataTask( 54 | request: URLRequest, 55 | method: RequestMethod, 56 | completion: @escaping (_ response: AnyObject?, _ error: Error?) -> Void) { 57 | var request = request 58 | request.httpMethod = method.rawValue 59 | let session = URLSession(configuration: .default) 60 | 61 | let task = session.dataTask(with: request) { data, response, error -> Void in 62 | if let error = error { 63 | completion(nil, error) 64 | return 65 | } 66 | do { 67 | let json = try JSONSerialization.jsonObject(with: data!, options: []) 68 | completion(json as AnyObject, nil) 69 | } catch let error { 70 | completion(nil, error) 71 | } 72 | } 73 | task.resume() 74 | } 75 | } 76 | 77 | private extension String { 78 | var percentageEncoded: String? { 79 | var allowedCharacterSet = CharacterSet.urlQueryAllowed 80 | allowedCharacterSet.remove(charactersIn: "&") 81 | return addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Tests/Credentials.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import TeslaAPI 3 | 4 | extension TeslaAPITests { 5 | func username() -> String { 6 | return "" 7 | } 8 | func password() -> String { 9 | return "" 10 | } 11 | func accessToken() -> String { 12 | return "" 13 | } 14 | func vehicleIdentifier() -> String { 15 | return "" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/ModelMocks.swift: -------------------------------------------------------------------------------- 1 | @testable import TeslaAPI 2 | 3 | class ModelMocks { 4 | // swiftlint:disable force_try 5 | static let token = try! Token( 6 | // swiftlint:enable force_try 7 | dictionary: [ 8 | "access_token": "" as AnyObject, 9 | "token_type": "" as AnyObject, 10 | "expires_in": 0.0 as AnyObject, 11 | "created_at": 0.0 as AnyObject, 12 | "refresh_token": "" as AnyObject 13 | ]) 14 | 15 | static let vehicle = Vehicle( 16 | dictionary: [ 17 | "color": "" as AnyObject, 18 | "id": 0 as AnyObject, 19 | "option_codes": "" as AnyObject, 20 | "vehicle_id": 0 as AnyObject, 21 | "vin": "" as AnyObject, 22 | "tokens": [""] as AnyObject, 23 | "state": "online" as AnyObject, 24 | "display_name": "Zeus" as AnyObject, 25 | ])! 26 | } 27 | -------------------------------------------------------------------------------- /Tests/Request Tests/ChargeStateRequestTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import TeslaAPI 3 | 4 | extension TeslaAPITests { 5 | func testChargeState() { 6 | let waitExpectation = expectation(description: "Charge state") 7 | ChargeStateRequest( 8 | accessToken: accessToken(), 9 | vehicleIdentifier: vehicleIdentifier()).execute { result in 10 | switch result { 11 | case .success(_): 12 | waitExpectation.fulfill() 13 | case .failure(_): 14 | XCTFail() 15 | } 16 | } 17 | waitForExpectations(timeout: 30, handler: nil) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/Request Tests/ListVehiclesRequestTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import TeslaAPI 3 | 4 | extension TeslaAPITests { 5 | func testListVehicles() { 6 | let waitExpectation = expectation(description: "List vehicles") 7 | 8 | ListVehiclesRequest( 9 | accessToken: accessToken()).execute { result in 10 | XCTAssert(Thread.isMainThread) 11 | switch result { 12 | case .success(_): 13 | waitExpectation.fulfill() 14 | case .failure(_): 15 | XCTFail() 16 | } 17 | } 18 | 19 | waitForExpectations(timeout: 30, handler: nil) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/Request Tests/LockRequestTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import TeslaAPI 3 | 4 | extension TeslaAPITests { 5 | func testLock() { 6 | let waitExpectation = expectation(description: "Lock") 7 | 8 | LockRequest( 9 | accessToken: accessToken(), 10 | vehicleIdentifier: vehicleIdentifier(), 11 | state: .lock).execute { result in 12 | XCTAssert(Thread.isMainThread) 13 | switch result { 14 | case .success(_): 15 | waitExpectation.fulfill() 16 | case .failure(_): 17 | XCTFail() 18 | } 19 | } 20 | waitForExpectations(timeout: 30, handler: nil) 21 | } 22 | 23 | func testUnlock() { 24 | let waitExpectation = expectation(description: "Unlock") 25 | 26 | LockRequest( 27 | accessToken: accessToken(), 28 | vehicleIdentifier: vehicleIdentifier(), 29 | state: .unlock).execute { result in 30 | XCTAssert(Thread.isMainThread) 31 | switch result { 32 | case .success(_): 33 | waitExpectation.fulfill() 34 | case .failure(_): 35 | XCTFail() 36 | } 37 | } 38 | waitForExpectations(timeout: 30, handler: nil) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Tests/Request Tests/MobileEnabledForRequestTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import TeslaAPI 3 | 4 | extension TeslaAPITests { 5 | func testMobileEnabled() { 6 | let waitExpectation = expectation(description: "Check if mobile is enabled on vehicle") 7 | 8 | MobileEnabledForVehicleRequest( 9 | vehicle: ModelMocks.vehicle, 10 | accessToken: accessToken()).execute { result in 11 | XCTAssert(Thread.isMainThread) 12 | switch result { 13 | case .success(_): 14 | waitExpectation.fulfill() 15 | case .failure(_): 16 | XCTFail() 17 | } 18 | } 19 | waitForExpectations(timeout: 30, handler: nil) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/Request Tests/OpenChargePortRequestTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import TeslaAPI 3 | 4 | extension TeslaAPITests { 5 | func testOpenChargePort() { 6 | let waitExpectation = expectation(description: "Open charge port") 7 | 8 | OpenChargePortRequest( 9 | accessToken: accessToken(), 10 | vehicleIdentifier: vehicleIdentifier()).execute { result in 11 | XCTAssert(Thread.isMainThread) 12 | switch result { 13 | case .success(_): 14 | waitExpectation.fulfill() 15 | case .failure(_): 16 | XCTFail() 17 | } 18 | } 19 | waitForExpectations(timeout: 30, handler: nil) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/TeslaAPITests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import TeslaAPI 3 | 4 | class TeslaAPITests: XCTestCase { 5 | func test_Login() { 6 | let waitExpectation = expectation(description: "Sign in") 7 | 8 | AuthenticateRequest(username: username(), password: password()).execute { result in 9 | XCTAssert(Thread.isMainThread) 10 | switch result { 11 | case .success: 12 | waitExpectation.fulfill() 13 | case .failure(let error): 14 | XCTFail(error.localizedDescription) 15 | } 16 | } 17 | 18 | waitForExpectations(timeout: 30, handler: nil) 19 | } 20 | } 21 | --------------------------------------------------------------------------------