my changes

This commit is contained in:
Alex Janka 2023-12-28 13:06:34 +11:00
parent 13f4be3478
commit 3123cad1a5
5 changed files with 311 additions and 8 deletions

View file

@ -1,5 +1,5 @@
use crate::error::TeslatteError::{CouldNotFindCallbackCode, CouldNotFindState}; use crate::error::TeslatteError::{CouldNotFindCallbackCode, CouldNotFindState};
use crate::{OwnerApi, TeslatteError}; use crate::{FleetApi, OwnerApi, TeslatteError};
use derive_more::{Display, FromStr}; use derive_more::{Display, FromStr};
use rand::Rng; use rand::Rng;
use reqwest::Client; use reqwest::Client;
@ -10,6 +10,7 @@ use url::Url;
const AUTHORIZE_URL: &str = "https://auth.tesla.com/oauth2/v3/authorize"; const AUTHORIZE_URL: &str = "https://auth.tesla.com/oauth2/v3/authorize";
const TOKEN_URL: &str = "https://auth.tesla.com/oauth2/v3/token"; 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)] #[derive(Debug, Clone, Serialize, Deserialize, FromStr, Display)]
pub struct AccessToken(pub String); 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<RefreshTokenResponse, TeslatteError> {
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<D, TeslatteError>
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::<D>(&body).map_err(|source| TeslatteError::DecodeJsonError {
source,
body: body.to_string(),
request: url.to_string(),
})?;
Ok(json)
}
}
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
struct RefreshTokenRequest { struct RefreshTokenRequest {
grant_type: String, grant_type: String,

View file

@ -6,7 +6,7 @@ use crate::auth::{AccessToken, RefreshToken};
use crate::error::TeslatteError; use crate::error::TeslatteError;
use crate::vehicles::{ use crate::vehicles::{
GetVehicleData, SetChargeLimit, SetChargingAmps, SetScheduledCharging, SetScheduledDeparture, GetVehicleData, SetChargeLimit, SetChargingAmps, SetScheduledCharging, SetScheduledDeparture,
SetTemperatures, VehicleData, SetTemperatures, Vehicle, VehicleData,
}; };
use chrono::{DateTime, SecondsFormat, TimeZone}; use chrono::{DateTime, SecondsFormat, TimeZone};
use derive_more::{Deref, Display, From, FromStr}; use derive_more::{Deref, Display, From, FromStr};
@ -25,7 +25,11 @@ pub mod vehicles;
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
pub mod 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 { pub trait VehicleApi {
async fn vehicle_data( async fn vehicle_data(
@ -97,6 +101,61 @@ pub trait VehicleApi {
) -> Result<PostResponse, TeslatteError>; ) -> Result<PostResponse, TeslatteError>;
} }
pub trait FleetVehicleApi {
async fn vehicles(&self) -> Result<Vec<Vehicle>, TeslatteError>;
async fn vehicle_data(
&self,
get_vehicle_data: &GetVehicleData,
) -> Result<VehicleData, TeslatteError>;
async fn wake_up(&self, vin: &str) -> Result<PostResponse, TeslatteError>;
// Alerts
async fn honk_horn(&self, vin: &str) -> Result<PostResponse, TeslatteError>;
async fn flash_lights(&self, vin: &str) -> Result<PostResponse, TeslatteError>;
// Charging
async fn charge_port_door_open(&self, vin: &str) -> Result<PostResponse, TeslatteError>;
async fn charge_port_door_close(&self, vin: &str) -> Result<PostResponse, TeslatteError>;
async fn set_charge_limit(
&self,
vin: &str,
data: &SetChargeLimit,
) -> Result<PostResponse, TeslatteError>;
async fn set_charging_amps(
&self,
vin: &str,
data: &SetChargingAmps,
) -> Result<PostResponse, TeslatteError>;
async fn charge_standard(&self, vin: &str) -> Result<PostResponse, TeslatteError>;
async fn charge_max_range(&self, vin: &str) -> Result<PostResponse, TeslatteError>;
async fn charge_start(&self, vin: &str) -> Result<PostResponse, TeslatteError>;
async fn charge_stop(&self, vin: &str) -> Result<PostResponse, TeslatteError>;
async fn set_scheduled_charging(
&self,
vin: &str,
data: &SetScheduledCharging,
) -> Result<PostResponse, TeslatteError>;
async fn set_scheduled_departure(
&self,
vin: &str,
data: &SetScheduledDeparture,
) -> Result<PostResponse, TeslatteError>;
// HVAC
async fn auto_conditioning_start(&self, vin: &str) -> Result<PostResponse, TeslatteError>;
async fn auto_conditioning_stop(&self, vin: &str) -> Result<PostResponse, TeslatteError>;
async fn set_temps(
&self,
vin: &str,
data: &SetTemperatures,
) -> Result<PostResponse, TeslatteError>;
// Doors
async fn door_unlock(&self, vin: &str) -> Result<PostResponse, TeslatteError>;
async fn door_lock(&self, vin: &str) -> Result<PostResponse, TeslatteError>;
async fn remote_start_drive(&self, vin: &str) -> Result<PostResponse, TeslatteError>;
}
trait ApiValues { trait ApiValues {
fn format(&self, url: &str) -> String; fn format(&self, url: &str) -> String;
} }
@ -269,6 +328,135 @@ impl OwnerApi {
} }
} }
pub struct FleetApi {
pub access_token: AccessToken,
pub refresh_token: Option<RefreshToken>,
pub print_responses: PrintResponses,
client: Client,
}
impl FleetApi {
pub fn new(access_token: AccessToken, refresh_token: Option<RefreshToken>) -> 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<D>(&self, url: &str) -> Result<D, TeslatteError>
where
D: for<'de> Deserialize<'de> + Debug,
{
self.request(&RequestData::Get { url }).await
}
async fn post<S>(&self, url: &str, body: S) -> Result<PostResponse, TeslatteError>
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::<PostResponse>(&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<T>(&self, request_data: &RequestData<'_>) -> Result<T, TeslatteError>
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<T>(
request_data: &RequestData,
response_body: String,
print_response: PrintResponses,
) -> Result<T, TeslatteError>
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<T> = serde_json::from_str::<ResponseDeserializer<T>>(&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)] #[derive(Debug, Deserialize)]
struct ResponseDeserializer<T> { struct ResponseDeserializer<T> {
error: Option<ResponseError>, error: Option<ResponseError>,

View file

@ -2,7 +2,7 @@ use crate::energy_sites::WallConnector;
use crate::error::TeslatteError; use crate::error::TeslatteError;
use crate::powerwall::PowerwallId; use crate::powerwall::PowerwallId;
use crate::vehicles::VehicleData; use crate::vehicles::VehicleData;
use crate::{pub_get, OwnerApi}; use crate::{pub_get, FleetApi, OwnerApi};
use derive_more::Display; use derive_more::Display;
use serde::{Deserialize, Deserializer, Serialize}; use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value; use serde_json::Value;
@ -13,6 +13,11 @@ impl OwnerApi {
pub_get!(products, Vec<Product>, "/products"); pub_get!(products, Vec<Product>, "/products");
} }
#[rustfmt::skip]
impl FleetApi {
pub_get!(products, Vec<Product>, "/products");
}
#[derive(Debug, Clone, Deserialize, Display)] #[derive(Debug, Clone, Deserialize, Display)]
pub struct EnergySiteId(pub u64); pub struct EnergySiteId(pub u64);

14
src/selfsigned.pem Normal file
View file

@ -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-----

View file

@ -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 // 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. // will cause the deserializer to fail. Please log an issue to fix these if you come across it.
use crate::{ use crate::{
get_args, post_arg, post_arg_empty, ApiValues, Empty, ExternalVehicleId, OwnerApi, VehicleApi, get, get_args, post_arg, post_arg_empty, ApiValues, Empty, ExternalVehicleId, FleetApi,
VehicleId, FleetVehicleApi, OwnerApi, VehicleApi, VehicleId,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum::{Display, EnumString}; use strum::{Display, EnumString};
@ -42,6 +42,39 @@ impl VehicleApi for OwnerApi {
post_arg_empty!(remote_start_drive, "/vehicles/{}/command/remote_start_drive", VehicleId); post_arg_empty!(remote_start_drive, "/vehicles/{}/command/remote_start_drive", VehicleId);
} }
#[rustfmt::skip]
impl FleetVehicleApi for FleetApi {
get!(vehicles, Vec<Vehicle>, "/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)] #[derive(Debug, Clone, Display, EnumString)]
#[strum(serialize_all = "snake_case")] #[strum(serialize_all = "snake_case")]
pub enum Endpoint { pub enum Endpoint {
@ -207,8 +240,8 @@ pub struct ChargeState {
pub scheduled_charging_start_time: Option<i64>, pub scheduled_charging_start_time: Option<i64>,
pub scheduled_charging_start_time_app: Option<i64>, pub scheduled_charging_start_time_app: Option<i64>,
pub scheduled_charging_start_time_minutes: Option<i64>, pub scheduled_charging_start_time_minutes: Option<i64>,
pub scheduled_departure_time: i64, pub scheduled_departure_time: Option<i64>,
pub scheduled_departure_time_minutes: i64, pub scheduled_departure_time_minutes: Option<i64>,
pub supercharger_session_trip_planner: bool, pub supercharger_session_trip_planner: bool,
pub time_to_full_charge: f64, pub time_to_full_charge: f64,
pub timestamp: u64, pub timestamp: u64,