refactor!: Rename Api to OwnerApi. Remove ResponseData<T>. 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.
This commit is contained in:
gak 2023-10-22 09:17:32 +11:00
parent a8b58e1157
commit 8c059769ee
No known key found for this signature in database
11 changed files with 265 additions and 167 deletions

View file

@ -1,7 +1,7 @@
use std::env; use std::env;
use teslatte::auth::AccessToken; use teslatte::auth::AccessToken;
use teslatte::products::Product; use teslatte::products::Product;
use teslatte::OwnerApi; use teslatte::{OwnerApi, VehicleApi};
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
@ -21,7 +21,7 @@ async fn main() {
if !vehicles.is_empty() { if !vehicles.is_empty() {
let vehicle_data = api.vehicle_data(&vehicles[0].id).await.unwrap(); let vehicle_data = api.vehicle_data(&vehicles[0].id).await.unwrap();
dbg!(&*vehicle_data); dbg!(&vehicle_data);
} else { } else {
println!("No vehicles found!"); println!("No vehicles found!");
} }
@ -38,13 +38,13 @@ async fn main() {
Product::Solar(e) => { Product::Solar(e) => {
let site_info = api.energy_sites_site_info(&e.energy_site_id).await.unwrap(); let site_info = api.energy_sites_site_info(&e.energy_site_id).await.unwrap();
dbg!(&*site_info); dbg!(&site_info);
let live_info = api let live_info = api
.energy_sites_live_status(&e.energy_site_id) .energy_sites_live_status(&e.energy_site_id)
.await .await
.unwrap(); .unwrap();
dbg!(&*live_info); dbg!(&live_info);
} }
Product::Powerwall(p) => { Product::Powerwall(p) => {
@ -52,7 +52,7 @@ async fn main() {
.energy_sites_live_status(&p.energy_site_id) .energy_sites_live_status(&p.energy_site_id)
.await .await
.unwrap(); .unwrap();
dbg!(&*live_info); dbg!(&live_info);
} }
} }
} }

View file

@ -1,37 +1,3 @@
pub mod energy; pub mod energy;
pub mod powerwall; pub mod powerwall;
pub mod vehicle; pub mod vehicle;
use crate::error::TeslatteError;
use crate::ResponseData;
use std::process::exit;
pub fn print_json<T>(result: Result<ResponseData<T>, 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<T>(data: ResponseData<T>) {
// 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);
}
}

View file

@ -1,4 +1,3 @@
use crate::cli::print_json;
use crate::energy_sites::{CalendarHistoryValues, HistoryKind, HistoryPeriod}; use crate::energy_sites::{CalendarHistoryValues, HistoryKind, HistoryPeriod};
use crate::products::EnergySiteId; use crate::products::EnergySiteId;
use crate::OwnerApi; use crate::OwnerApi;
@ -26,13 +25,13 @@ impl EnergySiteArgs {
pub async fn run(&self, api: &OwnerApi) -> miette::Result<()> { pub async fn run(&self, api: &OwnerApi) -> miette::Result<()> {
match &self.command { match &self.command {
EnergySiteCommand::SiteStatus => { EnergySiteCommand::SiteStatus => {
print_json(api.energy_sites_site_status(&self.id).await); api.energy_sites_site_status(&self.id).await?;
} }
EnergySiteCommand::LiveStatus => { EnergySiteCommand::LiveStatus => {
print_json(api.energy_sites_live_status(&self.id).await); api.energy_sites_live_status(&self.id).await?;
} }
EnergySiteCommand::SiteInfo => { EnergySiteCommand::SiteInfo => {
print_json(api.energy_sites_site_info(&self.id).await); api.energy_sites_site_info(&self.id).await?;
} }
EnergySiteCommand::CalendarHistory(args) => { EnergySiteCommand::CalendarHistory(args) => {
let start_date = args let start_date = args
@ -54,7 +53,7 @@ impl EnergySiteArgs {
start_date, start_date,
end_date, end_date,
}; };
print_json(api.energy_sites_calendar_history(&values).await); api.energy_sites_calendar_history(&values).await?;
} }
} }
Ok(()) Ok(())

View file

@ -1,4 +1,3 @@
use crate::cli::print_json_data;
use crate::energy_sites::{HistoryKind, HistoryPeriod}; use crate::energy_sites::{HistoryKind, HistoryPeriod};
use crate::powerwall::{PowerwallEnergyHistoryValues, PowerwallId}; use crate::powerwall::{PowerwallEnergyHistoryValues, PowerwallId};
use crate::OwnerApi; use crate::OwnerApi;
@ -24,10 +23,9 @@ impl PowerwallArgs {
pub async fn run(&self, api: &OwnerApi) -> miette::Result<()> { pub async fn run(&self, api: &OwnerApi) -> miette::Result<()> {
match self.command { match self.command {
PowerwallCommand::Status => { PowerwallCommand::Status => {
print_json_data(api.powerwall_status(&self.id).await?); api.powerwall_status(&self.id).await?;
} }
PowerwallCommand::History => { PowerwallCommand::History => {
print_json_data(
api.powerwall_energy_history(&PowerwallEnergyHistoryValues { api.powerwall_energy_history(&PowerwallEnergyHistoryValues {
powerwall_id: self.id.clone(), powerwall_id: self.id.clone(),
period: HistoryPeriod::Day, period: HistoryPeriod::Day,
@ -35,8 +33,7 @@ impl PowerwallArgs {
start_date: None, start_date: None,
end_date: None, end_date: None,
}) })
.await?, .await?;
);
} }
} }
Ok(()) Ok(())

View file

@ -1,8 +1,7 @@
use crate::cli::print_json;
use crate::vehicles::{ use crate::vehicles::{
SetChargeLimit, SetChargingAmps, SetScheduledCharging, SetScheduledDeparture, SetTemperatures, SetChargeLimit, SetChargingAmps, SetScheduledCharging, SetScheduledDeparture, SetTemperatures,
}; };
use crate::{OwnerApi, VehicleId}; use crate::{OwnerApi, VehicleApi, VehicleId};
use clap::{Args, Subcommand}; use clap::{Args, Subcommand};
#[derive(Debug, Subcommand)] #[derive(Debug, Subcommand)]
@ -77,61 +76,61 @@ impl VehicleArgs {
pub async fn run(self, api: &OwnerApi) -> miette::Result<()> { pub async fn run(self, api: &OwnerApi) -> miette::Result<()> {
match self.command { match self.command {
VehicleCommand::VehicleData => { VehicleCommand::VehicleData => {
print_json(api.vehicle_data(&self.id).await); api.vehicle_data(&self.id).await?;
} }
VehicleCommand::SetChargeLimit(limit) => { 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) => { 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 => { VehicleCommand::ChargeStart => {
print_json(api.charge_start(&self.id).await); api.charge_start(&self.id).await?;
} }
VehicleCommand::ChargeStop => { VehicleCommand::ChargeStop => {
print_json(api.charge_stop(&self.id).await); api.charge_stop(&self.id).await?;
} }
VehicleCommand::ChargePortDoorOpen => { VehicleCommand::ChargePortDoorOpen => {
print_json(api.charge_port_door_open(&self.id).await); api.charge_port_door_open(&self.id).await?;
} }
VehicleCommand::ChargePortDoorClose => { VehicleCommand::ChargePortDoorClose => {
print_json(api.charge_port_door_close(&self.id).await); api.charge_port_door_close(&self.id).await?;
} }
VehicleCommand::ChargeStandard => { VehicleCommand::ChargeStandard => {
print_json(api.charge_standard(&self.id).await); api.charge_standard(&self.id).await?;
} }
VehicleCommand::ChargeMaxRange => { VehicleCommand::ChargeMaxRange => {
print_json(api.charge_max_range(&self.id).await); api.charge_max_range(&self.id).await?;
} }
VehicleCommand::SetScheduledCharging(charging) => { VehicleCommand::SetScheduledCharging(charging) => {
print_json(api.set_scheduled_charging(&self.id, &charging).await); api.set_scheduled_charging(&self.id, &charging).await?;
} }
VehicleCommand::SetScheduledDeparture(departure) => { VehicleCommand::SetScheduledDeparture(departure) => {
print_json(api.set_scheduled_departure(&self.id, &departure).await); api.set_scheduled_departure(&self.id, &departure).await?;
} }
VehicleCommand::HonkHorn => { VehicleCommand::HonkHorn => {
print_json(api.honk_horn(&self.id).await); api.honk_horn(&self.id).await?;
} }
VehicleCommand::FlashLights => { VehicleCommand::FlashLights => {
print_json(api.flash_lights(&self.id).await); api.flash_lights(&self.id).await?;
} }
VehicleCommand::EnableHvac => { VehicleCommand::EnableHvac => {
print_json(api.auto_conditioning_start(&self.id).await); api.auto_conditioning_start(&self.id).await?;
} }
VehicleCommand::DisableHvac => { VehicleCommand::DisableHvac => {
print_json(api.auto_conditioning_stop(&self.id).await); api.auto_conditioning_stop(&self.id).await?;
} }
VehicleCommand::HvacTemperature(temps) => { VehicleCommand::HvacTemperature(temps) => {
print_json(api.set_temps(&self.id, &temps).await); api.set_temps(&self.id, &temps).await?;
} }
VehicleCommand::DoorUnlock => { VehicleCommand::DoorUnlock => {
print_json(api.door_unlock(&self.id).await); api.door_unlock(&self.id).await?;
} }
VehicleCommand::DoorLock => { VehicleCommand::DoorLock => {
print_json(api.door_lock(&self.id).await); api.door_lock(&self.id).await?;
} }
VehicleCommand::RemoteStartDrive => { VehicleCommand::RemoteStartDrive => {
print_json(api.remote_start_drive(&self.id).await); api.remote_start_drive(&self.id).await?;
} }
} }
Ok(()) Ok(())

View file

@ -1,15 +1,15 @@
use crate::products::EnergySiteId; 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 chrono::{DateTime, FixedOffset};
use serde::Deserialize; use serde::Deserialize;
use strum::{Display, EnumString, IntoStaticStr}; use strum::{Display, EnumString, IntoStaticStr};
#[rustfmt::skip] #[rustfmt::skip]
impl OwnerApi { impl OwnerApi {
get_arg!(energy_sites_site_status, SiteStatus, "/energy_sites/{}/site_status", EnergySiteId); pub_get_arg!(energy_sites_site_status, SiteStatus, "/energy_sites/{}/site_status", EnergySiteId);
get_arg!(energy_sites_live_status, LiveStatus, "/energy_sites/{}/live_status", EnergySiteId); pub_get_arg!(energy_sites_live_status, LiveStatus, "/energy_sites/{}/live_status", EnergySiteId);
get_arg!(energy_sites_site_info, SiteInfo, "/energy_sites/{}/site_info", EnergySiteId); pub_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_args!(energy_sites_calendar_history, CalendarHistory, "/energy_sites/{}/calendar_history", CalendarHistoryValues);
} }
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
@ -22,8 +22,10 @@ pub struct SiteStatus {
pub gateway_id: String, pub gateway_id: String,
pub percentage_charged: f64, pub percentage_charged: f64,
pub powerwall_onboarding_settings_set: bool, pub powerwall_onboarding_settings_set: bool,
pub powerwall_tesla_electric_interested_in: Option<()>, // TODO: Unknown type. Was null. // TODO: Unknown type. Was null.
pub resource_type: String, // battery pub powerwall_tesla_electric_interested_in: Option<()>,
// battery
pub resource_type: String,
pub site_name: String, pub site_name: String,
pub storm_mode_enabled: bool, pub storm_mode_enabled: bool,
pub sync_grid_alert_enabled: bool, pub sync_grid_alert_enabled: bool,

View file

@ -1,5 +1,11 @@
#![feature(async_fn_in_trait)]
use crate::auth::{AccessToken, RefreshToken}; use crate::auth::{AccessToken, RefreshToken};
use crate::error::TeslatteError; use crate::error::TeslatteError;
use crate::vehicles::{
SetChargeLimit, SetChargingAmps, SetScheduledCharging, SetScheduledDeparture, SetTemperatures,
Vehicle, VehicleData,
};
use chrono::{DateTime, SecondsFormat, TimeZone}; use chrono::{DateTime, SecondsFormat, TimeZone};
use derive_more::{Display, FromStr}; use derive_more::{Display, FromStr};
use reqwest::Client; use reqwest::Client;
@ -19,6 +25,76 @@ pub mod cli;
const API_URL: &str = "https://owner-api.teslamotors.com/api/1"; const API_URL: &str = "https://owner-api.teslamotors.com/api/1";
pub trait VehicleApi {
async fn vehicles(&self) -> Result<Vec<Vehicle>, TeslatteError>;
async fn vehicle_data(&self, vehicle_id: &VehicleId) -> Result<VehicleData, TeslatteError>;
async fn wake_up(&self, vehicle_id: &VehicleId) -> Result<PostResponse, TeslatteError>;
// Alerts
async fn honk_horn(&self, vehicle_id: &VehicleId) -> Result<PostResponse, TeslatteError>;
async fn flash_lights(&self, vehicle_id: &VehicleId) -> Result<PostResponse, TeslatteError>;
// Charging
async fn charge_port_door_open(
&self,
vehicle_id: &VehicleId,
) -> Result<PostResponse, TeslatteError>;
async fn charge_port_door_close(
&self,
vehicle_id: &VehicleId,
) -> Result<PostResponse, TeslatteError>;
async fn set_charge_limit(
&self,
vehicle_id: &VehicleId,
data: &SetChargeLimit,
) -> Result<PostResponse, TeslatteError>;
async fn set_charging_amps(
&self,
vehicle_id: &VehicleId,
data: &SetChargingAmps,
) -> Result<PostResponse, TeslatteError>;
async fn charge_standard(&self, vehicle_id: &VehicleId) -> Result<PostResponse, TeslatteError>;
async fn charge_max_range(&self, vehicle_id: &VehicleId)
-> Result<PostResponse, TeslatteError>;
async fn charge_start(&self, vehicle_id: &VehicleId) -> Result<PostResponse, TeslatteError>;
async fn charge_stop(&self, vehicle_id: &VehicleId) -> Result<PostResponse, TeslatteError>;
async fn set_scheduled_charging(
&self,
vehicle_id: &VehicleId,
data: &SetScheduledCharging,
) -> Result<PostResponse, TeslatteError>;
async fn set_scheduled_departure(
&self,
vehicle_id: &VehicleId,
data: &SetScheduledDeparture,
) -> Result<PostResponse, TeslatteError>;
// HVAC
async fn auto_conditioning_start(
&self,
vehicle_id: &VehicleId,
) -> Result<PostResponse, TeslatteError>;
async fn auto_conditioning_stop(
&self,
vehicle_id: &VehicleId,
) -> Result<PostResponse, TeslatteError>;
async fn set_temps(
&self,
vehicle_id: &VehicleId,
data: &SetTemperatures,
) -> Result<PostResponse, TeslatteError>;
// Doors
async fn door_unlock(&self, vehicle_id: &VehicleId) -> Result<PostResponse, TeslatteError>;
async fn door_lock(&self, vehicle_id: &VehicleId) -> Result<PostResponse, TeslatteError>;
async fn remote_start_drive(
&self,
vehicle_id: &VehicleId,
) -> Result<PostResponse, TeslatteError>;
}
trait EnergySitesApi {}
trait Values { trait Values {
fn format(&self, url: &str) -> String; 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. /// API client for the Tesla API.
/// ///
/// Main entry point for the API. It contains the access token and refresh token, and can be used /// 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 struct OwnerApi {
pub access_token: AccessToken, pub access_token: AccessToken,
pub refresh_token: Option<RefreshToken>, pub refresh_token: Option<RefreshToken>,
pub print_responses: PrintResponses,
client: Client, client: Client,
} }
@ -64,6 +148,7 @@ impl OwnerApi {
OwnerApi { OwnerApi {
access_token, access_token,
refresh_token, refresh_token,
print_responses: PrintResponses::No,
client: Client::builder() client: Client::builder()
.timeout(std::time::Duration::from_secs(10)) .timeout(std::time::Duration::from_secs(10))
.build() .build()
@ -71,14 +156,14 @@ impl OwnerApi {
} }
} }
async fn get<D>(&self, url: &str) -> Result<ResponseData<D>, TeslatteError> async fn get<D>(&self, url: &str) -> Result<D, TeslatteError>
where where
D: for<'de> Deserialize<'de> + Debug, D: for<'de> Deserialize<'de> + Debug,
{ {
self.request(&RequestData::Get { url }).await self.request(&RequestData::Get { url }).await
} }
async fn post<S>(&self, url: &str, body: S) -> Result<ResponseData<PostResponse>, TeslatteError> async fn post<S>(&self, url: &str, body: S) -> Result<PostResponse, TeslatteError>
where where
S: Serialize + Debug, S: Serialize + Debug,
{ {
@ -87,22 +172,19 @@ impl OwnerApi {
let request_data = RequestData::Post { url, payload }; let request_data = RequestData::Post { url, payload };
let data = self.request::<PostResponse>(&request_data).await?; let data = self.request::<PostResponse>(&request_data).await?;
if !data.data.result { if !data.result {
return Err(TeslatteError::ServerError { return Err(TeslatteError::ServerError {
request: format!("{request_data}"), request: format!("{request_data}"),
msg: data.data.reason,
description: None, description: None,
body: Some(data.body), msg: data.reason,
body: None,
}); });
} }
Ok(data) Ok(data)
} }
async fn request<T>( async fn request<T>(&self, request_data: &RequestData<'_>) -> Result<T, TeslatteError>
&self,
request_data: &RequestData<'_>,
) -> Result<ResponseData<T>, TeslatteError>
where where
T: for<'de> Deserialize<'de> + Debug, T: for<'de> Deserialize<'de> + Debug,
{ {
@ -138,16 +220,27 @@ impl OwnerApi {
debug!("Response: {response_body}"); debug!("Response: {response_body}");
Self::parse_json(request_data, response_body) Self::parse_json(request_data, response_body, self.print_responses)
} }
fn parse_json<T>( fn parse_json<T>(
request_data: &RequestData, request_data: &RequestData,
response_body: String, response_body: String,
) -> Result<ResponseData<T>, TeslatteError> print_response: PrintResponses,
) -> Result<T, TeslatteError>
where where
T: for<'de> Deserialize<'de> + Debug, T: for<'de> Deserialize<'de> + Debug,
{ {
match print_response {
PrintResponses::No => {}
PrintResponses::Plain => {
println!("{}", response_body);
}
PrintResponses::Pretty => {
print_json_str(&response_body);
}
}
let response: Response<T> = serde_json::from_str::<ResponseDeserializer<T>>(&response_body) let response: Response<T> = serde_json::from_str::<ResponseDeserializer<T>>(&response_body)
.map_err(|source| TeslatteError::DecodeJsonError { .map_err(|source| TeslatteError::DecodeJsonError {
source, source,
@ -157,10 +250,7 @@ impl OwnerApi {
.into(); .into();
match response { match response {
Response::Response(data) => Ok(ResponseData { Response::Response(data) => Ok(data),
data,
body: response_body,
}),
Response::Error(e) => Err(TeslatteError::ServerError { Response::Error(e) => Err(TeslatteError::ServerError {
request: format!("{request_data}"), request: format!("{request_data}"),
msg: e.error, msg: e.error,
@ -212,57 +302,41 @@ struct ResponseError {
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
struct Empty {} 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<T> {
data: T,
body: String,
}
impl<T> ResponseData<T> {
pub fn data(&self) -> &T {
&self.data
}
pub fn body(&self) -> &str {
&self.body
}
}
impl<T> std::ops::Deref for ResponseData<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.data
}
}
/// GET /api/1/[url] /// GET /api/1/[url]
macro_rules! get { macro_rules! get {
($name:ident, $return_type:ty, $url:expr) => { ($name:ident, $return_type:ty, $url:expr) => {
pub async fn $name( async fn $name(&self) -> Result<$return_type, crate::error::TeslatteError> {
&self,
) -> Result<crate::ResponseData<$return_type>, crate::error::TeslatteError> {
let url = format!("{}{}", crate::API_URL, $url); 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; 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. /// GET /api/1/[url] with an argument.
/// ///
/// Pass in the URL as a format string with one arg, which has to impl Display. /// Pass in the URL as a format string with one arg, which has to impl Display.
macro_rules! get_arg { macro_rules! get_arg {
($name:ident, $return_type:ty, $url:expr, $arg_type:ty) => { ($name:ident, $return_type:ty, $url:expr, $arg_type:ty) => {
pub async fn $name( async fn $name(
&self, &self,
arg: &$arg_type, arg: &$arg_type,
) -> miette::Result<crate::ResponseData<$return_type>, crate::error::TeslatteError> { ) -> miette::Result<$return_type, crate::error::TeslatteError> {
let url = format!($url, arg); let url = format!($url, arg);
let url = format!("{}{}", crate::API_URL, url); let url = format!("{}{}", crate::API_URL, url);
self.get(&url).await self.get(&url).await
@ -271,29 +345,61 @@ macro_rules! get_arg {
} }
pub(crate) use get_arg; pub(crate) use get_arg;
/// GET /api/1/[url] with a struct. /// Public variant of get_arg.
macro_rules! get_args { macro_rules! pub_get_arg {
($name:ident, $return_type:ty, $url:expr, $args:ty) => { ($name:ident, $return_type:ty, $url:expr, $arg_type:ty) => {
pub async fn $name( 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, &self,
values: &$args, values: &$args,
) -> miette::Result<crate::ResponseData<$return_type>, crate::error::TeslatteError> { ) -> miette::Result<$return_type, crate::error::TeslatteError> {
let url = values.format($url); let url = values.format($url);
let url = format!("{}{}", crate::API_URL, url); let url = format!("{}{}", crate::API_URL, url);
self.get(&url).await 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; 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 /// POST /api/1/[url] with an argument and data
macro_rules! post_arg { macro_rules! post_arg {
($name:ident, $request_type:ty, $url:expr, $arg_type:ty) => { ($name:ident, $request_type:ty, $url:expr, $arg_type:ty) => {
pub async fn $name( async fn $name(
&self, &self,
arg: &$arg_type, arg: &$arg_type,
data: &$request_type, data: &$request_type,
) -> miette::Result<crate::ResponseData<crate::PostResponse>, crate::error::TeslatteError> { ) -> miette::Result<crate::PostResponse, crate::error::TeslatteError> {
let url = format!($url, arg); let url = format!($url, arg);
let url = format!("{}{}", crate::API_URL, url); let url = format!("{}{}", crate::API_URL, url);
self.post(&url, data).await 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. /// Post like above but with an empty body using the Empty struct.
macro_rules! post_arg_empty { macro_rules! post_arg_empty {
($name:ident, $url:expr, $arg_type:ty) => { ($name:ident, $url:expr, $arg_type:ty) => {
pub async fn $name( async fn $name(
&self, &self,
arg: &$arg_type, arg: &$arg_type,
) -> miette::Result<crate::ResponseData<crate::PostResponse>, crate::error::TeslatteError> { ) -> miette::Result<crate::PostResponse, crate::error::TeslatteError> {
let url = format!($url, arg); let url = format!($url, arg);
let url = format!("{}{}", crate::API_URL, url); let url = format!("{}{}", crate::API_URL, url);
self.post(&url, &Empty {}).await self.post(&url, &Empty {}).await
@ -333,6 +439,19 @@ pub(crate) fn join_query_pairs(pairs: &[(&str, String)]) -> String {
.join("&") .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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -350,7 +469,11 @@ mod tests {
payload: "doesn't matter", payload: "doesn't matter",
}; };
let e = OwnerApi::parse_json::<ChargeState>(&request_data, s.to_string()); let e = OwnerApi::parse_json::<ChargeState>(
&request_data,
s.to_string(),
PrintResponses::Pretty,
);
if let Err(e) = e { if let Err(e) = e {
if let TeslatteError::ServerError { if let TeslatteError::ServerError {
msg, description, .. msg, description, ..

View file

@ -3,9 +3,8 @@ use serde::{Deserialize, Serialize};
use teslatte::auth::{AccessToken, RefreshToken}; use teslatte::auth::{AccessToken, RefreshToken};
use teslatte::cli::energy::EnergySiteArgs; use teslatte::cli::energy::EnergySiteArgs;
use teslatte::cli::powerwall::PowerwallArgs; use teslatte::cli::powerwall::PowerwallArgs;
use teslatte::cli::print_json;
use teslatte::cli::vehicle::VehicleArgs; use teslatte::cli::vehicle::VehicleArgs;
use teslatte::OwnerApi; use teslatte::{OwnerApi, PrintResponses, VehicleApi};
/// Teslatte /// 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 { match api_args.command {
ApiCommand::Vehicles => { ApiCommand::Vehicles => {
print_json(api.vehicles().await); api.vehicles().await?;
} }
ApiCommand::Vehicle(v) => { ApiCommand::Vehicle(v) => {
v.run(&api).await?; v.run(&api).await?;
} }
ApiCommand::Products => { ApiCommand::Products => {
print_json(api.products().await); api.products().await?;
} }
ApiCommand::EnergySite(e) => { ApiCommand::EnergySite(e) => {
e.run(&api).await?; e.run(&api).await?;

View file

@ -1,14 +1,14 @@
use crate::energy_sites::{HistoryKind, HistoryPeriod}; use crate::energy_sites::{HistoryKind, HistoryPeriod};
use crate::products::GatewayId; 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 chrono::{DateTime, FixedOffset};
use derive_more::{Display, FromStr}; use derive_more::{Display, FromStr};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[rustfmt::skip] #[rustfmt::skip]
impl OwnerApi { impl OwnerApi {
get_arg!(powerwall_status, PowerwallStatus, "/powerwalls/{}/status", PowerwallId); pub_get_arg!(powerwall_status, PowerwallStatus, "/powerwalls/{}/status", PowerwallId);
get_args!(powerwall_energy_history, PowerwallEnergyHistory, "/powerwalls/{}/energyhistory", PowerwallEnergyHistoryValues); pub_get_args!(powerwall_energy_history, PowerwallEnergyHistory, "/powerwalls/{}/energyhistory", PowerwallEnergyHistoryValues);
} }
#[derive(Debug, Clone, Serialize, Deserialize, Display, FromStr)] #[derive(Debug, Clone, Serialize, Deserialize, Display, FromStr)]

View file

@ -1,14 +1,14 @@
use crate::error::TeslatteError; use crate::error::TeslatteError;
use crate::powerwall::PowerwallId; use crate::powerwall::PowerwallId;
use crate::vehicles::VehicleData; use crate::vehicles::VehicleData;
use crate::{get, OwnerApi}; use crate::{pub_get, OwnerApi};
use derive_more::Display; use derive_more::Display;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::str::FromStr; use std::str::FromStr;
#[rustfmt::skip] #[rustfmt::skip]
impl OwnerApi { impl OwnerApi {
get!(products, Vec<Product>, "/products"); pub_get!(products, Vec<Product>, "/products");
} }
#[derive(Debug, Clone, Deserialize, Display)] #[derive(Debug, Clone, Deserialize, Display)]

View file

@ -3,12 +3,13 @@
/// Sometimes the API will return a null for a field where I've put in a non Option type, which /// Sometimes the API will return a null for a field where I've put in a non Option type, which
/// will cause the deserializer to fail. Please log an issue to fix these if you come across it. /// will cause the deserializer to fail. Please log an issue to fix these if you come across it.
use crate::{ use crate::{
get, 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}; use serde::{Deserialize, Serialize};
#[rustfmt::skip] #[rustfmt::skip]
impl OwnerApi { impl VehicleApi for OwnerApi {
get!(vehicles, Vec<Vehicle>, "/vehicles"); get!(vehicles, Vec<Vehicle>, "/vehicles");
get_arg!(vehicle_data, VehicleData, "/vehicles/{}/vehicle_data", VehicleId); get_arg!(vehicle_data, VehicleData, "/vehicles/{}/vehicle_data", VehicleId);
post_arg_empty!(wake_up, "/vehicles/{}/command/wake_up", VehicleId); post_arg_empty!(wake_up, "/vehicles/{}/command/wake_up", VehicleId);
@ -414,7 +415,7 @@ pub struct SetScheduledDeparture {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::RequestData; use crate::{PrintResponses, RequestData};
#[test] #[test]
fn json_charge_state() { fn json_charge_state() {
@ -484,7 +485,8 @@ mod tests {
let request_data = RequestData::Get { let request_data = RequestData::Get {
url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/data_request/charge_state", url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/data_request/charge_state",
}; };
OwnerApi::parse_json::<ChargeState>(&request_data, s.to_string()).unwrap(); OwnerApi::parse_json::<ChargeState>(&request_data, s.to_string(), PrintResponses::Pretty)
.unwrap();
} }
#[test] #[test]
@ -531,7 +533,8 @@ mod tests {
let request_data = RequestData::Get { let request_data = RequestData::Get {
url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/data_request/climate_state", url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/data_request/climate_state",
}; };
OwnerApi::parse_json::<ClimateState>(&request_data, s.to_string()).unwrap(); OwnerApi::parse_json::<ClimateState>(&request_data, s.to_string(), PrintResponses::Pretty)
.unwrap();
} }
#[test] #[test]
@ -558,7 +561,8 @@ mod tests {
let request_data = RequestData::Get { let request_data = RequestData::Get {
url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/data_request/drive_state", url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/data_request/drive_state",
}; };
OwnerApi::parse_json::<DriveState>(&request_data, s.to_string()).unwrap(); OwnerApi::parse_json::<DriveState>(&request_data, s.to_string(), PrintResponses::Pretty)
.unwrap();
} }
#[test] #[test]
@ -580,7 +584,8 @@ mod tests {
let request_data = RequestData::Get { let request_data = RequestData::Get {
url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/data_request/gui_settings", url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/data_request/gui_settings",
}; };
OwnerApi::parse_json::<GuiSettings>(&request_data, s.to_string()).unwrap(); OwnerApi::parse_json::<GuiSettings>(&request_data, s.to_string(), PrintResponses::Pretty)
.unwrap();
} }
#[test] #[test]
@ -630,7 +635,8 @@ mod tests {
let request_data = RequestData::Get { let request_data = RequestData::Get {
url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/data_request/vehicle_config", url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/data_request/vehicle_config",
}; };
OwnerApi::parse_json::<VehicleConfig>(&request_data, s.to_string()).unwrap(); OwnerApi::parse_json::<VehicleConfig>(&request_data, s.to_string(), PrintResponses::Pretty)
.unwrap();
} }
#[test] #[test]
@ -705,7 +711,8 @@ mod tests {
let request_data = RequestData::Get { let request_data = RequestData::Get {
url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/data_request/vehicle_state", url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/data_request/vehicle_state",
}; };
OwnerApi::parse_json::<VehicleState>(&request_data, s.to_string()).unwrap(); OwnerApi::parse_json::<VehicleState>(&request_data, s.to_string(), PrintResponses::Pretty)
.unwrap();
} }
#[test] #[test]
@ -715,7 +722,8 @@ mod tests {
let request_data = RequestData::Get { let request_data = RequestData::Get {
url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/vehicle_data", url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/vehicle_data",
}; };
OwnerApi::parse_json::<VehicleData>(&request_data, s.to_string()).unwrap(); OwnerApi::parse_json::<VehicleData>(&request_data, s.to_string(), PrintResponses::Pretty)
.unwrap();
} }
#[test] #[test]
@ -725,7 +733,8 @@ mod tests {
let request_data = RequestData::Get { let request_data = RequestData::Get {
url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/vehicle_data", url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/vehicle_data",
}; };
OwnerApi::parse_json::<VehicleData>(&request_data, s.to_string()).unwrap(); OwnerApi::parse_json::<VehicleData>(&request_data, s.to_string(), PrintResponses::Pretty)
.unwrap();
} }
#[test] #[test]
@ -735,7 +744,8 @@ mod tests {
let request_data = RequestData::Get { let request_data = RequestData::Get {
url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/vehicle_data", url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/vehicle_data",
}; };
OwnerApi::parse_json::<VehicleData>(&request_data, s.to_string()).unwrap(); OwnerApi::parse_json::<VehicleData>(&request_data, s.to_string(), PrintResponses::Pretty)
.unwrap();
} }
#[test] #[test]
@ -745,7 +755,8 @@ mod tests {
let request_data = RequestData::Get { let request_data = RequestData::Get {
url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/vehicle_data", url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/vehicle_data",
}; };
OwnerApi::parse_json::<VehicleData>(&request_data, s.to_string()).unwrap(); OwnerApi::parse_json::<VehicleData>(&request_data, s.to_string(), PrintResponses::Pretty)
.unwrap();
} }
#[test] #[test]
@ -755,6 +766,7 @@ mod tests {
let request_data = RequestData::Get { let request_data = RequestData::Get {
url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/vehicle_data", url: "https://owner-api.teslamotors.com/api/1/vehicles/1234567890/vehicle_data",
}; };
OwnerApi::parse_json::<VehicleData>(&request_data, s.to_string()).unwrap(); OwnerApi::parse_json::<VehicleData>(&request_data, s.to_string(), PrintResponses::Pretty)
.unwrap();
} }
} }