From 3123cad1a57afb2e20234db724db9469c317de03 Mon Sep 17 00:00:00 2001 From: Alex Janka Date: Thu, 28 Dec 2023 13:06:34 +1100 Subject: [PATCH] my changes --- src/auth.rs | 65 ++++++++++++++- src/lib.rs | 192 ++++++++++++++++++++++++++++++++++++++++++++- src/products.rs | 7 +- src/selfsigned.pem | 14 ++++ src/vehicles.rs | 41 +++++++++- 5 files changed, 311 insertions(+), 8 deletions(-) create mode 100644 src/selfsigned.pem diff --git a/src/auth.rs b/src/auth.rs index 5d0be5b..201e0ba 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,5 +1,5 @@ use crate::error::TeslatteError::{CouldNotFindCallbackCode, CouldNotFindState}; -use crate::{OwnerApi, TeslatteError}; +use crate::{FleetApi, OwnerApi, TeslatteError}; use derive_more::{Display, FromStr}; use rand::Rng; use reqwest::Client; @@ -10,6 +10,7 @@ use url::Url; const AUTHORIZE_URL: &str = "https://auth.tesla.com/oauth2/v3/authorize"; const TOKEN_URL: &str = "https://auth.tesla.com/oauth2/v3/token"; +const CLIENT_ID: &str = "48ad82d96e76-4cf0-a301-08794a139ad9"; #[derive(Debug, Clone, Serialize, Deserialize, FromStr, Display)] pub struct AccessToken(pub String); @@ -193,6 +194,68 @@ page, where the URL will start with https://auth.tesla.com/void/callback?code=.. } } +impl FleetApi { + /// Refresh the internally stored access token using the known refresh token. + pub async fn refresh(&mut self) -> Result<(), TeslatteError> { + match &self.refresh_token { + None => Err(TeslatteError::NoRefreshToken), + Some(refresh_token) => { + let response = Self::refresh_token(refresh_token).await?; + self.access_token = response.access_token; + self.refresh_token = Some(response.refresh_token); + Ok(()) + } + } + } + + pub async fn refresh_token( + refresh_token: &RefreshToken, + ) -> Result { + let url = "https://auth.tesla.com/oauth2/v3/token"; + let payload = RefreshTokenRequest { + grant_type: "refresh_token".into(), + client_id: CLIENT_ID.into(), + refresh_token: refresh_token.0.clone(), + scope: "openid email offline_access".into(), + }; + Self::auth_post(url, &payload).await + } + + async fn auth_post<'a, S, D>(url: &str, payload: &S) -> Result + where + S: Serialize, + D: DeserializeOwned, + { + let response = Client::new() + .post(url) + .header("Accept", "application/json") + .json(payload) + .send() + .await + .map_err(|source| TeslatteError::FetchError { + source, + request: url.to_string(), + })?; + + let body = response + .text() + .await + .map_err(|source| TeslatteError::FetchError { + source, + request: url.to_string(), + })?; + + let json = + serde_json::from_str::(&body).map_err(|source| TeslatteError::DecodeJsonError { + source, + body: body.to_string(), + request: url.to_string(), + })?; + + Ok(json) + } +} + #[derive(Debug, Serialize)] struct RefreshTokenRequest { grant_type: String, diff --git a/src/lib.rs b/src/lib.rs index 5aacb78..774f7b0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,7 @@ use crate::auth::{AccessToken, RefreshToken}; use crate::error::TeslatteError; use crate::vehicles::{ GetVehicleData, SetChargeLimit, SetChargingAmps, SetScheduledCharging, SetScheduledDeparture, - SetTemperatures, VehicleData, + SetTemperatures, Vehicle, VehicleData, }; use chrono::{DateTime, SecondsFormat, TimeZone}; use derive_more::{Deref, Display, From, FromStr}; @@ -25,7 +25,11 @@ pub mod vehicles; #[cfg(feature = "cli")] pub mod cli; -const API_URL: &str = "https://owner-api.teslamotors.com/api/1"; +const API_URL: &str = if cfg!(debug_assertions) { + "https://cnut:4443/api/1" +} else { + "https://localhost:4443/api/1" +}; pub trait VehicleApi { async fn vehicle_data( @@ -97,6 +101,61 @@ pub trait VehicleApi { ) -> Result; } +pub trait FleetVehicleApi { + async fn vehicles(&self) -> Result, TeslatteError>; + async fn vehicle_data( + &self, + get_vehicle_data: &GetVehicleData, + ) -> Result; + async fn wake_up(&self, vin: &str) -> Result; + + // Alerts + async fn honk_horn(&self, vin: &str) -> Result; + async fn flash_lights(&self, vin: &str) -> Result; + + // Charging + async fn charge_port_door_open(&self, vin: &str) -> Result; + async fn charge_port_door_close(&self, vin: &str) -> Result; + async fn set_charge_limit( + &self, + vin: &str, + data: &SetChargeLimit, + ) -> Result; + async fn set_charging_amps( + &self, + vin: &str, + data: &SetChargingAmps, + ) -> Result; + async fn charge_standard(&self, vin: &str) -> Result; + async fn charge_max_range(&self, vin: &str) -> Result; + async fn charge_start(&self, vin: &str) -> Result; + async fn charge_stop(&self, vin: &str) -> Result; + async fn set_scheduled_charging( + &self, + vin: &str, + data: &SetScheduledCharging, + ) -> Result; + async fn set_scheduled_departure( + &self, + vin: &str, + data: &SetScheduledDeparture, + ) -> Result; + + // HVAC + async fn auto_conditioning_start(&self, vin: &str) -> Result; + async fn auto_conditioning_stop(&self, vin: &str) -> Result; + async fn set_temps( + &self, + vin: &str, + data: &SetTemperatures, + ) -> Result; + + // Doors + async fn door_unlock(&self, vin: &str) -> Result; + async fn door_lock(&self, vin: &str) -> Result; + async fn remote_start_drive(&self, vin: &str) -> Result; +} + trait ApiValues { fn format(&self, url: &str) -> String; } @@ -269,6 +328,135 @@ impl OwnerApi { } } +pub struct FleetApi { + pub access_token: AccessToken, + pub refresh_token: Option, + pub print_responses: PrintResponses, + client: Client, +} + +impl FleetApi { + pub fn new(access_token: AccessToken, refresh_token: Option) -> Self { + Self { + access_token, + refresh_token, + print_responses: PrintResponses::No, + client: Client::builder() + .timeout(std::time::Duration::from_secs(10)) + .add_root_certificate( + reqwest::Certificate::from_pem(include_bytes!("./selfsigned.pem")).unwrap(), + ) + .danger_accept_invalid_certs(cfg!(debug_assertions)) + .build() + .unwrap(), // TODO: unwrap + } + } + + async fn get(&self, url: &str) -> Result + where + D: for<'de> Deserialize<'de> + Debug, + { + self.request(&RequestData::Get { url }).await + } + + async fn post(&self, url: &str, body: S) -> Result + where + S: Serialize + Debug, + { + let payload = + &serde_json::to_string(&body).expect("Should not fail creating the request struct."); + let request_data = RequestData::Post { url, payload }; + let data = self.request::(&request_data).await?; + + if !data.result { + return Err(TeslatteError::ServerError { + request: format!("{request_data}"), + description: None, + msg: data.reason, + body: None, + }); + } + + Ok(data) + } + + async fn request(&self, request_data: &RequestData<'_>) -> Result + where + T: for<'de> Deserialize<'de> + Debug, + { + debug!("{request_data}"); + + let request_builder = match request_data { + RequestData::Get { url } => self.client.get(*url), + RequestData::Post { url, payload } => self + .client + .post(*url) + .header("Content-Type", "application/json") + .body(payload.to_string()), + }; + + let response_body = request_builder + .header("Accept", "application/json") + .header( + "Authorization", + format!("Bearer {}", self.access_token.0.trim()), + ) + .send() + .await + .map_err(|source| TeslatteError::FetchError { + source, + request: format!("{request_data}"), + })? + .text() + .await + .map_err(|source| TeslatteError::FetchError { + source, + request: format!("{request_data}"), + })?; + + debug!("Response: {response_body}"); + + Self::parse_json(request_data, response_body, self.print_responses) + } + + fn parse_json( + request_data: &RequestData, + response_body: String, + print_response: PrintResponses, + ) -> Result + where + T: for<'de> Deserialize<'de> + Debug, + { + match print_response { + PrintResponses::No => {} + PrintResponses::Plain => { + println!("{}", response_body); + } + PrintResponses::Pretty => { + print_json_str(&response_body); + } + } + + let response: Response = serde_json::from_str::>(&response_body) + .map_err(|source| TeslatteError::DecodeJsonError { + source, + request: format!("{request_data}"), + body: response_body.to_string(), + })? + .into(); + + match response { + Response::Response(data) => Ok(data), + Response::Error(e) => Err(TeslatteError::ServerError { + request: format!("{request_data}"), + msg: e.error, + description: e.error_description, + body: Some(response_body.to_owned()), + }), + } + } +} + #[derive(Debug, Deserialize)] struct ResponseDeserializer { error: Option, diff --git a/src/products.rs b/src/products.rs index 53e4545..28483d1 100644 --- a/src/products.rs +++ b/src/products.rs @@ -2,7 +2,7 @@ use crate::energy_sites::WallConnector; use crate::error::TeslatteError; use crate::powerwall::PowerwallId; use crate::vehicles::VehicleData; -use crate::{pub_get, OwnerApi}; +use crate::{pub_get, FleetApi, OwnerApi}; use derive_more::Display; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; @@ -13,6 +13,11 @@ impl OwnerApi { pub_get!(products, Vec, "/products"); } +#[rustfmt::skip] +impl FleetApi { + pub_get!(products, Vec, "/products"); +} + #[derive(Debug, Clone, Deserialize, Display)] pub struct EnergySiteId(pub u64); diff --git a/src/selfsigned.pem b/src/selfsigned.pem new file mode 100644 index 0000000..d70e11d --- /dev/null +++ b/src/selfsigned.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICJjCCAYigAwIBAgIUT9V89Ca28OFWbQM4tx7rBOaqw4UwCgYIKoZIzj0EAwIw +FDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMTIyNzA3MDcxNVoXDTMzMTIyNDA3 +MDcxNVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIGbMBAGByqGSM49AgEGBSuBBAAj +A4GGAAQAQ0BavfhlRoxnE4CmRx22LKafnx6Oqs0fO/vSmPqgqWzMmMUCsak9bNoJ +fNSFS2beUY8+myR2kpAPadp5AQAz2wsBCHEriqlp88sQKJWGaAzzmuZP0UmxaIJK +Ftcv0RLyuWST5NN61xp0yzrbF9tSjXq34qrIPcxU7t2t3IylP3rApYujdTBzMB0G +A1UdDgQWBBThM5D2TLjTuZ6we4CgyRl+iWScezAfBgNVHSMEGDAWgBThM5D2TLjT +uZ6we4CgyRl+iWScezAPBgNVHRMBAf8EBTADAQH/MBMGA1UdJQQMMAoGCCsGAQUF +BwMBMAsGA1UdDwQEAwICjDAKBggqhkjOPQQDAgOBiwAwgYcCQUx062mqiK+K8AFf +/TkjzxXYacUbvy0+ubIpytOMOT36noMkShe8m0Y/1Y3l2HGlSvWeTQzkCF0fIu/d +NdBiRFkwAkIBtqfzcXGHklZgKNg9iKfUhoX93mDUFv/b1Z3AGHYHnVT3kJvOy2zO +uQLK0NgYXCVMADGzjWuY14XYvTyTBGxfw2w= +-----END CERTIFICATE----- diff --git a/src/vehicles.rs b/src/vehicles.rs index bbd93f6..f7b1391 100644 --- a/src/vehicles.rs +++ b/src/vehicles.rs @@ -4,8 +4,8 @@ use derive_more::{Deref, DerefMut, From}; // Sometimes the API will return a null for a field where I've put in a non Option type, which // will cause the deserializer to fail. Please log an issue to fix these if you come across it. use crate::{ - get_args, post_arg, post_arg_empty, ApiValues, Empty, ExternalVehicleId, OwnerApi, VehicleApi, - VehicleId, + get, get_args, post_arg, post_arg_empty, ApiValues, Empty, ExternalVehicleId, FleetApi, + FleetVehicleApi, OwnerApi, VehicleApi, VehicleId, }; use serde::{Deserialize, Serialize}; use strum::{Display, EnumString}; @@ -42,6 +42,39 @@ impl VehicleApi for OwnerApi { post_arg_empty!(remote_start_drive, "/vehicles/{}/command/remote_start_drive", VehicleId); } +#[rustfmt::skip] +impl FleetVehicleApi for FleetApi { + get!(vehicles, Vec, "/vehicles"); + get_args!(vehicle_data, VehicleData, "/vehicles/{}/vehicle_data", GetVehicleData); + post_arg_empty!(wake_up, "/vehicles/{}/command/wake_up", str); + + // Alerts + post_arg_empty!(honk_horn, "/vehicles/{}/command/honk_horn", str); + post_arg_empty!(flash_lights, "/vehicles/{}/command/flash_lights", str); + + // Charging + post_arg_empty!(charge_port_door_open, "/vehicles/{}/command/charge_port_door_open", str); + post_arg_empty!(charge_port_door_close, "/vehicles/{}/command/charge_port_door_close", str); + post_arg!(set_charge_limit, SetChargeLimit, "/vehicles/{}/command/set_charge_limit", str); + post_arg!(set_charging_amps, SetChargingAmps, "/vehicles/{}/command/set_charging_amps", str); + post_arg_empty!(charge_standard, "/vehicles/{}/command/charge_standard", str); + post_arg_empty!(charge_max_range, "/vehicles/{}/command/charge_max_range", str); + post_arg_empty!(charge_start, "/vehicles/{}/command/charge_start", str); + post_arg_empty!(charge_stop, "/vehicles/{}/command/charge_stop", str); + post_arg!(set_scheduled_charging, SetScheduledCharging, "/vehicles/{}/command/set_scheduled_charging", str); + post_arg!(set_scheduled_departure, SetScheduledDeparture, "/vehicles/{}/command/set_scheduled_departure", str); + + // HVAC + post_arg_empty!(auto_conditioning_start, "/vehicles/{}/command/auto_conditioning_start", str); + post_arg_empty!(auto_conditioning_stop, "/vehicles/{}/command/auto_conditioning_stop", str); + post_arg!(set_temps, SetTemperatures, "/vehicles/{}/command/set_temps", str); + + // Doors + post_arg_empty!(door_unlock, "/vehicles/{}/command/door_unlock", str); + post_arg_empty!(door_lock, "/vehicles/{}/command/door_lock", str); + post_arg_empty!(remote_start_drive, "/vehicles/{}/command/remote_start_drive", str); +} + #[derive(Debug, Clone, Display, EnumString)] #[strum(serialize_all = "snake_case")] pub enum Endpoint { @@ -207,8 +240,8 @@ pub struct ChargeState { pub scheduled_charging_start_time: Option, pub scheduled_charging_start_time_app: Option, pub scheduled_charging_start_time_minutes: Option, - pub scheduled_departure_time: i64, - pub scheduled_departure_time_minutes: i64, + pub scheduled_departure_time: Option, + pub scheduled_departure_time_minutes: Option, pub supercharger_session_trip_planner: bool, pub time_to_full_charge: f64, pub timestamp: u64,