diff --git a/examples/basic.rs b/examples/basic.rs index 7dcb9a5..2bd71f3 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,6 +1,7 @@ use std::env; use teslatte::auth::AccessToken; use teslatte::products::Product; +use teslatte::vehicles::GetVehicleData; use teslatte::{OwnerApi, VehicleApi}; #[tokio::main] @@ -20,7 +21,8 @@ async fn main() { dbg!(&*vehicles); if !vehicles.is_empty() { - let vehicle_data = api.vehicle_data(&vehicles[0].id).await.unwrap(); + let get_vehicle_data = GetVehicleData::new(vehicles[0].id.clone()); + let vehicle_data = api.vehicle_data(&get_vehicle_data).await.unwrap(); dbg!(&vehicle_data); } else { println!("No vehicles found!"); diff --git a/src/cli/vehicle.rs b/src/cli/vehicle.rs index 0f762e9..e592897 100644 --- a/src/cli/vehicle.rs +++ b/src/cli/vehicle.rs @@ -1,5 +1,6 @@ use crate::vehicles::{ - SetChargeLimit, SetChargingAmps, SetScheduledCharging, SetScheduledDeparture, SetTemperatures, + Endpoints, GetVehicleData, SetChargeLimit, SetChargingAmps, SetScheduledCharging, + SetScheduledDeparture, SetTemperatures, }; use crate::{OwnerApi, VehicleApi, VehicleId}; use clap::{Args, Subcommand}; @@ -7,7 +8,7 @@ use clap::{Args, Subcommand}; #[derive(Debug, Subcommand)] pub enum VehicleCommand { /// Get vehicle data. - VehicleData, + VehicleData(Endpoints), /// Open the charge port door or unlocks the cable. ChargePortDoorOpen, @@ -75,8 +76,9 @@ pub struct VehicleArgs { impl VehicleArgs { pub async fn run(self, api: &OwnerApi) -> miette::Result<()> { match self.command { - VehicleCommand::VehicleData => { - api.vehicle_data(&self.id).await?; + VehicleCommand::VehicleData(endpoints) => { + let get_vehicle_data = GetVehicleData::new_with_endpoints(self.id, endpoints); + api.vehicle_data(&get_vehicle_data).await?; } VehicleCommand::SetChargeLimit(limit) => { api.set_charge_limit(&self.id, &limit).await?; diff --git a/src/energy_sites.rs b/src/energy_sites.rs index b778595..daf65f6 100644 --- a/src/energy_sites.rs +++ b/src/energy_sites.rs @@ -1,5 +1,5 @@ use crate::products::EnergySiteId; -use crate::{join_query_pairs, pub_get_arg, pub_get_args, rfc3339, OwnerApi, Values}; +use crate::{join_query_pairs, pub_get_arg, pub_get_args, rfc3339, ApiValues, OwnerApi}; use chrono::{DateTime, FixedOffset}; use serde::Deserialize; use strum::{Display, EnumString, IntoStaticStr}; @@ -194,7 +194,7 @@ pub struct CalendarHistoryValues { pub end_date: Option>, } -impl Values for CalendarHistoryValues { +impl ApiValues for CalendarHistoryValues { fn format(&self, url: &str) -> String { let url = url.replace("{}", &format!("{}", self.site_id.0)); let mut pairs: Vec<(&str, String)> = vec![ diff --git a/src/lib.rs b/src/lib.rs index f96a918..76962b2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,11 +5,11 @@ use crate::auth::{AccessToken, RefreshToken}; use crate::error::TeslatteError; use crate::vehicles::{ - SetChargeLimit, SetChargingAmps, SetScheduledCharging, SetScheduledDeparture, SetTemperatures, - Vehicle, VehicleData, + GetVehicleData, SetChargeLimit, SetChargingAmps, SetScheduledCharging, SetScheduledDeparture, + SetTemperatures, Vehicle, VehicleData, }; use chrono::{DateTime, SecondsFormat, TimeZone}; -use derive_more::{Display, FromStr}; +use derive_more::{Deref, Display, From, FromStr}; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::fmt::{Debug, Display}; @@ -29,7 +29,10 @@ const API_URL: &str = "https://owner-api.teslamotors.com/api/1"; pub trait VehicleApi { async fn vehicles(&self) -> Result, TeslatteError>; - async fn vehicle_data(&self, vehicle_id: &VehicleId) -> Result; + async fn vehicle_data( + &self, + get_vehicle_data: &GetVehicleData, + ) -> Result; async fn wake_up(&self, vehicle_id: &VehicleId) -> Result; // Alerts @@ -97,16 +100,22 @@ pub trait VehicleApi { trait EnergySitesApi {} -trait Values { +trait ApiValues { fn format(&self, url: &str) -> String; } /// Vehicle ID used by the owner-api endpoint. /// /// This data comes from [`OwnerApi::vehicles()`] `id` field. -#[derive(Debug, Serialize, Deserialize, Clone, Display, FromStr)] +#[derive(Debug, Serialize, Deserialize, Clone, Display, FromStr, From, Deref)] pub struct VehicleId(u64); +impl VehicleId { + pub fn new(id: u64) -> Self { + Self(id) + } +} + /// Vehicle ID used by other endpoints. /// /// This data comes from [`OwnerApi::vehicles()`] `vehicle_id` field. @@ -333,6 +342,7 @@ pub(crate) use pub_get; /// GET /api/1/[url] with an argument. /// /// Pass in the URL as a format string with one arg, which has to impl Display. +#[allow(unused_macros)] macro_rules! get_arg { ($name:ident, $return_type:ty, $url:expr, $arg_type:ty) => { async fn $name( @@ -345,6 +355,7 @@ macro_rules! get_arg { } }; } +#[allow(unused_imports)] pub(crate) use get_arg; /// Public variant of get_arg. @@ -362,8 +373,7 @@ macro_rules! pub_get_arg { } pub(crate) use pub_get_arg; -/// GET /api/1/[url] with a struct. -#[allow(unused)] // Leaving this here for now. I'm sure it'll be used during this refactor. +/// GET /api/1/[url] with a struct to format the URL. macro_rules! get_args { ($name:ident, $return_type:ty, $url:expr, $args:ty) => { async fn $name( @@ -376,7 +386,6 @@ macro_rules! get_args { } }; } -#[allow(unused)] // Leaving this here for now. I'm sure it'll be used during this refactor. pub(crate) use get_args; /// Public variant of get_args. diff --git a/src/powerwall.rs b/src/powerwall.rs index c276f41..8c11d17 100644 --- a/src/powerwall.rs +++ b/src/powerwall.rs @@ -1,6 +1,6 @@ use crate::energy_sites::{HistoryKind, HistoryPeriod}; use crate::products::GatewayId; -use crate::{join_query_pairs, pub_get_arg, pub_get_args, rfc3339, OwnerApi, Values}; +use crate::{join_query_pairs, pub_get_arg, pub_get_args, rfc3339, ApiValues, OwnerApi}; use chrono::{DateTime, FixedOffset}; use derive_more::{Display, FromStr}; use serde::{Deserialize, Serialize}; @@ -33,7 +33,7 @@ pub struct PowerwallEnergyHistoryValues { pub end_date: Option>, } -impl Values for PowerwallEnergyHistoryValues { +impl ApiValues for PowerwallEnergyHistoryValues { fn format(&self, url: &str) -> String { let url = url.replace("{}", &self.powerwall_id.0.to_string()); let mut pairs: Vec<(&str, String)> = vec![ diff --git a/src/products.rs b/src/products.rs index 1068b7d..e5a93ae 100644 --- a/src/products.rs +++ b/src/products.rs @@ -88,7 +88,7 @@ pub struct Components { mod tests { use super::*; use crate::energy_sites::{CalendarHistoryValues, HistoryKind, HistoryPeriod}; - use crate::Values; + use crate::ApiValues; use chrono::DateTime; #[test] diff --git a/src/vehicles.rs b/src/vehicles.rs index 5eda578..b3a8eb5 100644 --- a/src/vehicles.rs +++ b/src/vehicles.rs @@ -1,17 +1,19 @@ -/// Please note that many of these structs are generated from my own API call 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 derive_more::{Deref, DerefMut, From}; +// Please note that many of these structs are generated from my own API call 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::{ - get, get_arg, post_arg, post_arg_empty, Empty, ExternalVehicleId, OwnerApi, VehicleApi, - VehicleId, + get, get_args, post_arg, post_arg_empty, ApiValues, Empty, ExternalVehicleId, OwnerApi, + VehicleApi, VehicleId, }; use serde::{Deserialize, Serialize}; +use strum::{Display, EnumString}; #[rustfmt::skip] impl VehicleApi for OwnerApi { get!(vehicles, Vec, "/vehicles"); - get_arg!(vehicle_data, VehicleData, "/vehicles/{}/vehicle_data", VehicleId); + get_args!(vehicle_data, VehicleData, "/vehicles/{}/vehicle_data", GetVehicleData); post_arg_empty!(wake_up, "/vehicles/{}/command/wake_up", VehicleId); // Alerts @@ -41,6 +43,84 @@ impl VehicleApi for OwnerApi { post_arg_empty!(remote_start_drive, "/vehicles/{}/command/remote_start_drive", VehicleId); } +#[derive(Debug, Clone, Display, EnumString)] +#[strum(serialize_all = "snake_case")] +pub enum Endpoint { + ChargeState, + ClimateState, + ClosuresState, + DriveState, + GuiSettings, + LocationData, + VehicleConfig, + VehicleState, + VehicleDataCombo, +} + +#[derive(Debug, Clone, From, Deref, DerefMut)] +#[cfg_attr(feature = "cli", derive(clap::Args))] +pub struct Endpoints { + endpoints: Vec, +} + +pub struct GetVehicleData { + pub vehicle_id: VehicleId, + + /// From https://developer.tesla.com/docs/fleet-api#vehicle_data + /// String of URL-encoded, semicolon-separated values. Can be many of 'charge_state', + /// 'climate_state', 'closures_state', 'drive_state', 'gui_settings', 'location_data', + /// 'vehicle_config', 'vehicle_state', 'vehicle_data_combo'. + pub endpoints: Endpoints, +} + +impl GetVehicleData { + /// Create a new GetVehicleData request with no endpoints. + /// + /// ```rust + /// # use teslatte::VehicleId; + /// # use teslatte::vehicles::GetVehicleData; + /// let get_vehicle_data = GetVehicleData::new(123u64); + /// let get_vehicle_data = GetVehicleData::new(VehicleId::new(123u64)); + /// ``` + pub fn new(vehicle_id: impl Into) -> Self { + Self::new_with_endpoints(vehicle_id, vec![]) + } + + /// Create a new GetVehicleData request with endpoints. + /// ```rust + /// # use teslatte::vehicles::{Endpoint, GetVehicleData}; + /// let get_vehicle_data = GetVehicleData::new_with_endpoints(123u64, vec![Endpoint::ChargeState, Endpoint::ClimateState]); + /// ``` + pub fn new_with_endpoints( + vehicle_id: impl Into, + endpoints: impl Into, + ) -> Self { + Self { + vehicle_id: vehicle_id.into(), + endpoints: endpoints.into(), + } + } +} + +impl ApiValues for GetVehicleData { + fn format(&self, url: &str) -> String { + let url = url.replace("{}", &format!("{}", *self.vehicle_id)); + + if self.endpoints.is_empty() { + return url; + } + + let endpoints = self + .endpoints + .iter() + .map(|e| e.to_string()) + .collect::>() + .join(";"); + + format!("{}?endpoints={}", url, endpoints) + } +} + #[derive(Debug, Clone, Deserialize)] pub struct VehicleData { pub id: VehicleId, @@ -183,9 +263,9 @@ pub struct ClimateState { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DriveState { - /// gak: The following fields (up to native_type) suddenly vanished from the API response so - /// I've made them all Option. Maybe API now only returns them when driving? - /// TODO: Check if they come back + /// From https://developer.tesla.com/docs/fleet-api#vehicle_data + /// For vehicles running firmware versions 2023.38+, location_data is required to fetch vehicle + /// location. This will result in a location sharing icon to show on the vehicle UI. pub gps_as_of: Option, pub heading: Option, pub latitude: Option, @@ -461,6 +541,23 @@ mod tests { use super::*; use crate::{PrintResponses, RequestData}; + #[test] + fn vehicle_query() { + let url = "A/{}/B"; + + let get_vehicle_data = GetVehicleData::new(123); + assert_eq!(get_vehicle_data.format(url), "A/123/B"); + + let get_vehicle_data = GetVehicleData::new_with_endpoints( + 123, + vec![Endpoint::ChargeState, Endpoint::ClimateState], + ); + assert_eq!( + get_vehicle_data.format(url), + "A/123/B?endpoints=charge_state;climate_state" + ); + } + #[test] fn json_charge_state() { let s = r#"