diff --git a/.gitignore b/.gitignore index 4fffb2f..e5f3f78 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target /Cargo.lock +cli.json diff --git a/Cargo.toml b/Cargo.toml index fabe95b..31f7001 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,3 +26,5 @@ regex = "1.5" [dev-dependencies] test-log = { version = "0.2", default-features = false, features = ["trace"] } +tracing-subscriber = "0.3" +clap = { version = "3.2", features = ["derive", "env"]} diff --git a/examples/basic.rs b/examples/basic.rs index 5fcce37..03ae443 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,20 +1,36 @@ -use teslatte::auth::Authentication; +use clap::Parser; +use std::env; +use teslatte::auth::{AccessToken, Authentication}; +use teslatte::vehicle_state::SetChargeLimit; use teslatte::Api; #[tokio::main] async fn main() { tracing_subscriber::fmt::init(); - let api = Authentication::new().unwrap(); - let (access_token, refresh_token) = api.interactive_get_access_token().await.unwrap(); - println!("Access token: {}", access_token.0); - println!("Refresh token: {}", refresh_token.0); + let access_token = match env::var("TESLA_ACCESS_TOKEN") { + Ok(t) => AccessToken(t), + Err(_) => { + let auth = Authentication::new().unwrap(); + let (access_token, refresh_token) = auth.interactive_get_access_token().await.unwrap(); + println!("Access token: {}", access_token.0); + println!("Refresh token: {}", refresh_token.0); + access_token + } + }; let api = Api::new(&access_token); let vehicles = api.vehicles().await.unwrap(); dbg!(&vehicles); - let charge_state = api.charge_state(&vehicles[0].id).await.unwrap(); - dbg!(&charge_state); + if vehicles.len() > 0 { + let vehicle_data = api.vehicle_data(&vehicles[0].id).await.unwrap(); + dbg!(vehicle_data); + + let charge_state = api.charge_state(&vehicles[0].id).await.unwrap(); + dbg!(&charge_state); + } else { + println!("No vehicles found!"); + } } diff --git a/examples/cli.rs b/examples/cli.rs new file mode 100644 index 0000000..f809e52 --- /dev/null +++ b/examples/cli.rs @@ -0,0 +1,154 @@ +use clap::{Args, Parser, Subcommand}; +use serde::{Deserialize, Serialize}; +use teslatte::auth::{AccessToken, Authentication, RefreshToken}; +use teslatte::vehicle_state::SetChargeLimit; +use teslatte::{Api, Id}; + +const TESLA_ACCESS_TOKEN: &str = "TESLA_ACCESS_TOKEN"; +const TESLA_REFRESH_TOKEN: &str = "TESLA_REFRESH_TOKEN"; + +/// Teslatte +/// +/// A command line interface for the Tesla API. +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +struct Cli { + #[clap(subcommand)] + command: Command, +} + +#[derive(Debug, Subcommand)] +enum Command { + /// Authenticate with Tesla via URL, and receive an access token and refresh token. + Auth { + /// Save tokens to a cli.json file. + /// + /// Be careful with your access tokens! + #[clap(short, long)] + save: bool, + }, + + /// Refresh your tokens. + Refresh { + /// If not provided, will try to read the token from a cli.json file and automatically + /// update the file. + #[clap(short, long, env = "TESLA_REFRESH_TOKEN")] + refresh_token: Option, + }, + + /// Run API commands. + Api(ApiArgs), +} + +#[derive(Debug, Args)] +struct ApiArgs { + /// Access token. If not provided, will try to load from the cli.json file. + #[clap(short, long, env = "TESLA_ACCESS_TOKEN")] + access_token: Option, + + #[clap(subcommand)] + command: ApiCommand, +} + +#[derive(Debug, Subcommand)] +enum ApiCommand { + /// Get a list of vehicles. + Vehicles, + + /// Get vehicle data. + VehicleData { id: Id }, + + /// Get charge state. + ChargeState { id: Id }, + + /// Set charge limit. + SetChargeLimit { id: Id, percent: u8 }, +} + +#[tokio::main] +async fn main() { + let args = Cli::parse(); + + match args.command { + Command::Auth { save } => { + let auth = Authentication::new().unwrap(); + let (access_token, refresh_token) = auth.interactive_get_access_token().await.unwrap(); + updated_tokens(save, access_token, refresh_token); + } + Command::Refresh { refresh_token } => { + let (save, refresh_token) = match refresh_token { + Some(refresh_token) => (false, refresh_token), + None => { + let config = Config::load(); + (true, config.refresh_token) + } + }; + + let auth = Authentication::new().unwrap(); + let response = auth.refresh_access_token(&refresh_token).await.unwrap(); + updated_tokens(save, response.access_token, refresh_token); + } + Command::Api(api_args) => { + let access_token = match api_args.access_token { + Some(a) => a, + None => { + let config = Config::load(); + config.access_token + } + }; + + let api = Api::new(&access_token); + #[allow(unused_results)] + match api_args.command { + ApiCommand::Vehicles => { + dbg!(api.vehicles().await.unwrap()); + } + ApiCommand::VehicleData { id } => { + dbg!(api.vehicle_data(&id).await.unwrap()); + } + ApiCommand::ChargeState { id } => { + dbg!(api.charge_state(&id).await.unwrap()); + } + ApiCommand::SetChargeLimit { id, percent } => { + dbg!(api + .set_charge_limit(&id, &SetChargeLimit { percent }) + .await + .unwrap()); + } + } + } + } +} + +fn updated_tokens(save: bool, access_token: AccessToken, refresh_token: RefreshToken) { + println!("Access token: {}", access_token.0); + println!("Refresh token: {}", refresh_token.0); + if save { + Config { + access_token, + refresh_token, + } + .save(); + } +} + +#[derive(Serialize, Deserialize)] +struct Config { + access_token: AccessToken, + refresh_token: RefreshToken, +} + +impl Config { + fn save(&self) { + let json = serde_json::to_string(&self).unwrap(); + std::fs::write("cli.json", json).unwrap(); + } + + fn load() -> Self { + let file = std::fs::File::open("cli.json").unwrap(); + let reader = std::io::BufReader::new(file); + let json: serde_json::Value = serde_json::from_reader(reader).unwrap(); + let config: Config = serde_json::from_str(&json.to_string()).unwrap(); + config + } +} diff --git a/src/auth.rs b/src/auth.rs index ce87b99..595ce9e 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -5,6 +5,7 @@ use reqwest::Client; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use std::io::{stdin, stdout, Write}; +use std::str::FromStr; use url::Url; const AUTHORIZE_URL: &str = "https://auth.tesla.com/oauth2/v3/authorize"; @@ -17,9 +18,23 @@ pub struct Authentication { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AccessToken(pub String); +impl FromStr for AccessToken { + type Err = TeslatteError; + fn from_str(s: &str) -> Result { + Ok(AccessToken(s.to_string())) + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RefreshToken(pub String); +impl FromStr for RefreshToken { + type Err = TeslatteError; + fn from_str(s: &str) -> Result { + Ok(RefreshToken(s.to_string())) + } +} + impl Authentication { pub fn new() -> Result { let client = Client::builder() diff --git a/src/lib.rs b/src/lib.rs index 2862d58..86a9281 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,15 @@ use crate::auth::AccessToken; use crate::error::TeslatteError; +use miette::IntoDiagnostic; use reqwest::Client; use serde::{Deserialize, Serialize}; -use std::fmt::Debug; +use std::fmt::{Debug, Display, Formatter}; +use std::str::FromStr; use tracing::{debug, instrument, trace}; pub mod auth; pub mod error; +pub mod vehicle_state; const API_URL: &str = "https://owner-api.teslamotors.com"; @@ -16,6 +19,19 @@ const API_URL: &str = "https://owner-api.teslamotors.com"; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Id(u64); +impl Display for Id { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl FromStr for Id { + type Err = miette::Error; + fn from_str(s: &str) -> Result { + Ok(Id(s.parse().into_diagnostic()?)) + } +} + /// Vehicle ID used by other endpoints. /// /// This data comes from [`Api::vehicles()`] `vehicle_id` field. @@ -44,10 +60,9 @@ impl Api { // I don't understand but it works: https://stackoverflow.com/a/60131725/11125 D: for<'de> Deserialize<'de> + Debug, { - trace!("Fetching"); let url = format!("{}{}", API_URL, path); let request = || format!("GET {url}"); - debug!("Fetching"); + trace!(?url, "Fetching"); let response = self .client .get(&url) @@ -69,7 +84,7 @@ impl Api { })?; trace!(?body); - let json = Self::json::(&body, request)?; + let json = Self::parse_json::(&body, request)?; trace!(?json); Ok(json) @@ -106,7 +121,7 @@ impl Api { source, request: request(), })?; - let json = Self::json::(&body, request)?; + let json = Self::parse_json::(&body, request)?; trace!(?json); if json.result { @@ -121,7 +136,7 @@ impl Api { } // The `request` argument is for additional context in the error. - fn json(body: &str, request: F) -> Result + fn parse_json(body: &str, request: F) -> Result where T: for<'de> Deserialize<'de> + Debug, F: FnOnce() -> String + Copy, @@ -182,152 +197,86 @@ struct ResponseError { error_description: Option, } -#[derive(Debug, Deserialize)] -pub struct ChargeState { - pub battery_heater_on: bool, - pub battery_level: i64, - pub battery_range: f64, - pub charge_amps: i64, - pub charge_current_request: i64, - pub charge_current_request_max: i64, - pub charge_enable_request: bool, - pub charge_energy_added: f64, - pub charge_limit_soc: i64, - pub charge_limit_soc_max: i64, - pub charge_limit_soc_min: i64, - pub charge_limit_soc_std: i64, - pub charge_miles_added_ideal: f64, - pub charge_miles_added_rated: f64, - pub charge_port_cold_weather_mode: bool, - pub charge_port_color: String, - pub charge_port_door_open: bool, - pub charge_port_latch: String, - pub charge_rate: f64, - pub charge_to_max_range: bool, - pub charger_actual_current: i64, - pub charger_phases: Option, - pub charger_pilot_current: i64, - pub charger_power: i64, - pub charger_voltage: i64, - pub charging_state: String, - pub conn_charge_cable: String, - pub est_battery_range: f64, - pub fast_charger_brand: String, - pub fast_charger_present: bool, - pub fast_charger_type: String, - pub ideal_battery_range: f64, - pub managed_charging_active: bool, - pub managed_charging_start_time: Option, - pub managed_charging_user_canceled: bool, - pub max_range_charge_counter: i64, - pub minutes_to_full_charge: i64, - pub not_enough_power_to_heat: Option, - pub off_peak_charging_enabled: bool, - pub off_peak_charging_times: String, - pub off_peak_hours_end_time: i64, - pub preconditioning_enabled: bool, - pub preconditioning_times: String, - pub scheduled_charging_mode: String, - pub scheduled_charging_pending: bool, - 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 supercharger_session_trip_planner: bool, - pub time_to_full_charge: f64, - pub timestamp: u64, - pub trip_charging: bool, - pub usable_battery_level: i64, - pub user_charge_enable_request: Option, -} - -#[derive(Debug, Deserialize)] -pub struct Vehicles(Vec); - -#[derive(Debug, Deserialize)] -pub struct Vehicle { - pub id: Id, - pub vehicle_id: VehicleId, - pub vin: String, - pub display_name: String, -} - -#[derive(Debug, Deserialize)] -pub struct VehicleData { - id: Id, - user_id: u64, - display_name: String, -} - -#[derive(Debug, Serialize)] -pub struct SetChargingAmps { - pub charging_amps: i64, -} - -#[derive(Debug, Serialize)] -pub struct SetChargeLimit { - // pub percent: Percentage, - pub percent: u8, -} - #[derive(Debug, Serialize)] struct Empty {} -/// GET /api/1/vehicles/[id]/... +/// GET /api/1/[url] macro_rules! get { - ($name:ident, $struct:ty, $url:expr) => { - pub async fn $name(&self) -> Result<$struct, TeslatteError> { - let url = format!("/api/1/vehicles{}", $url); + ($name:ident, $return_type:ty, $url:expr) => { + pub async fn $name(&self) -> Result<$return_type, TeslatteError> { + let url = format!("/api/1{}", $url); self.get(&url).await } }; } -/// GET /api/1/vehicles/[id]/... -macro_rules! get_v { - ($name:ident, $struct:ty, $url:expr) => { - pub async fn $name(&self, id: &Id) -> Result<$struct, TeslatteError> { - let url = format!("/api/1/vehicles/{}{}", id.0, $url); +/// GET /api/1/[url] with an argument. +/// +/// Pass in the URL as a format string with one arg, which has to impl Display. +macro_rules! get_arg { + ($name:ident, $return_type:ty, $url:expr, $arg_type:ty) => { + pub async fn $name(&self, arg: &$arg_type) -> miette::Result<$return_type, TeslatteError> { + let url = format!(concat!("/api/1", $url), arg); self.get(&url).await } }; } -/// POST /api/1/vehicles/[id]/... without data -macro_rules! post_v { - ($name:ident, $url:expr) => { - pub async fn $name(&self, id: &Id) -> miette::Result<(), TeslatteError> { - let url = format!("/api/1/vehicles/{}{}", id.0, $url); - self.post(&url, &Empty {}).await +/// POST /api/1/[url] with an argument and data +macro_rules! post_arg { + ($name:ident, $request_type:ty, $url:expr, $arg_type:ty) => { + pub async fn $name( + &self, + arg: &$arg_type, + data: &$request_type, + ) -> miette::Result<(), TeslatteError> { + let url_fmt = format!($url, arg); + let url = format!(concat!("/api/1", $url), arg); + self.post(&url, data).await } }; } -/// POST /api/1/vehicles/[id]/... with data -macro_rules! post_vd { - ($name:ident, $struct:ty, $url:expr) => { - pub async fn $name(&self, id: &Id, data: &$struct) -> miette::Result<(), TeslatteError> { - let url = format!("/api/1/vehicles/{}{}", id.0, $url); - self.post(&url, &data).await - } - }; -} +// /// POST /api/1/vehicles/[id]/... without data +// macro_rules! post_v { +// ($name:ident, $url:expr) => { +// pub async fn $name(&self, id: &Id) -> miette::Result<(), TeslatteError> { +// let url = format!("/vehicles/{}{}", id.0, $url); +// self.post(&url, &Empty {}).await +// } +// }; +// } +// +// /// POST /api/1/vehicles/[id]/... with data +// macro_rules! post_vd { +// ($name:ident, $struct:ty, $url:expr) => { +// pub async fn $name(&self, id: &Id, data: &$struct) -> miette::Result<(), TeslatteError> { +// let url = format!("/api/1/vehicles/{}{}", id.0, $url); +// self.post(&url, &data).await +// } +// }; +// } + +use crate::vehicle_state::ChargeState; +use crate::vehicle_state::SetChargeLimit; +use crate::vehicle_state::Vehicle; +use crate::vehicle_state::VehicleData; #[rustfmt::skip] impl Api { - get!(vehicles, Vec, ""); - get_v!(vehicle_data, VehicleData, "/vehicle_data"); - get_v!(charge_state, ChargeState, "/data_request/charge_state"); - post_vd!(set_charge_limit, SetChargeLimit, "/command/set_charge_limit"); - post_vd!(set_charging_amps, SetChargingAmps, "/command/set_charging_amps"); - post_v!(charge_start, "/command/charge_start"); - post_v!(charge_stop, "/command/charge_stop"); + get!(vehicles, Vec, "/vehicles"); + get_arg!(vehicle_data, VehicleData, "/vehicles/{}/vehicle_data", Id); + get_arg!(charge_state, ChargeState, "/vehicles/{}/data_request/charge_state", Id); + post_arg!(set_charge_limit, SetChargeLimit, "/vehicles/{}/command/set_charge_limit", Id); + // post_vd!(set_charging_amps, SetChargingAmps, "/command/set_charging_amps"); + // post_v!(charge_start, "/command/charge_start"); + // post_v!(charge_stop, "/command/charge_stop"); } #[cfg(test)] mod tests { use super::*; + use crate::vehicle_state::ChargeState; use test_log::test; #[test] @@ -394,7 +343,7 @@ mod tests { } } "#; - Api::json::(s, || "req".to_string()).unwrap(); + Api::parse_json::(s, || "req".to_string()).unwrap(); } #[test] @@ -403,7 +352,7 @@ mod tests { "response": null, "error":{"error": "timeout","error_description": "s"} }"#; - let e = Api::json::(s, || "req".to_string()); + let e = Api::parse_json::(s, || "req".to_string()); if let Err(e) = e { if let TeslatteError::ServerError { msg, description, .. diff --git a/src/vehicle_state.rs b/src/vehicle_state.rs new file mode 100644 index 0000000..34846a2 --- /dev/null +++ b/src/vehicle_state.rs @@ -0,0 +1,306 @@ +/// Please note that these structs are generated from my own responses. +/// +/// 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::{Id, VehicleId}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Deserialize)] +pub struct VehicleData { + pub id: Id, + pub user_id: i64, + pub vehicle_id: VehicleId, + pub vin: String, + pub display_name: String, + pub option_codes: String, + /// gak: This was null for me, assuming String. + pub color: Option, + pub access_type: String, + pub tokens: Vec, + pub state: String, + pub in_service: bool, + pub id_s: String, + pub calendar_enabled: bool, + pub api_version: i64, + /// gak: This was null for me, assuming String. + pub backseat_token: Option, + /// gak: This was null for me, assuming String. + pub backseat_token_updated_at: Option, + pub charge_state: ChargeState, + pub climate_state: ClimateState, + pub drive_state: DriveState, + pub gui_settings: GuiSettings, + pub vehicle_config: VehicleConfig, + pub vehicle_state: VehicleState, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct ChargeState { + pub battery_heater_on: bool, + pub battery_level: i64, + pub battery_range: f64, + pub charge_amps: i64, + pub charge_current_request: i64, + pub charge_current_request_max: i64, + pub charge_enable_request: bool, + pub charge_energy_added: f64, + pub charge_limit_soc: i64, + pub charge_limit_soc_max: i64, + pub charge_limit_soc_min: i64, + pub charge_limit_soc_std: i64, + pub charge_miles_added_ideal: f64, + pub charge_miles_added_rated: f64, + pub charge_port_cold_weather_mode: bool, + pub charge_port_color: String, + pub charge_port_door_open: bool, + pub charge_port_latch: String, + pub charge_rate: f64, + pub charge_to_max_range: bool, + pub charger_actual_current: i64, + pub charger_phases: Option, + pub charger_pilot_current: i64, + pub charger_power: i64, + pub charger_voltage: i64, + pub charging_state: String, + pub conn_charge_cable: String, + pub est_battery_range: f64, + pub fast_charger_brand: String, + pub fast_charger_present: bool, + pub fast_charger_type: String, + pub ideal_battery_range: f64, + pub managed_charging_active: bool, + pub managed_charging_start_time: Option, + pub managed_charging_user_canceled: bool, + pub max_range_charge_counter: i64, + pub minutes_to_full_charge: i64, + pub not_enough_power_to_heat: Option, + pub off_peak_charging_enabled: bool, + pub off_peak_charging_times: String, + pub off_peak_hours_end_time: i64, + pub preconditioning_enabled: bool, + pub preconditioning_times: String, + pub scheduled_charging_mode: String, + pub scheduled_charging_pending: bool, + 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 supercharger_session_trip_planner: bool, + pub time_to_full_charge: f64, + pub timestamp: u64, + pub trip_charging: bool, + pub usable_battery_level: i64, + pub user_charge_enable_request: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ClimateState { + pub allow_cabin_overheat_protection: bool, + pub auto_seat_climate_left: bool, + pub auto_seat_climate_right: bool, + pub battery_heater: bool, + pub battery_heater_no_power: Option, + pub cabin_overheat_protection: String, + pub cabin_overheat_protection_actively_cooling: bool, + pub climate_keeper_mode: String, + pub defrost_mode: i64, + pub driver_temp_setting: f64, + pub fan_status: i64, + pub hvac_auto_request: String, + pub inside_temp: f64, + pub is_auto_conditioning_on: bool, + pub is_climate_on: bool, + pub is_front_defroster_on: bool, + pub is_preconditioning: bool, + pub is_rear_defroster_on: bool, + pub left_temp_direction: i64, + pub max_avail_temp: f64, + pub min_avail_temp: f64, + pub outside_temp: f64, + pub passenger_temp_setting: f64, + pub remote_heater_control_enabled: bool, + pub right_temp_direction: i64, + pub seat_heater_left: i64, + pub seat_heater_rear_center: i64, + pub seat_heater_rear_left: i64, + pub seat_heater_rear_right: i64, + pub seat_heater_right: i64, + pub side_mirror_heaters: bool, + pub steering_wheel_heater: bool, + pub supports_fan_only_cabin_overheat_protection: bool, + pub timestamp: i64, + pub wiper_blade_heater: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DriveState { + pub gps_as_of: i64, + pub heading: i64, + pub latitude: f64, + pub longitude: f64, + pub native_latitude: f64, + pub native_location_supported: i64, + pub native_longitude: f64, + pub native_type: String, + pub power: i64, + pub shift_state: Option, + /// gak: I've assumed this to be String. + pub speed: Option, + pub timestamp: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GuiSettings { + pub gui_24_hour_time: bool, + pub gui_charge_rate_units: String, + pub gui_distance_units: String, + pub gui_range_display: String, + pub gui_temperature_units: String, + pub show_range_units: bool, + pub timestamp: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VehicleConfig { + pub aux_park_lamps: String, + pub badge_version: i64, + pub can_accept_navigation_requests: bool, + pub can_actuate_trunks: bool, + pub car_special_type: String, + pub car_type: String, + pub charge_port_type: String, + pub dashcam_clip_save_supported: bool, + pub default_charge_to_max: bool, + pub driver_assist: String, + pub ece_restrictions: bool, + pub efficiency_package: String, + pub eu_vehicle: bool, + pub exterior_color: String, + pub exterior_trim: String, + pub exterior_trim_override: String, + pub has_air_suspension: bool, + pub has_ludicrous_mode: bool, + pub has_seat_cooling: bool, + pub headlamp_type: String, + pub interior_trim_type: String, + pub key_version: i64, + pub motorized_charge_port: bool, + pub paint_color_override: String, + pub performance_package: String, + pub plg: bool, + pub pws: bool, + pub rear_drive_unit: String, + pub rear_seat_heaters: i64, + pub rear_seat_type: i64, + pub rhd: bool, + pub roof_color: String, + pub seat_type: Option, + pub spoiler_type: String, + pub sun_roof_installed: Option, + pub supports_qr_pairing: bool, + pub third_row_seats: String, + pub timestamp: i64, + pub trim_badging: String, + pub use_range_badging: bool, + pub utc_offset: i64, + pub webcam_supported: bool, + pub wheel_type: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VehicleState { + pub api_version: i64, + pub autopark_state_v2: String, + pub autopark_style: String, + pub calendar_supported: bool, + pub car_version: String, + pub center_display_state: i64, + pub dashcam_clip_save_available: bool, + pub dashcam_state: String, + pub df: i64, + pub dr: i64, + pub fd_window: i64, + pub feature_bitmask: String, + pub fp_window: i64, + pub ft: i64, + pub is_user_present: bool, + pub last_autopark_error: String, + pub locked: bool, + pub media_state: MediaState, + pub notifications_supported: bool, + pub odometer: f64, + pub parsed_calendar_supported: bool, + pub pf: i64, + pub pr: i64, + pub rd_window: i64, + pub remote_start: bool, + pub remote_start_enabled: bool, + pub remote_start_supported: bool, + pub rp_window: i64, + pub rt: i64, + pub santa_mode: i64, + pub sentry_mode: bool, + pub sentry_mode_available: bool, + pub service_mode: bool, + pub service_mode_plus: bool, + pub smart_summon_available: bool, + pub software_update: SoftwareUpdate, + pub speed_limit_mode: SpeedLimitMode, + pub summon_standby_mode_enabled: bool, + pub timestamp: i64, + pub tpms_pressure_fl: f64, + pub tpms_pressure_fr: f64, + pub tpms_pressure_rl: f64, + pub tpms_pressure_rr: f64, + pub valet_mode: bool, + pub vehicle_name: String, + pub vehicle_self_test_progress: i64, + pub vehicle_self_test_requested: bool, + pub webcam_available: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MediaState { + pub remote_control_enabled: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SoftwareUpdate { + pub download_perc: i64, + pub expected_duration_sec: i64, + pub install_perc: i64, + pub status: String, + pub version: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SpeedLimitMode { + pub active: bool, + pub current_limit_mph: f64, + pub max_limit_mph: i64, + pub min_limit_mph: f64, + pub pin_code_set: bool, +} + +#[derive(Debug, Deserialize)] +pub struct Vehicles(Vec); + +#[derive(Debug, Deserialize)] +pub struct Vehicle { + pub id: Id, + pub vehicle_id: VehicleId, + pub vin: String, + pub display_name: String, +} + +#[derive(Debug, Serialize)] +pub struct SetChargingAmps { + pub charging_amps: i64, +} + +#[derive(Debug, Serialize)] +pub struct SetChargeLimit { + // pub percent: Percentage, + pub percent: u8, +}