diff --git a/examples/basic.rs b/examples/basic.rs index bfe4344..7dcb9a5 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, VehicleApi}; #[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 } @@ -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/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.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 d383efa..45f3386 100644 --- a/src/cli/energy.rs +++ b/src/cli/energy.rs @@ -1,7 +1,6 @@ -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,16 +22,16 @@ 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); + 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 8b5e3e0..d6f0cc8 100644 --- a/src/cli/powerwall.rs +++ b/src/cli/powerwall.rs @@ -1,7 +1,6 @@ -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,22 +20,20 @@ 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?); + 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 9c5a5ef..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::{Api, VehicleId}; +use crate::{OwnerApi, VehicleApi, VehicleId}; use clap::{Args, Subcommand}; #[derive(Debug, Subcommand)] @@ -74,64 +73,64 @@ 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); + 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 359866e..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, Api, 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 Api { - 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); +impl OwnerApi { + 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 5c67845..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,19 +25,89 @@ 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; } /// 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); @@ -49,21 +125,30 @@ 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 /// to make requests to the API. -pub struct Api { +pub struct OwnerApi { pub access_token: AccessToken, pub refresh_token: Option, + pub print_responses: PrintResponses, client: Client, } -impl Api { +impl OwnerApi { pub fn new(access_token: AccessToken, refresh_token: Option) -> Self { - Api { + 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 Api { } } - 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 Api { 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, { @@ -119,7 +201,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 { @@ -135,16 +220,27 @@ impl Api { 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, @@ -154,10 +250,7 @@ impl Api { .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, @@ -209,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 @@ -268,29 +345,61 @@ macro_rules! get_arg { } pub(crate) use get_arg; -/// GET /api/1/[url] with a struct. -macro_rules! get_args { - ($name:ident, $return_type:ty, $url:expr, $args:ty) => { +/// 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) => { + 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 @@ -302,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 @@ -330,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::*; @@ -347,7 +469,11 @@ 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(), + PrintResponses::Pretty, + ); if let Err(e) = e { if let TeslatteError::ServerError { msg, description, .. diff --git a/src/main.rs b/src/main.rs index 9ba48f8..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::Api; +use teslatte::{OwnerApi, PrintResponses, VehicleApi}; /// Teslatte /// @@ -76,7 +75,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 +87,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,16 +102,17 @@ async fn main() -> miette::Result<()> { } }; - let api = Api::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?; @@ -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..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, Api, 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 Api { - get_arg!(powerwall_status, PowerwallStatus, "/powerwalls/{}/status", PowerwallId); - get_args!(powerwall_energy_history, PowerwallEnergyHistory, "/powerwalls/{}/energyhistory", PowerwallEnergyHistoryValues); +impl OwnerApi { + 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 defe63f..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, Api}; +use crate::{pub_get, OwnerApi}; use derive_more::Display; use serde::{Deserialize, Serialize}; use std::str::FromStr; #[rustfmt::skip] -impl Api { - get!(products, Vec, "/products"); +impl OwnerApi { + pub_get!(products, Vec, "/products"); } #[derive(Debug, Clone, Deserialize, Display)] @@ -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..0a690ab 100644 --- a/src/vehicles.rs +++ b/src/vehicles.rs @@ -2,11 +2,14 @@ /// /// 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, VehicleApi, + VehicleId, +}; use serde::{Deserialize, Serialize}; #[rustfmt::skip] -impl Api { +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); @@ -412,7 +415,7 @@ pub struct SetScheduledDeparture { #[cfg(test)] mod tests { use super::*; - use crate::RequestData; + use crate::{PrintResponses, RequestData}; #[test] fn json_charge_state() { @@ -482,7 +485,8 @@ 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(), PrintResponses::Pretty) + .unwrap(); } #[test] @@ -529,7 +533,8 @@ 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(), PrintResponses::Pretty) + .unwrap(); } #[test] @@ -556,7 +561,8 @@ 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(), PrintResponses::Pretty) + .unwrap(); } #[test] @@ -578,7 +584,8 @@ 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(), PrintResponses::Pretty) + .unwrap(); } #[test] @@ -628,7 +635,8 @@ 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(), PrintResponses::Pretty) + .unwrap(); } #[test] @@ -703,7 +711,8 @@ 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(), PrintResponses::Pretty) + .unwrap(); } #[test] @@ -713,7 +722,8 @@ 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(), PrintResponses::Pretty) + .unwrap(); } #[test] @@ -723,7 +733,8 @@ 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(), PrintResponses::Pretty) + .unwrap(); } #[test] @@ -733,7 +744,8 @@ 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(), PrintResponses::Pretty) + .unwrap(); } #[test] @@ -743,7 +755,8 @@ 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(), PrintResponses::Pretty) + .unwrap(); } #[test] @@ -753,6 +766,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(), PrintResponses::Pretty) + .unwrap(); } }