diff --git a/examples/basic.rs b/examples/basic.rs index 03ae443..203a6e1 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,7 +1,7 @@ use clap::Parser; use std::env; use teslatte::auth::{AccessToken, Authentication}; -use teslatte::vehicle_state::SetChargeLimit; +use teslatte::vehicles::SetChargeLimit; use teslatte::Api; #[tokio::main] diff --git a/examples/cli.rs b/examples/cli.rs index f809e52..17a27e3 100644 --- a/examples/cli.rs +++ b/examples/cli.rs @@ -1,7 +1,7 @@ use clap::{Args, Parser, Subcommand}; use serde::{Deserialize, Serialize}; use teslatte::auth::{AccessToken, Authentication, RefreshToken}; -use teslatte::vehicle_state::SetChargeLimit; +use teslatte::vehicles::{SetChargeLimit, SetChargingAmps}; use teslatte::{Api, Id}; const TESLA_ACCESS_TOKEN: &str = "TESLA_ACCESS_TOKEN"; @@ -55,14 +55,37 @@ enum ApiCommand { /// Get a list of vehicles. Vehicles, + /// Specific Vehicle. + Vehicle(Vehicle), +} + +#[derive(Debug, Args)] +struct Vehicle { + pub id: Id, + + #[clap(subcommand)] + pub command: VehicleCommand, +} + +#[derive(Debug, Subcommand)] +enum VehicleCommand { /// Get vehicle data. - VehicleData { id: Id }, + Data, /// Get charge state. - ChargeState { id: Id }, + ChargeState, /// Set charge limit. - SetChargeLimit { id: Id, percent: u8 }, + SetChargeLimit { percent: u8 }, + + /// Set charge amps. + SetChargingAmps { charging_amps: i64 }, + + /// Start charging. + ChargeStart, + + /// Stop charging. + ChargeStop, } #[tokio::main] @@ -89,37 +112,55 @@ async fn main() { updated_tokens(save, response.access_token, refresh_token); } Command::Api(api_args) => { - let access_token = match api_args.access_token { - Some(a) => a, + let access_token = match &api_args.access_token { + Some(a) => a.clone(), None => { let config = Config::load(); - config.access_token + config.access_token.clone() } }; let api = Api::new(&access_token); - #[allow(unused_results)] match api_args.command { ApiCommand::Vehicles => { - dbg!(api.vehicles().await.unwrap()); - } - ApiCommand::VehicleData { id } => { - dbg!(api.vehicle_data(&id).await.unwrap()); - } - ApiCommand::ChargeState { id } => { - dbg!(api.charge_state(&id).await.unwrap()); - } - ApiCommand::SetChargeLimit { id, percent } => { - dbg!(api - .set_charge_limit(&id, &SetChargeLimit { percent }) - .await - .unwrap()); + let vehicles = api.vehicles().await.unwrap(); + dbg!(&vehicles); } + ApiCommand::Vehicle(v) => vehicles(&api, v).await, } } } } +async fn vehicles(api: &Api, vehicle: Vehicle) { + match vehicle.command { + VehicleCommand::Data => { + dbg!(api.vehicle_data(&vehicle.id).await.unwrap()); + } + VehicleCommand::ChargeState => { + dbg!(api.charge_state(&vehicle.id).await.unwrap()); + } + VehicleCommand::SetChargeLimit { percent } => { + dbg!(api + .set_charge_limit(&vehicle.id, &SetChargeLimit { percent }) + .await + .unwrap()); + } + VehicleCommand::SetChargingAmps { charging_amps } => { + dbg!(api + .set_charging_amps(&vehicle.id, &SetChargingAmps { charging_amps }) + .await + .unwrap()); + } + VehicleCommand::ChargeStart => { + dbg!(api.charge_start(&vehicle.id).await.unwrap()); + } + VehicleCommand::ChargeStop => { + dbg!(api.charge_stop(&vehicle.id).await.unwrap()); + } + } +} + fn updated_tokens(save: bool, access_token: AccessToken, refresh_token: RefreshToken) { println!("Access token: {}", access_token.0); println!("Refresh token: {}", refresh_token.0); diff --git a/src/lib.rs b/src/lib.rs index 86a9281..e5110ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,7 @@ use tracing::{debug, instrument, trace}; pub mod auth; pub mod error; -pub mod vehicle_state; +pub mod vehicles; const API_URL: &str = "https://owner-api.teslamotors.com"; @@ -209,6 +209,7 @@ macro_rules! get { } }; } +pub(crate) use get; /// GET /api/1/[url] with an argument. /// @@ -216,11 +217,13 @@ macro_rules! get { macro_rules! get_arg { ($name:ident, $return_type:ty, $url:expr, $arg_type:ty) => { pub async fn $name(&self, arg: &$arg_type) -> miette::Result<$return_type, TeslatteError> { - let url = format!(concat!("/api/1", $url), arg); + let url = format!($url, arg); + let url = format!("/api/1{}", url); self.get(&url).await } }; } +pub(crate) use get_arg; /// POST /api/1/[url] with an argument and data macro_rules! post_arg { @@ -230,122 +233,32 @@ macro_rules! post_arg { arg: &$arg_type, data: &$request_type, ) -> miette::Result<(), TeslatteError> { - let url_fmt = format!($url, arg); - let url = format!(concat!("/api/1", $url), arg); + let url = format!($url, arg); + let url = format!("/api/1{}", url); self.post(&url, data).await } }; } +pub(crate) use post_arg; -// /// POST /api/1/vehicles/[id]/... without data -// macro_rules! post_v { -// ($name:ident, $url:expr) => { -// pub async fn $name(&self, id: &Id) -> miette::Result<(), TeslatteError> { -// let url = format!("/vehicles/{}{}", id.0, $url); -// self.post(&url, &Empty {}).await -// } -// }; -// } -// -// /// POST /api/1/vehicles/[id]/... with data -// macro_rules! post_vd { -// ($name:ident, $struct:ty, $url:expr) => { -// pub async fn $name(&self, id: &Id, data: &$struct) -> miette::Result<(), TeslatteError> { -// let url = format!("/api/1/vehicles/{}{}", id.0, $url); -// self.post(&url, &data).await -// } -// }; -// } - -use crate::vehicle_state::ChargeState; -use crate::vehicle_state::SetChargeLimit; -use crate::vehicle_state::Vehicle; -use crate::vehicle_state::VehicleData; - -#[rustfmt::skip] -impl Api { - get!(vehicles, Vec, "/vehicles"); - get_arg!(vehicle_data, VehicleData, "/vehicles/{}/vehicle_data", Id); - get_arg!(charge_state, ChargeState, "/vehicles/{}/data_request/charge_state", Id); - post_arg!(set_charge_limit, SetChargeLimit, "/vehicles/{}/command/set_charge_limit", Id); - // post_vd!(set_charging_amps, SetChargingAmps, "/command/set_charging_amps"); - // post_v!(charge_start, "/command/charge_start"); - // post_v!(charge_stop, "/command/charge_stop"); +/// 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(&self, arg: &$arg_type) -> miette::Result<(), TeslatteError> { + let url = format!($url, arg); + let url = format!("/api/1{}", url); + self.post(&url, &Empty {}).await + } + }; } +pub(crate) use post_arg_empty; #[cfg(test)] mod tests { use super::*; - use crate::vehicle_state::ChargeState; + use crate::vehicles::ChargeState; use test_log::test; - #[test] - fn json() { - let s = r#" - { - "response": { - "battery_heater_on": false, - "battery_level": 50, - "battery_range": 176.08, - "charge_amps": 5, - "charge_current_request": 5, - "charge_current_request_max": 16, - "charge_enable_request": true, - "charge_energy_added": 1.05, - "charge_limit_soc": 75, - "charge_limit_soc_max": 100, - "charge_limit_soc_min": 50, - "charge_limit_soc_std": 90, - "charge_miles_added_ideal": 5, - "charge_miles_added_rated": 5, - "charge_port_cold_weather_mode": false, - "charge_port_color": "", - "charge_port_door_open": true, - "charge_port_latch": "Engaged", - "charge_rate": 14.8, - "charge_to_max_range": false, - "charger_actual_current": 5, - "charger_phases": 2, - "charger_pilot_current": 16, - "charger_power": 4, - "charger_voltage": 241, - "charging_state": "Charging", - "conn_charge_cable": "IEC", - "est_battery_range": 163.81, - "fast_charger_brand": "", - "fast_charger_present": false, - "fast_charger_type": "ACSingleWireCAN", - "ideal_battery_range": 176.08, - "managed_charging_active": false, - "managed_charging_start_time": null, - "managed_charging_user_canceled": false, - "max_range_charge_counter": 0, - "minutes_to_full_charge": 350, - "not_enough_power_to_heat": null, - "off_peak_charging_enabled": false, - "off_peak_charging_times": "all_week", - "off_peak_hours_end_time": 1140, - "preconditioning_enabled": false, - "preconditioning_times": "all_week", - "scheduled_charging_mode": "StartAt", - "scheduled_charging_pending": false, - "scheduled_charging_start_time": 1647045000, - "scheduled_charging_start_time_app": 690, - "scheduled_charging_start_time_minutes": 690, - "scheduled_departure_time": 1641337200, - "scheduled_departure_time_minutes": 600, - "supercharger_session_trip_planner": false, - "time_to_full_charge": 5.83, - "timestamp": 1646978638155, - "trip_charging": false, - "usable_battery_level": 50, - "user_charge_enable_request": null - } - } - "#; - Api::parse_json::(s, || "req".to_string()).unwrap(); - } - #[test] fn error() { let s = r#"{ diff --git a/src/vehicle_state.rs b/src/vehicles.rs similarity index 75% rename from src/vehicle_state.rs rename to src/vehicles.rs index 34846a2..398cd57 100644 --- a/src/vehicle_state.rs +++ b/src/vehicles.rs @@ -1,10 +1,22 @@ +use crate::error::TeslatteError; /// Please note that these structs are generated from my own responses. /// /// Sometimes the API will return a null for a field where I've put in a non Option type, which /// will cause the deserializer to fail. Please log an issue to fix these if you come across it. -use crate::{Id, VehicleId}; +use crate::{get, get_arg, post_arg, post_arg_empty, Api, Empty, Id, VehicleId}; use serde::{Deserialize, Serialize}; +#[rustfmt::skip] +impl Api { + get!(vehicles, Vec, "/vehicles"); + get_arg!(vehicle_data, VehicleData, "/vehicles/{}/vehicle_data", Id); + get_arg!(charge_state, ChargeState, "/vehicles/{}/data_request/charge_state", Id); + post_arg!(set_charge_limit, SetChargeLimit, "/vehicles/{}/command/set_charge_limit", Id); + post_arg!(set_charging_amps, SetChargingAmps, "/vehicles/{}/command/set_charging_amps", Id); + post_arg_empty!(charge_start, "/vehicles/{}/command/charge_start", Id); + post_arg_empty!(charge_stop, "/vehicles/{}/command/charge_stop", Id); +} + #[derive(Debug, Clone, Deserialize)] pub struct VehicleData { pub id: Id, @@ -304,3 +316,70 @@ pub struct SetChargeLimit { // pub percent: Percentage, pub percent: u8, } + +#[test] +fn json() { + let s = r#" + { + "response": { + "battery_heater_on": false, + "battery_level": 50, + "battery_range": 176.08, + "charge_amps": 5, + "charge_current_request": 5, + "charge_current_request_max": 16, + "charge_enable_request": true, + "charge_energy_added": 1.05, + "charge_limit_soc": 75, + "charge_limit_soc_max": 100, + "charge_limit_soc_min": 50, + "charge_limit_soc_std": 90, + "charge_miles_added_ideal": 5, + "charge_miles_added_rated": 5, + "charge_port_cold_weather_mode": false, + "charge_port_color": "", + "charge_port_door_open": true, + "charge_port_latch": "Engaged", + "charge_rate": 14.8, + "charge_to_max_range": false, + "charger_actual_current": 5, + "charger_phases": 2, + "charger_pilot_current": 16, + "charger_power": 4, + "charger_voltage": 241, + "charging_state": "Charging", + "conn_charge_cable": "IEC", + "est_battery_range": 163.81, + "fast_charger_brand": "", + "fast_charger_present": false, + "fast_charger_type": "ACSingleWireCAN", + "ideal_battery_range": 176.08, + "managed_charging_active": false, + "managed_charging_start_time": null, + "managed_charging_user_canceled": false, + "max_range_charge_counter": 0, + "minutes_to_full_charge": 350, + "not_enough_power_to_heat": null, + "off_peak_charging_enabled": false, + "off_peak_charging_times": "all_week", + "off_peak_hours_end_time": 1140, + "preconditioning_enabled": false, + "preconditioning_times": "all_week", + "scheduled_charging_mode": "StartAt", + "scheduled_charging_pending": false, + "scheduled_charging_start_time": 1647045000, + "scheduled_charging_start_time_app": 690, + "scheduled_charging_start_time_minutes": 690, + "scheduled_departure_time": 1641337200, + "scheduled_departure_time_minutes": 600, + "supercharger_session_trip_planner": false, + "time_to_full_charge": 5.83, + "timestamp": 1646978638155, + "trip_charging": false, + "usable_battery_level": 50, + "user_charge_enable_request": null + } + } + "#; + Api::parse_json::(s, || "req".to_string()).unwrap(); +}