From a8b58e1157ef6c146eed4681c862f812e66b1d79 Mon Sep 17 00:00:00 2001 From: gak Date: Fri, 20 Oct 2023 12:18:38 +1100 Subject: [PATCH 1/2] refactor: Api -> OwnerApi --- examples/basic.rs | 6 +++--- src/auth.rs | 22 ++++++++++++---------- src/cli/energy.rs | 4 ++-- src/cli/powerwall.rs | 4 ++-- src/cli/vehicle.rs | 4 ++-- src/energy_sites.rs | 4 ++-- src/lib.rs | 17 ++++++++++------- src/main.rs | 10 +++++----- src/powerwall.rs | 4 ++-- src/products.rs | 9 ++++++--- src/vehicles.rs | 28 +++++++++++++++------------- 11 files changed, 61 insertions(+), 51 deletions(-) diff --git a/examples/basic.rs b/examples/basic.rs index bfe4344..ccac7d6 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,16 +1,16 @@ use std::env; use teslatte::auth::AccessToken; use teslatte::products::Product; -use teslatte::Api; +use teslatte::OwnerApi; #[tokio::main] async fn main() { tracing_subscriber::fmt::init(); let api = match env::var("TESLA_ACCESS_TOKEN") { - Ok(t) => Api::new(AccessToken(t), None), + Ok(t) => OwnerApi::new(AccessToken(t), None), Err(_) => { - let api = Api::from_interactive_url().await.unwrap(); + let api = OwnerApi::from_interactive_url().await.unwrap(); println!("TOKEN: {:?}", api.access_token); api } diff --git a/src/auth.rs b/src/auth.rs index 55a3e2f..5d0be5b 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,5 +1,5 @@ use crate::error::TeslatteError::{CouldNotFindCallbackCode, CouldNotFindState}; -use crate::{Api, TeslatteError}; +use crate::{OwnerApi, TeslatteError}; use derive_more::{Display, FromStr}; use rand::Rng; use reqwest::Client; @@ -22,11 +22,11 @@ struct Callback { state: String, } -impl Api { +impl OwnerApi { /// Show a URL for the user to click on to log into tesla.com, the ask them to paste the /// URL they end up on, which is a 404 page. The URL contains OAuth information needed to /// complete authentication for an access key. - pub async fn from_interactive_url() -> Result { + pub async fn from_interactive_url() -> Result { let login_form = Self::get_login_url_for_user().await; println!("{}", "-".repeat(80)); println!("{}", login_form.url); @@ -40,12 +40,12 @@ page, where the URL will start with https://auth.tesla.com/void/callback?code=.. let callback_url = ask_input("Enter the whole URL of the 404 page: "); println!(); // Newline to make the next output more separated and clear. - Api::from_callback_url(&login_form, &callback_url).await + OwnerApi::from_callback_url(&login_form, &callback_url).await } /// Generate a [LoginForm] containing a URL the user should visit. /// - /// See [Api::from_callback_url()] for the next step. + /// See [OwnerApi::from_callback_url()] for the next step. pub async fn get_login_url_for_user() -> LoginForm { let code = Code::new(); let state = random_string(8); @@ -54,11 +54,11 @@ page, where the URL will start with https://auth.tesla.com/void/callback?code=.. } /// Parse a callback URL that the user was redirected to after logging in via - /// [Api::from_interactive_url()]. + /// [OwnerApi::from_interactive_url()]. pub async fn from_callback_url( login_form: &LoginForm, callback_url: &str, - ) -> Result { + ) -> Result { let callback = Self::extract_callback_from_url(callback_url)?; if callback.state != login_form.state { return Err(TeslatteError::StateMismatch { @@ -70,12 +70,14 @@ page, where the URL will start with https://auth.tesla.com/void/callback?code=.. let bearer = Self::exchange_auth_for_bearer(&login_form.code, &callback.code).await?; let access_token = AccessToken(bearer.access_token); let refresh_token = RefreshToken(bearer.refresh_token); - Ok(Api::new(access_token, Some(refresh_token))) + Ok(OwnerApi::new(access_token, Some(refresh_token))) } - pub async fn from_refresh_token(refresh_token: &RefreshToken) -> Result { + pub async fn from_refresh_token( + refresh_token: &RefreshToken, + ) -> Result { let response = Self::refresh_token(refresh_token).await?; - Ok(Api::new( + Ok(OwnerApi::new( response.access_token, Some(response.refresh_token), )) diff --git a/src/cli/energy.rs b/src/cli/energy.rs index d383efa..6454e5d 100644 --- a/src/cli/energy.rs +++ b/src/cli/energy.rs @@ -1,7 +1,7 @@ use crate::cli::print_json; use crate::energy_sites::{CalendarHistoryValues, HistoryKind, HistoryPeriod}; use crate::products::EnergySiteId; -use crate::Api; +use crate::OwnerApi; use chrono::DateTime; use clap::{Args, Subcommand}; use miette::{IntoDiagnostic, WrapErr}; @@ -23,7 +23,7 @@ pub struct EnergySiteArgs { } impl EnergySiteArgs { - pub async fn run(&self, api: &Api) -> miette::Result<()> { + pub async fn run(&self, api: &OwnerApi) -> miette::Result<()> { match &self.command { EnergySiteCommand::SiteStatus => { print_json(api.energy_sites_site_status(&self.id).await); diff --git a/src/cli/powerwall.rs b/src/cli/powerwall.rs index 8b5e3e0..77b5f76 100644 --- a/src/cli/powerwall.rs +++ b/src/cli/powerwall.rs @@ -1,7 +1,7 @@ use crate::cli::print_json_data; use crate::energy_sites::{HistoryKind, HistoryPeriod}; use crate::powerwall::{PowerwallEnergyHistoryValues, PowerwallId}; -use crate::Api; +use crate::OwnerApi; use clap::{Args, Subcommand}; #[derive(Debug, Subcommand)] @@ -21,7 +21,7 @@ pub struct PowerwallArgs { } impl PowerwallArgs { - pub async fn run(&self, api: &Api) -> miette::Result<()> { + pub async fn run(&self, api: &OwnerApi) -> miette::Result<()> { match self.command { PowerwallCommand::Status => { print_json_data(api.powerwall_status(&self.id).await?); diff --git a/src/cli/vehicle.rs b/src/cli/vehicle.rs index 9c5a5ef..57ab5e0 100644 --- a/src/cli/vehicle.rs +++ b/src/cli/vehicle.rs @@ -2,7 +2,7 @@ use crate::cli::print_json; use crate::vehicles::{ SetChargeLimit, SetChargingAmps, SetScheduledCharging, SetScheduledDeparture, SetTemperatures, }; -use crate::{Api, VehicleId}; +use crate::{OwnerApi, VehicleId}; use clap::{Args, Subcommand}; #[derive(Debug, Subcommand)] @@ -74,7 +74,7 @@ pub struct VehicleArgs { } impl VehicleArgs { - pub async fn run(self, api: &Api) -> miette::Result<()> { + pub async fn run(self, api: &OwnerApi) -> miette::Result<()> { match self.command { VehicleCommand::VehicleData => { print_json(api.vehicle_data(&self.id).await); diff --git a/src/energy_sites.rs b/src/energy_sites.rs index 359866e..cc03b51 100644 --- a/src/energy_sites.rs +++ b/src/energy_sites.rs @@ -1,11 +1,11 @@ use crate::products::EnergySiteId; -use crate::{get_arg, get_args, join_query_pairs, rfc3339, Api, Values}; +use crate::{get_arg, get_args, join_query_pairs, rfc3339, OwnerApi, Values}; use chrono::{DateTime, FixedOffset}; use serde::Deserialize; use strum::{Display, EnumString, IntoStaticStr}; #[rustfmt::skip] -impl Api { +impl OwnerApi { get_arg!(energy_sites_site_status, SiteStatus, "/energy_sites/{}/site_status", EnergySiteId); get_arg!(energy_sites_live_status, LiveStatus, "/energy_sites/{}/live_status", EnergySiteId); get_arg!(energy_sites_site_info, SiteInfo, "/energy_sites/{}/site_info", EnergySiteId); diff --git a/src/lib.rs b/src/lib.rs index 5c67845..fc1189b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,13 +25,13 @@ trait Values { /// Vehicle ID used by the owner-api endpoint. /// -/// This data comes from [`Api::vehicles()`] `id` field. +/// This data comes from [`OwnerApi::vehicles()`] `id` field. #[derive(Debug, Serialize, Deserialize, Clone, Display, FromStr)] pub struct VehicleId(u64); /// Vehicle ID used by other endpoints. /// -/// This data comes from [`Api::vehicles()`] `vehicle_id` field. +/// This data comes from [`OwnerApi::vehicles()`] `vehicle_id` field. #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ExternalVehicleId(u64); @@ -53,15 +53,15 @@ impl Display for RequestData<'_> { /// /// Main entry point for the API. It contains the access token and refresh token, and can be used /// to make requests to the API. -pub struct Api { +pub struct OwnerApi { pub access_token: AccessToken, pub refresh_token: Option, client: Client, } -impl Api { +impl OwnerApi { pub fn new(access_token: AccessToken, refresh_token: Option) -> Self { - Api { + OwnerApi { access_token, refresh_token, client: Client::builder() @@ -119,7 +119,10 @@ impl Api { let response_body = request_builder .header("Accept", "application/json") - .header("Authorization", format!("Bearer {}", self.access_token.0.trim())) + .header( + "Authorization", + format!("Bearer {}", self.access_token.0.trim()), + ) .send() .await .map_err(|source| TeslatteError::FetchError { @@ -347,7 +350,7 @@ mod tests { payload: "doesn't matter", }; - let e = Api::parse_json::(&request_data, s.to_string()); + let e = OwnerApi::parse_json::(&request_data, s.to_string()); if let Err(e) = e { if let TeslatteError::ServerError { msg, description, .. diff --git a/src/main.rs b/src/main.rs index 9ba48f8..5e417e1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ use teslatte::cli::energy::EnergySiteArgs; use teslatte::cli::powerwall::PowerwallArgs; use teslatte::cli::print_json; use teslatte::cli::vehicle::VehicleArgs; -use teslatte::Api; +use teslatte::OwnerApi; /// Teslatte /// @@ -76,7 +76,7 @@ async fn main() -> miette::Result<()> { match args.command { Command::Auth { save } => { - let api = Api::from_interactive_url().await?; + let api = OwnerApi::from_interactive_url().await?; print_or_save_tokens(save, &api); } Command::Refresh { refresh_token } => { @@ -88,7 +88,7 @@ async fn main() -> miette::Result<()> { } }; - let api = Api::from_refresh_token(&refresh_token).await?; + let api = OwnerApi::from_refresh_token(&refresh_token).await?; print_or_save_tokens(save, &api); } Command::Api(api_args) => { @@ -103,7 +103,7 @@ async fn main() -> miette::Result<()> { } }; - let api = Api::new(access_token, refresh_token); + let api = OwnerApi::new(access_token, refresh_token); match api_args.command { ApiCommand::Vehicles => { print_json(api.vehicles().await); @@ -126,7 +126,7 @@ async fn main() -> miette::Result<()> { Ok(()) } -fn print_or_save_tokens(save: bool, api: &Api) { +fn print_or_save_tokens(save: bool, api: &OwnerApi) { let access_token = api.access_token.clone(); let refresh_token = api.refresh_token.clone().unwrap(); diff --git a/src/powerwall.rs b/src/powerwall.rs index dd3f233..5b365f0 100644 --- a/src/powerwall.rs +++ b/src/powerwall.rs @@ -1,12 +1,12 @@ use crate::energy_sites::{HistoryKind, HistoryPeriod}; use crate::products::GatewayId; -use crate::{get_arg, get_args, join_query_pairs, rfc3339, Api, Values}; +use crate::{get_arg, get_args, join_query_pairs, rfc3339, OwnerApi, Values}; use chrono::{DateTime, FixedOffset}; use derive_more::{Display, FromStr}; use serde::{Deserialize, Serialize}; #[rustfmt::skip] -impl Api { +impl OwnerApi { get_arg!(powerwall_status, PowerwallStatus, "/powerwalls/{}/status", PowerwallId); get_args!(powerwall_energy_history, PowerwallEnergyHistory, "/powerwalls/{}/energyhistory", PowerwallEnergyHistoryValues); } diff --git a/src/products.rs b/src/products.rs index defe63f..0216bce 100644 --- a/src/products.rs +++ b/src/products.rs @@ -1,13 +1,13 @@ use crate::error::TeslatteError; use crate::powerwall::PowerwallId; use crate::vehicles::VehicleData; -use crate::{get, Api}; +use crate::{get, OwnerApi}; use derive_more::Display; use serde::{Deserialize, Serialize}; use std::str::FromStr; #[rustfmt::skip] -impl Api { +impl OwnerApi { get!(products, Vec, "/products"); } @@ -226,7 +226,10 @@ mod tests { assert_eq!(v.api_version, 42); assert_eq!(v.backseat_token, None); assert_eq!(v.backseat_token_updated_at, None); - assert_eq!(v.vehicle_config.unwrap().aux_park_lamps, Some("Eu".to_string())); + assert_eq!( + v.vehicle_config.unwrap().aux_park_lamps, + Some("Eu".to_string()) + ); } else { panic!("Wrong EnergySite"); } diff --git a/src/vehicles.rs b/src/vehicles.rs index a689fe4..e9c328b 100644 --- a/src/vehicles.rs +++ b/src/vehicles.rs @@ -2,11 +2,13 @@ /// /// 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, Api, Empty, ExternalVehicleId, VehicleId}; +use crate::{ + get, get_arg, post_arg, post_arg_empty, Empty, ExternalVehicleId, OwnerApi, VehicleId, +}; use serde::{Deserialize, Serialize}; #[rustfmt::skip] -impl Api { +impl OwnerApi { get!(vehicles, Vec, "/vehicles"); get_arg!(vehicle_data, VehicleData, "/vehicles/{}/vehicle_data", VehicleId); post_arg_empty!(wake_up, "/vehicles/{}/command/wake_up", VehicleId); @@ -482,7 +484,7 @@ mod tests { let request_data = RequestData::Get { url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/data_request/charge_state", }; - Api::parse_json::(&request_data, s.to_string()).unwrap(); + OwnerApi::parse_json::(&request_data, s.to_string()).unwrap(); } #[test] @@ -529,7 +531,7 @@ mod tests { let request_data = RequestData::Get { url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/data_request/climate_state", }; - Api::parse_json::(&request_data, s.to_string()).unwrap(); + OwnerApi::parse_json::(&request_data, s.to_string()).unwrap(); } #[test] @@ -556,7 +558,7 @@ mod tests { let request_data = RequestData::Get { url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/data_request/drive_state", }; - Api::parse_json::(&request_data, s.to_string()).unwrap(); + OwnerApi::parse_json::(&request_data, s.to_string()).unwrap(); } #[test] @@ -578,7 +580,7 @@ mod tests { let request_data = RequestData::Get { url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/data_request/gui_settings", }; - Api::parse_json::(&request_data, s.to_string()).unwrap(); + OwnerApi::parse_json::(&request_data, s.to_string()).unwrap(); } #[test] @@ -628,7 +630,7 @@ mod tests { let request_data = RequestData::Get { url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/data_request/vehicle_config", }; - Api::parse_json::(&request_data, s.to_string()).unwrap(); + OwnerApi::parse_json::(&request_data, s.to_string()).unwrap(); } #[test] @@ -703,7 +705,7 @@ mod tests { let request_data = RequestData::Get { url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/data_request/vehicle_state", }; - Api::parse_json::(&request_data, s.to_string()).unwrap(); + OwnerApi::parse_json::(&request_data, s.to_string()).unwrap(); } #[test] @@ -713,7 +715,7 @@ mod tests { let request_data = RequestData::Get { url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/vehicle_data", }; - Api::parse_json::(&request_data, s.to_string()).unwrap(); + OwnerApi::parse_json::(&request_data, s.to_string()).unwrap(); } #[test] @@ -723,7 +725,7 @@ mod tests { let request_data = RequestData::Get { url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/vehicle_data", }; - Api::parse_json::(&request_data, s.to_string()).unwrap(); + OwnerApi::parse_json::(&request_data, s.to_string()).unwrap(); } #[test] @@ -733,7 +735,7 @@ mod tests { let request_data = RequestData::Get { url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/vehicle_data", }; - Api::parse_json::(&request_data, s.to_string()).unwrap(); + OwnerApi::parse_json::(&request_data, s.to_string()).unwrap(); } #[test] @@ -743,7 +745,7 @@ mod tests { let request_data = RequestData::Get { url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/vehicle_data", }; - Api::parse_json::(&request_data, s.to_string()).unwrap(); + OwnerApi::parse_json::(&request_data, s.to_string()).unwrap(); } #[test] @@ -753,6 +755,6 @@ mod tests { let request_data = RequestData::Get { url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/vehicle_data", }; - Api::parse_json::(&request_data, s.to_string()).unwrap(); + OwnerApi::parse_json::(&request_data, s.to_string()).unwrap(); } } From 8c059769eea47dc8a3e81020698e9b9a487ed5a8 Mon Sep 17 00:00:00 2001 From: gak Date: Sun, 22 Oct 2023 09:17:32 +1100 Subject: [PATCH 2/2] refactor!: Rename Api to OwnerApi. Remove ResponseData. Add VehicleApi trait. Significant refactor to progress towards different API access that Tesla has introduced. See issues #6 and #7. Removed `ResponseData` because it wasn't very ergonomic, forcing the user to deref or call data(). Also it had specific fields for JSON output which was only used for the CLI, so I introduced a field `print_responses` in OwnerApi that the CLI can use. --- examples/basic.rs | 10 +- src/cli.rs | 34 ------- src/cli/energy.rs | 9 +- src/cli/powerwall.rs | 21 ++-- src/cli/vehicle.rs | 41 ++++---- src/energy_sites.rs | 16 +-- src/lib.rs | 237 ++++++++++++++++++++++++++++++++----------- src/main.rs | 10 +- src/powerwall.rs | 6 +- src/products.rs | 4 +- src/vehicles.rs | 40 +++++--- 11 files changed, 263 insertions(+), 165 deletions(-) diff --git a/examples/basic.rs b/examples/basic.rs index ccac7d6..7dcb9a5 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,7 +1,7 @@ use std::env; use teslatte::auth::AccessToken; use teslatte::products::Product; -use teslatte::OwnerApi; +use teslatte::{OwnerApi, VehicleApi}; #[tokio::main] async fn main() { @@ -21,7 +21,7 @@ async fn main() { if !vehicles.is_empty() { let vehicle_data = api.vehicle_data(&vehicles[0].id).await.unwrap(); - dbg!(&*vehicle_data); + dbg!(&vehicle_data); } else { println!("No vehicles found!"); } @@ -38,13 +38,13 @@ async fn main() { Product::Solar(e) => { let site_info = api.energy_sites_site_info(&e.energy_site_id).await.unwrap(); - dbg!(&*site_info); + dbg!(&site_info); let live_info = api .energy_sites_live_status(&e.energy_site_id) .await .unwrap(); - dbg!(&*live_info); + dbg!(&live_info); } Product::Powerwall(p) => { @@ -52,7 +52,7 @@ async fn main() { .energy_sites_live_status(&p.energy_site_id) .await .unwrap(); - dbg!(&*live_info); + dbg!(&live_info); } } } diff --git a/src/cli.rs b/src/cli.rs index 714804e..f3ca123 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,37 +1,3 @@ pub mod energy; pub mod powerwall; pub mod vehicle; - -use crate::error::TeslatteError; -use crate::ResponseData; -use std::process::exit; - -pub fn print_json(result: Result, TeslatteError>) { - match result { - Ok(data) => print_json_data(data), - Err(TeslatteError::ServerError { ref body, .. }) if body.is_some() => { - print_json_str(&body.clone().unwrap()) - } - Err(e) => { - eprintln!("{}", e); - exit(1); - } - } -} -pub fn print_json_data(data: ResponseData) { - // TODO: pretty print cli option - print_json_str(data.body()); -} - -pub fn print_json_str(body: &str) { - #[cfg(feature = "cli-pretty-json")] - { - use colored_json::prelude::*; - println!("{}", body.to_colored_json_auto().unwrap()); - } - - #[cfg(not(feature = "cli-pretty-json"))] - { - println!("{}", body); - } -} diff --git a/src/cli/energy.rs b/src/cli/energy.rs index 6454e5d..45f3386 100644 --- a/src/cli/energy.rs +++ b/src/cli/energy.rs @@ -1,4 +1,3 @@ -use crate::cli::print_json; use crate::energy_sites::{CalendarHistoryValues, HistoryKind, HistoryPeriod}; use crate::products::EnergySiteId; use crate::OwnerApi; @@ -26,13 +25,13 @@ impl EnergySiteArgs { pub async fn run(&self, api: &OwnerApi) -> miette::Result<()> { match &self.command { EnergySiteCommand::SiteStatus => { - print_json(api.energy_sites_site_status(&self.id).await); + api.energy_sites_site_status(&self.id).await?; } EnergySiteCommand::LiveStatus => { - print_json(api.energy_sites_live_status(&self.id).await); + api.energy_sites_live_status(&self.id).await?; } EnergySiteCommand::SiteInfo => { - print_json(api.energy_sites_site_info(&self.id).await); + api.energy_sites_site_info(&self.id).await?; } EnergySiteCommand::CalendarHistory(args) => { let start_date = args @@ -54,7 +53,7 @@ impl EnergySiteArgs { start_date, end_date, }; - print_json(api.energy_sites_calendar_history(&values).await); + api.energy_sites_calendar_history(&values).await?; } } Ok(()) diff --git a/src/cli/powerwall.rs b/src/cli/powerwall.rs index 77b5f76..d6f0cc8 100644 --- a/src/cli/powerwall.rs +++ b/src/cli/powerwall.rs @@ -1,4 +1,3 @@ -use crate::cli::print_json_data; use crate::energy_sites::{HistoryKind, HistoryPeriod}; use crate::powerwall::{PowerwallEnergyHistoryValues, PowerwallId}; use crate::OwnerApi; @@ -24,19 +23,17 @@ impl PowerwallArgs { pub async fn run(&self, api: &OwnerApi) -> miette::Result<()> { match self.command { PowerwallCommand::Status => { - print_json_data(api.powerwall_status(&self.id).await?); + api.powerwall_status(&self.id).await?; } PowerwallCommand::History => { - print_json_data( - api.powerwall_energy_history(&PowerwallEnergyHistoryValues { - powerwall_id: self.id.clone(), - period: HistoryPeriod::Day, - kind: HistoryKind::Power, - start_date: None, - end_date: None, - }) - .await?, - ); + api.powerwall_energy_history(&PowerwallEnergyHistoryValues { + powerwall_id: self.id.clone(), + period: HistoryPeriod::Day, + kind: HistoryKind::Power, + start_date: None, + end_date: None, + }) + .await?; } } Ok(()) diff --git a/src/cli/vehicle.rs b/src/cli/vehicle.rs index 57ab5e0..0f762e9 100644 --- a/src/cli/vehicle.rs +++ b/src/cli/vehicle.rs @@ -1,8 +1,7 @@ -use crate::cli::print_json; use crate::vehicles::{ SetChargeLimit, SetChargingAmps, SetScheduledCharging, SetScheduledDeparture, SetTemperatures, }; -use crate::{OwnerApi, VehicleId}; +use crate::{OwnerApi, VehicleApi, VehicleId}; use clap::{Args, Subcommand}; #[derive(Debug, Subcommand)] @@ -77,61 +76,61 @@ impl VehicleArgs { pub async fn run(self, api: &OwnerApi) -> miette::Result<()> { match self.command { VehicleCommand::VehicleData => { - print_json(api.vehicle_data(&self.id).await); + api.vehicle_data(&self.id).await?; } VehicleCommand::SetChargeLimit(limit) => { - print_json(api.set_charge_limit(&self.id, &limit).await); + api.set_charge_limit(&self.id, &limit).await?; } VehicleCommand::SetChargingAmps(charging_amps) => { - print_json(api.set_charging_amps(&self.id, &charging_amps).await); + api.set_charging_amps(&self.id, &charging_amps).await?; } VehicleCommand::ChargeStart => { - print_json(api.charge_start(&self.id).await); + api.charge_start(&self.id).await?; } VehicleCommand::ChargeStop => { - print_json(api.charge_stop(&self.id).await); + api.charge_stop(&self.id).await?; } VehicleCommand::ChargePortDoorOpen => { - print_json(api.charge_port_door_open(&self.id).await); + api.charge_port_door_open(&self.id).await?; } VehicleCommand::ChargePortDoorClose => { - print_json(api.charge_port_door_close(&self.id).await); + api.charge_port_door_close(&self.id).await?; } VehicleCommand::ChargeStandard => { - print_json(api.charge_standard(&self.id).await); + api.charge_standard(&self.id).await?; } VehicleCommand::ChargeMaxRange => { - print_json(api.charge_max_range(&self.id).await); + api.charge_max_range(&self.id).await?; } VehicleCommand::SetScheduledCharging(charging) => { - print_json(api.set_scheduled_charging(&self.id, &charging).await); + api.set_scheduled_charging(&self.id, &charging).await?; } VehicleCommand::SetScheduledDeparture(departure) => { - print_json(api.set_scheduled_departure(&self.id, &departure).await); + api.set_scheduled_departure(&self.id, &departure).await?; } VehicleCommand::HonkHorn => { - print_json(api.honk_horn(&self.id).await); + api.honk_horn(&self.id).await?; } VehicleCommand::FlashLights => { - print_json(api.flash_lights(&self.id).await); + api.flash_lights(&self.id).await?; } VehicleCommand::EnableHvac => { - print_json(api.auto_conditioning_start(&self.id).await); + api.auto_conditioning_start(&self.id).await?; } VehicleCommand::DisableHvac => { - print_json(api.auto_conditioning_stop(&self.id).await); + api.auto_conditioning_stop(&self.id).await?; } VehicleCommand::HvacTemperature(temps) => { - print_json(api.set_temps(&self.id, &temps).await); + api.set_temps(&self.id, &temps).await?; } VehicleCommand::DoorUnlock => { - print_json(api.door_unlock(&self.id).await); + api.door_unlock(&self.id).await?; } VehicleCommand::DoorLock => { - print_json(api.door_lock(&self.id).await); + api.door_lock(&self.id).await?; } VehicleCommand::RemoteStartDrive => { - print_json(api.remote_start_drive(&self.id).await); + api.remote_start_drive(&self.id).await?; } } Ok(()) diff --git a/src/energy_sites.rs b/src/energy_sites.rs index cc03b51..4f4e061 100644 --- a/src/energy_sites.rs +++ b/src/energy_sites.rs @@ -1,15 +1,15 @@ use crate::products::EnergySiteId; -use crate::{get_arg, get_args, join_query_pairs, rfc3339, OwnerApi, Values}; +use crate::{join_query_pairs, pub_get_arg, pub_get_args, rfc3339, OwnerApi, Values}; use chrono::{DateTime, FixedOffset}; use serde::Deserialize; use strum::{Display, EnumString, IntoStaticStr}; #[rustfmt::skip] impl OwnerApi { - get_arg!(energy_sites_site_status, SiteStatus, "/energy_sites/{}/site_status", EnergySiteId); - get_arg!(energy_sites_live_status, LiveStatus, "/energy_sites/{}/live_status", EnergySiteId); - get_arg!(energy_sites_site_info, SiteInfo, "/energy_sites/{}/site_info", EnergySiteId); - get_args!(energy_sites_calendar_history, CalendarHistory, "/energy_sites/{}/calendar_history", CalendarHistoryValues); + pub_get_arg!(energy_sites_site_status, SiteStatus, "/energy_sites/{}/site_status", EnergySiteId); + pub_get_arg!(energy_sites_live_status, LiveStatus, "/energy_sites/{}/live_status", EnergySiteId); + pub_get_arg!(energy_sites_site_info, SiteInfo, "/energy_sites/{}/site_info", EnergySiteId); + pub_get_args!(energy_sites_calendar_history, CalendarHistory, "/energy_sites/{}/calendar_history", CalendarHistoryValues); } #[derive(Debug, Clone, Deserialize)] @@ -22,8 +22,10 @@ pub struct SiteStatus { pub gateway_id: String, pub percentage_charged: f64, pub powerwall_onboarding_settings_set: bool, - pub powerwall_tesla_electric_interested_in: Option<()>, // TODO: Unknown type. Was null. - pub resource_type: String, // battery + // TODO: Unknown type. Was null. + pub powerwall_tesla_electric_interested_in: Option<()>, + // battery + pub resource_type: String, pub site_name: String, pub storm_mode_enabled: bool, pub sync_grid_alert_enabled: bool, diff --git a/src/lib.rs b/src/lib.rs index fc1189b..1399566 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,11 @@ +#![feature(async_fn_in_trait)] + use crate::auth::{AccessToken, RefreshToken}; use crate::error::TeslatteError; +use crate::vehicles::{ + SetChargeLimit, SetChargingAmps, SetScheduledCharging, SetScheduledDeparture, SetTemperatures, + Vehicle, VehicleData, +}; use chrono::{DateTime, SecondsFormat, TimeZone}; use derive_more::{Display, FromStr}; use reqwest::Client; @@ -19,6 +25,76 @@ pub mod cli; 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 wake_up(&self, vehicle_id: &VehicleId) -> Result; + + // Alerts + async fn honk_horn(&self, vehicle_id: &VehicleId) -> Result; + async fn flash_lights(&self, vehicle_id: &VehicleId) -> Result; + + // Charging + async fn charge_port_door_open( + &self, + vehicle_id: &VehicleId, + ) -> Result; + async fn charge_port_door_close( + &self, + vehicle_id: &VehicleId, + ) -> Result; + async fn set_charge_limit( + &self, + vehicle_id: &VehicleId, + data: &SetChargeLimit, + ) -> Result; + async fn set_charging_amps( + &self, + vehicle_id: &VehicleId, + data: &SetChargingAmps, + ) -> Result; + async fn charge_standard(&self, vehicle_id: &VehicleId) -> Result; + async fn charge_max_range(&self, vehicle_id: &VehicleId) + -> Result; + async fn charge_start(&self, vehicle_id: &VehicleId) -> Result; + async fn charge_stop(&self, vehicle_id: &VehicleId) -> Result; + async fn set_scheduled_charging( + &self, + vehicle_id: &VehicleId, + data: &SetScheduledCharging, + ) -> Result; + async fn set_scheduled_departure( + &self, + vehicle_id: &VehicleId, + data: &SetScheduledDeparture, + ) -> Result; + + // HVAC + async fn auto_conditioning_start( + &self, + vehicle_id: &VehicleId, + ) -> Result; + async fn auto_conditioning_stop( + &self, + vehicle_id: &VehicleId, + ) -> Result; + async fn set_temps( + &self, + vehicle_id: &VehicleId, + data: &SetTemperatures, + ) -> Result; + + // Doors + async fn door_unlock(&self, vehicle_id: &VehicleId) -> Result; + async fn door_lock(&self, vehicle_id: &VehicleId) -> Result; + async fn remote_start_drive( + &self, + vehicle_id: &VehicleId, + ) -> Result; +} + +trait EnergySitesApi {} + trait Values { fn format(&self, url: &str) -> String; } @@ -49,6 +125,13 @@ impl Display for RequestData<'_> { } } +#[derive(Copy, Clone, Debug)] +pub enum PrintResponses { + No, + Plain, + Pretty, +} + /// API client for the Tesla API. /// /// Main entry point for the API. It contains the access token and refresh token, and can be used @@ -56,6 +139,7 @@ impl Display for RequestData<'_> { pub struct OwnerApi { pub access_token: AccessToken, pub refresh_token: Option, + pub print_responses: PrintResponses, client: Client, } @@ -64,6 +148,7 @@ impl OwnerApi { OwnerApi { access_token, refresh_token, + print_responses: PrintResponses::No, client: Client::builder() .timeout(std::time::Duration::from_secs(10)) .build() @@ -71,14 +156,14 @@ impl OwnerApi { } } - async fn get(&self, url: &str) -> Result, TeslatteError> + 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, TeslatteError> + async fn post(&self, url: &str, body: S) -> Result where S: Serialize + Debug, { @@ -87,22 +172,19 @@ impl OwnerApi { let request_data = RequestData::Post { url, payload }; let data = self.request::(&request_data).await?; - if !data.data.result { + if !data.result { return Err(TeslatteError::ServerError { request: format!("{request_data}"), - msg: data.data.reason, description: None, - body: Some(data.body), + msg: data.reason, + body: None, }); } Ok(data) } - async fn request( - &self, - request_data: &RequestData<'_>, - ) -> Result, TeslatteError> + async fn request(&self, request_data: &RequestData<'_>) -> Result where T: for<'de> Deserialize<'de> + Debug, { @@ -138,16 +220,27 @@ impl OwnerApi { debug!("Response: {response_body}"); - Self::parse_json(request_data, response_body) + Self::parse_json(request_data, response_body, self.print_responses) } fn parse_json( request_data: &RequestData, response_body: String, - ) -> Result, TeslatteError> + 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, @@ -157,10 +250,7 @@ impl OwnerApi { .into(); match response { - Response::Response(data) => Ok(ResponseData { - data, - body: response_body, - }), + Response::Response(data) => Ok(data), Response::Error(e) => Err(TeslatteError::ServerError { request: format!("{request_data}"), msg: e.error, @@ -212,57 +302,41 @@ struct ResponseError { #[derive(Debug, Serialize)] struct Empty {} -/// Data and body from a request. The body can be used for debugging. -/// -/// The CLI can optionally print the raw JSON so the user can manipulate it. -/// -/// This struct will automatically deref to the `data` type for better ergonomics. -#[derive(Debug)] -pub struct ResponseData { - data: T, - body: String, -} - -impl ResponseData { - pub fn data(&self) -> &T { - &self.data - } - - pub fn body(&self) -> &str { - &self.body - } -} - -impl std::ops::Deref for ResponseData { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.data - } -} - /// GET /api/1/[url] macro_rules! get { ($name:ident, $return_type:ty, $url:expr) => { - pub async fn $name( - &self, - ) -> Result, crate::error::TeslatteError> { + async fn $name(&self) -> Result<$return_type, crate::error::TeslatteError> { let url = format!("{}{}", crate::API_URL, $url); - self.get(&url).await + self.get(&url) + .await + .map_err(|e| crate::error::TeslatteError::from(e)) } }; } pub(crate) use get; +/// Same as get, but public. +macro_rules! pub_get { + ($name:ident, $return_type:ty, $url:expr) => { + pub async fn $name(&self) -> Result<$return_type, crate::error::TeslatteError> { + let url = format!("{}{}", crate::API_URL, $url); + self.get(&url) + .await + .map_err(|e| crate::error::TeslatteError::from(e)) + } + }; +} +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. macro_rules! get_arg { ($name:ident, $return_type:ty, $url:expr, $arg_type:ty) => { - pub async fn $name( + async fn $name( &self, arg: &$arg_type, - ) -> miette::Result, crate::error::TeslatteError> { + ) -> miette::Result<$return_type, crate::error::TeslatteError> { let url = format!($url, arg); let url = format!("{}{}", crate::API_URL, url); self.get(&url).await @@ -271,29 +345,61 @@ macro_rules! get_arg { } pub(crate) use get_arg; +/// Public variant of get_arg. +macro_rules! pub_get_arg { + ($name:ident, $return_type:ty, $url:expr, $arg_type:ty) => { + pub async fn $name( + &self, + arg: &$arg_type, + ) -> miette::Result<$return_type, crate::error::TeslatteError> { + let url = format!($url, arg); + let url = format!("{}{}", crate::API_URL, url); + self.get(&url).await + } + }; +} +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. macro_rules! get_args { ($name:ident, $return_type:ty, $url:expr, $args:ty) => { - pub async fn $name( + async fn $name( &self, values: &$args, - ) -> miette::Result, crate::error::TeslatteError> { + ) -> miette::Result<$return_type, crate::error::TeslatteError> { let url = values.format($url); let url = format!("{}{}", crate::API_URL, url); self.get(&url).await } }; } +#[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. +macro_rules! pub_get_args { + ($name:ident, $return_type:ty, $url:expr, $args:ty) => { + pub async fn $name( + &self, + values: &$args, + ) -> miette::Result<$return_type, crate::error::TeslatteError> { + let url = values.format($url); + let url = format!("{}{}", crate::API_URL, url); + self.get(&url).await + } + }; +} +pub(crate) use pub_get_args; + /// 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( + async fn $name( &self, arg: &$arg_type, data: &$request_type, - ) -> miette::Result, crate::error::TeslatteError> { + ) -> miette::Result { let url = format!($url, arg); let url = format!("{}{}", crate::API_URL, url); self.post(&url, data).await @@ -305,10 +411,10 @@ pub(crate) use post_arg; /// Post like above but with an empty body using the Empty struct. macro_rules! post_arg_empty { ($name:ident, $url:expr, $arg_type:ty) => { - pub async fn $name( + async fn $name( &self, arg: &$arg_type, - ) -> miette::Result, crate::error::TeslatteError> { + ) -> miette::Result { let url = format!($url, arg); let url = format!("{}{}", crate::API_URL, url); self.post(&url, &Empty {}).await @@ -333,6 +439,19 @@ pub(crate) fn join_query_pairs(pairs: &[(&str, String)]) -> String { .join("&") } +pub fn print_json_str(body: &str) { + #[cfg(feature = "cli-pretty-json")] + { + use colored_json::prelude::*; + println!("{}", body.to_colored_json_auto().unwrap()); + } + + #[cfg(not(feature = "cli-pretty-json"))] + { + println!("{}", body); + } +} + #[cfg(test)] mod tests { use super::*; @@ -350,7 +469,11 @@ mod tests { payload: "doesn't matter", }; - let e = OwnerApi::parse_json::(&request_data, s.to_string()); + let e = OwnerApi::parse_json::( + &request_data, + s.to_string(), + PrintResponses::Pretty, + ); if let Err(e) = e { if let TeslatteError::ServerError { msg, description, .. diff --git a/src/main.rs b/src/main.rs index 5e417e1..905c529 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,9 +3,8 @@ use serde::{Deserialize, Serialize}; use teslatte::auth::{AccessToken, RefreshToken}; use teslatte::cli::energy::EnergySiteArgs; use teslatte::cli::powerwall::PowerwallArgs; -use teslatte::cli::print_json; use teslatte::cli::vehicle::VehicleArgs; -use teslatte::OwnerApi; +use teslatte::{OwnerApi, PrintResponses, VehicleApi}; /// Teslatte /// @@ -103,16 +102,17 @@ async fn main() -> miette::Result<()> { } }; - let api = OwnerApi::new(access_token, refresh_token); + let mut api = OwnerApi::new(access_token, refresh_token); + api.print_responses = PrintResponses::Pretty; match api_args.command { ApiCommand::Vehicles => { - print_json(api.vehicles().await); + api.vehicles().await?; } ApiCommand::Vehicle(v) => { v.run(&api).await?; } ApiCommand::Products => { - print_json(api.products().await); + api.products().await?; } ApiCommand::EnergySite(e) => { e.run(&api).await?; diff --git a/src/powerwall.rs b/src/powerwall.rs index 5b365f0..c276f41 100644 --- a/src/powerwall.rs +++ b/src/powerwall.rs @@ -1,14 +1,14 @@ use crate::energy_sites::{HistoryKind, HistoryPeriod}; use crate::products::GatewayId; -use crate::{get_arg, get_args, join_query_pairs, rfc3339, OwnerApi, Values}; +use crate::{join_query_pairs, pub_get_arg, pub_get_args, rfc3339, OwnerApi, Values}; use chrono::{DateTime, FixedOffset}; use derive_more::{Display, FromStr}; use serde::{Deserialize, Serialize}; #[rustfmt::skip] impl OwnerApi { - get_arg!(powerwall_status, PowerwallStatus, "/powerwalls/{}/status", PowerwallId); - get_args!(powerwall_energy_history, PowerwallEnergyHistory, "/powerwalls/{}/energyhistory", PowerwallEnergyHistoryValues); + pub_get_arg!(powerwall_status, PowerwallStatus, "/powerwalls/{}/status", PowerwallId); + pub_get_args!(powerwall_energy_history, PowerwallEnergyHistory, "/powerwalls/{}/energyhistory", PowerwallEnergyHistoryValues); } #[derive(Debug, Clone, Serialize, Deserialize, Display, FromStr)] diff --git a/src/products.rs b/src/products.rs index 0216bce..f4a162e 100644 --- a/src/products.rs +++ b/src/products.rs @@ -1,14 +1,14 @@ use crate::error::TeslatteError; use crate::powerwall::PowerwallId; use crate::vehicles::VehicleData; -use crate::{get, OwnerApi}; +use crate::{pub_get, OwnerApi}; use derive_more::Display; use serde::{Deserialize, Serialize}; use std::str::FromStr; #[rustfmt::skip] impl OwnerApi { - get!(products, Vec, "/products"); + pub_get!(products, Vec, "/products"); } #[derive(Debug, Clone, Deserialize, Display)] diff --git a/src/vehicles.rs b/src/vehicles.rs index e9c328b..0a690ab 100644 --- a/src/vehicles.rs +++ b/src/vehicles.rs @@ -3,12 +3,13 @@ /// 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, VehicleId, + get, get_arg, post_arg, post_arg_empty, Empty, ExternalVehicleId, OwnerApi, VehicleApi, + VehicleId, }; use serde::{Deserialize, Serialize}; #[rustfmt::skip] -impl OwnerApi { +impl VehicleApi for OwnerApi { get!(vehicles, Vec, "/vehicles"); get_arg!(vehicle_data, VehicleData, "/vehicles/{}/vehicle_data", VehicleId); post_arg_empty!(wake_up, "/vehicles/{}/command/wake_up", VehicleId); @@ -414,7 +415,7 @@ pub struct SetScheduledDeparture { #[cfg(test)] mod tests { use super::*; - use crate::RequestData; + use crate::{PrintResponses, RequestData}; #[test] fn json_charge_state() { @@ -484,7 +485,8 @@ mod tests { let request_data = RequestData::Get { url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/data_request/charge_state", }; - OwnerApi::parse_json::(&request_data, s.to_string()).unwrap(); + OwnerApi::parse_json::(&request_data, s.to_string(), PrintResponses::Pretty) + .unwrap(); } #[test] @@ -531,7 +533,8 @@ mod tests { let request_data = RequestData::Get { url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/data_request/climate_state", }; - OwnerApi::parse_json::(&request_data, s.to_string()).unwrap(); + OwnerApi::parse_json::(&request_data, s.to_string(), PrintResponses::Pretty) + .unwrap(); } #[test] @@ -558,7 +561,8 @@ mod tests { let request_data = RequestData::Get { url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/data_request/drive_state", }; - OwnerApi::parse_json::(&request_data, s.to_string()).unwrap(); + OwnerApi::parse_json::(&request_data, s.to_string(), PrintResponses::Pretty) + .unwrap(); } #[test] @@ -580,7 +584,8 @@ mod tests { let request_data = RequestData::Get { url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/data_request/gui_settings", }; - OwnerApi::parse_json::(&request_data, s.to_string()).unwrap(); + OwnerApi::parse_json::(&request_data, s.to_string(), PrintResponses::Pretty) + .unwrap(); } #[test] @@ -630,7 +635,8 @@ mod tests { let request_data = RequestData::Get { url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/data_request/vehicle_config", }; - OwnerApi::parse_json::(&request_data, s.to_string()).unwrap(); + OwnerApi::parse_json::(&request_data, s.to_string(), PrintResponses::Pretty) + .unwrap(); } #[test] @@ -705,7 +711,8 @@ mod tests { let request_data = RequestData::Get { url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/data_request/vehicle_state", }; - OwnerApi::parse_json::(&request_data, s.to_string()).unwrap(); + OwnerApi::parse_json::(&request_data, s.to_string(), PrintResponses::Pretty) + .unwrap(); } #[test] @@ -715,7 +722,8 @@ mod tests { let request_data = RequestData::Get { url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/vehicle_data", }; - OwnerApi::parse_json::(&request_data, s.to_string()).unwrap(); + OwnerApi::parse_json::(&request_data, s.to_string(), PrintResponses::Pretty) + .unwrap(); } #[test] @@ -725,7 +733,8 @@ mod tests { let request_data = RequestData::Get { url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/vehicle_data", }; - OwnerApi::parse_json::(&request_data, s.to_string()).unwrap(); + OwnerApi::parse_json::(&request_data, s.to_string(), PrintResponses::Pretty) + .unwrap(); } #[test] @@ -735,7 +744,8 @@ mod tests { let request_data = RequestData::Get { url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/vehicle_data", }; - OwnerApi::parse_json::(&request_data, s.to_string()).unwrap(); + OwnerApi::parse_json::(&request_data, s.to_string(), PrintResponses::Pretty) + .unwrap(); } #[test] @@ -745,7 +755,8 @@ mod tests { let request_data = RequestData::Get { url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/vehicle_data", }; - OwnerApi::parse_json::(&request_data, s.to_string()).unwrap(); + OwnerApi::parse_json::(&request_data, s.to_string(), PrintResponses::Pretty) + .unwrap(); } #[test] @@ -755,6 +766,7 @@ mod tests { let request_data = RequestData::Get { url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/vehicle_data", }; - OwnerApi::parse_json::(&request_data, s.to_string()).unwrap(); + OwnerApi::parse_json::(&request_data, s.to_string(), PrintResponses::Pretty) + .unwrap(); } }